diff --git a/.gitattributes b/.gitattributes index 2d9e090af2..5970628bf8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,9 @@ app/src/envDev/assets/configs/config.json filter=git-crypt diff=git-crypt app/src/debug/assets/configs/config_dev.json filter=git-crypt diff=git-crypt app/src/debug/assets/configs/config_production.json filter=git-crypt diff=git-crypt app/src/debug/assets/configs/config_release.json filter=git-crypt diff=git-crypt +app/src/stageDebuggable/assets/configs/config_dev.json filter=git-crypt diff=git-crypt +app/src/stageDebuggable/assets/configs/config_production.json filter=git-crypt diff=git-crypt +app/src/stageDebuggable/assets/configs/config_release.json filter=git-crypt diff=git-crypt buildsystem/cert/** filter=git-crypt diff=git-crypt buildsystem/secret.gradle filter=git-crypt diff=git-crypt fastlane/Fastfile filter=git-crypt diff=git-crypt diff --git a/app/src/main/java/org/stepic/droid/storage/dao/CourseDaoImpl.kt b/app/src/main/java/org/stepic/droid/storage/dao/CourseDaoImpl.kt index c5a7eaa915..1c7d56ff55 100644 --- a/app/src/main/java/org/stepic/droid/storage/dao/CourseDaoImpl.kt +++ b/app/src/main/java/org/stepic/droid/storage/dao/CourseDaoImpl.kt @@ -61,6 +61,7 @@ constructor( certificateLink = cursor.getString(DbStructureCourse.Columns.CERTIFICATE_LINK), isCertificateAutoIssued = cursor.getBoolean(DbStructureCourse.Columns.IS_CERTIFICATE_AUTO_ISSUED), isCertificateIssued = cursor.getBoolean(DbStructureCourse.Columns.IS_CERTIFICATE_ISSUED), + withCertificate = cursor.getBoolean(DbStructureCourse.Columns.WITH_CERTIFICATE), lastDeadline = cursor.getString(DbStructureCourse.Columns.LAST_DEADLINE), beginDate = cursor.getString(DbStructureCourse.Columns.BEGIN_DATE), endDate = cursor.getString(DbStructureCourse.Columns.END_DATE), @@ -126,6 +127,7 @@ constructor( values.put(DbStructureCourse.Columns.CERTIFICATE_LINK, course.certificateLink) values.put(DbStructureCourse.Columns.IS_CERTIFICATE_AUTO_ISSUED, course.isCertificateAutoIssued) values.put(DbStructureCourse.Columns.IS_CERTIFICATE_ISSUED, course.isCertificateIssued) + values.put(DbStructureCourse.Columns.WITH_CERTIFICATE, course.withCertificate) values.put(DbStructureCourse.Columns.LAST_DEADLINE, course.lastDeadline) values.put(DbStructureCourse.Columns.BEGIN_DATE, course.beginDate) values.put(DbStructureCourse.Columns.END_DATE, course.endDate) diff --git a/app/src/main/java/org/stepic/droid/storage/migration/MigrationFrom70To71.kt b/app/src/main/java/org/stepic/droid/storage/migration/MigrationFrom70To71.kt new file mode 100644 index 0000000000..0ea5d267ee --- /dev/null +++ b/app/src/main/java/org/stepic/droid/storage/migration/MigrationFrom70To71.kt @@ -0,0 +1,11 @@ +package org.stepic.droid.storage.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import org.stepic.droid.storage.structure.DbStructureCourse + +object MigrationFrom70To71 : Migration(70, 71) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE ${DbStructureCourse.TABLE_NAME} ADD COLUMN ${DbStructureCourse.Columns.WITH_CERTIFICATE} INTEGER") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/storage/migration/Migrations.kt b/app/src/main/java/org/stepic/droid/storage/migration/Migrations.kt index 5c5d65e1cd..d64464780f 100644 --- a/app/src/main/java/org/stepic/droid/storage/migration/Migrations.kt +++ b/app/src/main/java/org/stepic/droid/storage/migration/Migrations.kt @@ -201,6 +201,7 @@ object Migrations { MigrationFrom66To67, MigrationFrom67To68, MigrationFrom68To69, - MigrationFrom69To70 + MigrationFrom69To70, + MigrationFrom70To71 ) } \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/storage/structure/DbStructureCourse.kt b/app/src/main/java/org/stepic/droid/storage/structure/DbStructureCourse.kt index e84588947e..bdeafa135d 100644 --- a/app/src/main/java/org/stepic/droid/storage/structure/DbStructureCourse.kt +++ b/app/src/main/java/org/stepic/droid/storage/structure/DbStructureCourse.kt @@ -48,6 +48,7 @@ object DbStructureCourse { const val CERTIFICATE_LINK = "certificate_link" const val IS_CERTIFICATE_AUTO_ISSUED = "is_certificate_auto_issued" const val IS_CERTIFICATE_ISSUED = "is_certificate_issued" + const val WITH_CERTIFICATE = "with_certificate" const val LAST_DEADLINE = "last_deadline" const val BEGIN_DATE = "begin_date" diff --git a/app/src/main/java/org/stepic/droid/ui/activities/MainFeedActivity.kt b/app/src/main/java/org/stepic/droid/ui/activities/MainFeedActivity.kt index 030f8c3eba..f0ee634eee 100644 --- a/app/src/main/java/org/stepic/droid/ui/activities/MainFeedActivity.kt +++ b/app/src/main/java/org/stepic/droid/ui/activities/MainFeedActivity.kt @@ -70,6 +70,7 @@ class MainFeedActivity : BackToExitActivityWithSmartLockBase(), private const val NOTIFICATIONS_DEEPLINK = "notifications" private const val DEBUG_BUILD_TYPE = "debug" + private const val STAGE_DEBUGGABLE_BUILD_TYPE = "stageDebuggable" const val HOME_INDEX: Int = 1 const val CATALOG_INDEX: Int = 2 @@ -289,7 +290,7 @@ class MainFeedActivity : BackToExitActivityWithSmartLockBase(), private fun initNavigation() { navigationView.setOnNavigationItemSelectedListener(::onNavigationItemSelected) navigationView.setOnNavigationItemReselectedListener(::onNavigationItemReselected) - navigationView.menu.findItem(R.id.debug).isVisible = BuildConfig.BUILD_TYPE == DEBUG_BUILD_TYPE + navigationView.menu.findItem(R.id.debug).isVisible = BuildConfig.BUILD_TYPE == DEBUG_BUILD_TYPE || BuildConfig.BUILD_TYPE == STAGE_DEBUGGABLE_BUILD_TYPE } private fun showCurrentFragment(@IdRes id: Int) { diff --git a/app/src/main/java/org/stepik/android/cache/base/database/AppDatabase.kt b/app/src/main/java/org/stepik/android/cache/base/database/AppDatabase.kt index 36c09f6071..a3cdf5fb4c 100644 --- a/app/src/main/java/org/stepik/android/cache/base/database/AppDatabase.kt +++ b/app/src/main/java/org/stepik/android/cache/base/database/AppDatabase.kt @@ -53,7 +53,7 @@ import org.stepik.android.domain.visited_courses.model.VisitedCourse ) abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION = 70 + const val VERSION = 71 const val NAME = "stepic_database.db" } diff --git a/app/src/main/java/org/stepik/android/data/course_payments/repository/CoursePaymentsRepositoryImpl.kt b/app/src/main/java/org/stepik/android/data/course_payments/repository/CoursePaymentsRepositoryImpl.kt index c5abf77baf..97bf469a76 100644 --- a/app/src/main/java/org/stepik/android/data/course_payments/repository/CoursePaymentsRepositoryImpl.kt +++ b/app/src/main/java/org/stepik/android/data/course_payments/repository/CoursePaymentsRepositoryImpl.kt @@ -7,7 +7,7 @@ import org.stepik.android.data.course_payments.source.CoursePaymentsCacheDataSou import org.stepik.android.data.course_payments.source.CoursePaymentsRemoteDataSource import org.stepik.android.domain.base.DataSourceType import org.stepik.android.domain.course_payments.model.CoursePayment -import org.stepik.android.domain.course_payments.model.PromoCode +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode import org.stepik.android.domain.course_payments.repository.CoursePaymentsRepository import ru.nobird.android.domain.rx.doCompletableOnSuccess import javax.inject.Inject @@ -36,8 +36,8 @@ constructor( throw IllegalArgumentException("Unsupported source type = $sourceType") } - override fun checkPromoCodeValidity(courseId: Long, name: String): Single = + override fun checkDeeplinkPromoCodeValidity(courseId: Long, name: String): Single = coursePaymentsRemoteDataSource - .checkPromoCodeValidity(courseId, name) - .onErrorReturnItem(PromoCode.EMPTY) + .checkDeeplinkPromoCodeValidity(courseId, name) + .onErrorReturnItem(DeeplinkPromoCode.EMPTY) } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/course_payments/source/CoursePaymentsRemoteDataSource.kt b/app/src/main/java/org/stepik/android/data/course_payments/source/CoursePaymentsRemoteDataSource.kt index 21a3c3e788..e77537f70f 100644 --- a/app/src/main/java/org/stepik/android/data/course_payments/source/CoursePaymentsRemoteDataSource.kt +++ b/app/src/main/java/org/stepik/android/data/course_payments/source/CoursePaymentsRemoteDataSource.kt @@ -4,7 +4,7 @@ import io.reactivex.Single import org.solovyev.android.checkout.Purchase import org.solovyev.android.checkout.Sku import org.stepik.android.domain.course_payments.model.CoursePayment -import org.stepik.android.domain.course_payments.model.PromoCode +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode interface CoursePaymentsRemoteDataSource { @@ -17,5 +17,5 @@ interface CoursePaymentsRemoteDataSource { */ fun getCoursePaymentsByCourseId(courseId: Long, coursePaymentStatus: CoursePayment.Status? = null): Single> - fun checkPromoCodeValidity(courseId: Long, name: String): Single + fun checkDeeplinkPromoCodeValidity(courseId: Long, name: String): Single } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/course/interactor/CourseInteractor.kt b/app/src/main/java/org/stepik/android/domain/course/interactor/CourseInteractor.kt index b83063c4b4..8bd974f56f 100644 --- a/app/src/main/java/org/stepik/android/domain/course/interactor/CourseInteractor.kt +++ b/app/src/main/java/org/stepik/android/domain/course/interactor/CourseInteractor.kt @@ -12,7 +12,7 @@ import org.stepik.android.domain.base.DataSourceType import org.stepik.android.domain.course.model.CourseHeaderData import org.stepik.android.domain.course.repository.CourseRepository import org.stepik.android.domain.course_payments.mapper.DefaultPromoCodeMapper -import org.stepik.android.domain.course_payments.model.PromoCode +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode import org.stepik.android.domain.solutions.interactor.SolutionsInteractor import org.stepik.android.domain.solutions.model.SolutionItem import org.stepik.android.domain.wishlist.model.WishlistEntity @@ -62,9 +62,8 @@ constructor( zip( courseStatsInteractor.getCourseStats(listOf(course)), solutionsInteractor.fetchAttemptCacheItems(course.id, localOnly = true), - if (promo == null) Single.just(PromoCode.EMPTY) else courseStatsInteractor.checkPromoCodeValidity(course.id, promo), + if (promo == null) Single.just(DeeplinkPromoCode.EMPTY) else courseStatsInteractor.checkDeeplinkPromoCodeValidity(course.id, promo), (requireAuthorization() then wishlistRepository.getWishlistRecord(DataSourceType.CACHE)).onErrorReturnItem(WishlistEntity.EMPTY) -// if (sharedPreferenceHelper.authResponseFromStore != null) wishlistRepository.getWishlistRecord(DataSourceType.CACHE) else Single.just(WishlistEntity.EMPTY) ) { courseStats, localSubmissions, promoCode, wishlistEntity -> CourseHeaderData( courseId = course.id, @@ -74,7 +73,7 @@ constructor( stats = courseStats.first(), localSubmissionsCount = localSubmissions.count { it is SolutionItem.SubmissionItem }, - promoCode = promoCode, + deeplinkPromoCode = promoCode, defaultPromoCode = defaultPromoCodeMapper.mapToDefaultPromoCode(course), isWishlistUpdating = false, wishlistEntity = wishlistEntity diff --git a/app/src/main/java/org/stepik/android/domain/course/interactor/CourseStatsInteractor.kt b/app/src/main/java/org/stepik/android/domain/course/interactor/CourseStatsInteractor.kt index d9620221ce..3f14d0aba7 100644 --- a/app/src/main/java/org/stepik/android/domain/course/interactor/CourseStatsInteractor.kt +++ b/app/src/main/java/org/stepik/android/domain/course/interactor/CourseStatsInteractor.kt @@ -14,7 +14,7 @@ import org.stepik.android.domain.course.model.EnrollmentState import org.stepik.android.domain.course.model.SourceTypeComposition import org.stepik.android.domain.course.repository.CourseReviewSummaryRepository import org.stepik.android.domain.course_payments.model.CoursePayment -import org.stepik.android.domain.course_payments.model.PromoCode +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode import org.stepik.android.domain.course_payments.repository.CoursePaymentsRepository import org.stepik.android.domain.progress.mapper.getProgresses import org.stepik.android.domain.profile.repository.ProfileRepository @@ -70,9 +70,9 @@ constructor( } } - fun checkPromoCodeValidity(courseId: Long, promo: String): Single = + fun checkDeeplinkPromoCodeValidity(courseId: Long, promo: String): Single = coursePaymentsRepository - .checkPromoCodeValidity(courseId, promo) + .checkDeeplinkPromoCodeValidity(courseId, promo) /** * Load course reviews for not enrolled [courses] diff --git a/app/src/main/java/org/stepik/android/domain/course/model/CourseHeaderData.kt b/app/src/main/java/org/stepik/android/domain/course/model/CourseHeaderData.kt index d4e3fa4e41..91a92a6445 100644 --- a/app/src/main/java/org/stepik/android/domain/course/model/CourseHeaderData.kt +++ b/app/src/main/java/org/stepik/android/domain/course/model/CourseHeaderData.kt @@ -4,7 +4,7 @@ import android.os.Parcelable import kotlinx.android.parcel.Parcelize import org.stepik.android.domain.wishlist.model.WishlistEntity import org.stepik.android.domain.course_payments.model.DefaultPromoCode -import org.stepik.android.domain.course_payments.model.PromoCode +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode import org.stepik.android.model.Course @Parcelize @@ -16,7 +16,7 @@ data class CourseHeaderData( val stats: CourseStats, val localSubmissionsCount: Int, - val promoCode: PromoCode, + val deeplinkPromoCode: DeeplinkPromoCode, val defaultPromoCode: DefaultPromoCode, val isWishlistUpdating: Boolean, val wishlistEntity: WishlistEntity diff --git a/app/src/main/java/org/stepik/android/domain/course_info/interactor/CourseInfoInteractor.kt b/app/src/main/java/org/stepik/android/domain/course_info/interactor/CourseInfoInteractor.kt index 73c9d51e00..2645989a92 100644 --- a/app/src/main/java/org/stepik/android/domain/course_info/interactor/CourseInfoInteractor.kt +++ b/app/src/main/java/org/stepik/android/domain/course_info/interactor/CourseInfoInteractor.kt @@ -56,7 +56,7 @@ constructor( instructors = (instructors ?: course.instructors?.map { null })?.takeIf { it.isNotEmpty() }, language = course.language, certificate = course.certificate - ?.takeIf { course.hasCertificate } + ?.takeIf { course.withCertificate } ?.let { CourseInfoData.Certificate( title = it, diff --git a/app/src/main/java/org/stepik/android/domain/course_payments/model/PromoCode.kt b/app/src/main/java/org/stepik/android/domain/course_payments/model/DeeplinkPromoCode.kt similarity index 82% rename from app/src/main/java/org/stepik/android/domain/course_payments/model/PromoCode.kt rename to app/src/main/java/org/stepik/android/domain/course_payments/model/DeeplinkPromoCode.kt index 1162d97b8e..171939a9a3 100644 --- a/app/src/main/java/org/stepik/android/domain/course_payments/model/PromoCode.kt +++ b/app/src/main/java/org/stepik/android/domain/course_payments/model/DeeplinkPromoCode.kt @@ -5,13 +5,13 @@ import com.google.gson.annotations.SerializedName import kotlinx.android.parcel.Parcelize @Parcelize -data class PromoCode( +data class DeeplinkPromoCode( @SerializedName("price") val price: String, @SerializedName("currency_code") val currencyCode: String ) : Parcelable { companion object { - val EMPTY = PromoCode("", "") + val EMPTY = DeeplinkPromoCode("", "") } } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/course_payments/repository/CoursePaymentsRepository.kt b/app/src/main/java/org/stepik/android/domain/course_payments/repository/CoursePaymentsRepository.kt index a9a5b7a458..dc8785e1e6 100644 --- a/app/src/main/java/org/stepik/android/domain/course_payments/repository/CoursePaymentsRepository.kt +++ b/app/src/main/java/org/stepik/android/domain/course_payments/repository/CoursePaymentsRepository.kt @@ -5,7 +5,7 @@ import org.solovyev.android.checkout.Purchase import org.solovyev.android.checkout.Sku import org.stepik.android.domain.base.DataSourceType import org.stepik.android.domain.course_payments.model.CoursePayment -import org.stepik.android.domain.course_payments.model.PromoCode +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode interface CoursePaymentsRepository { fun createCoursePayment(courseId: Long, sku: Sku, purchase: Purchase): Single @@ -17,5 +17,5 @@ interface CoursePaymentsRepository { */ fun getCoursePaymentsByCourseId(courseId: Long, coursePaymentStatus: CoursePayment.Status? = null, sourceType: DataSourceType = DataSourceType.CACHE): Single> - fun checkPromoCodeValidity(courseId: Long, name: String): Single + fun checkDeeplinkPromoCodeValidity(courseId: Long, name: String): Single } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/course_payments/CoursePaymentsRemoteDataSourceImpl.kt b/app/src/main/java/org/stepik/android/remote/course_payments/CoursePaymentsRemoteDataSourceImpl.kt index b3b37e3418..8a989004b1 100644 --- a/app/src/main/java/org/stepik/android/remote/course_payments/CoursePaymentsRemoteDataSourceImpl.kt +++ b/app/src/main/java/org/stepik/android/remote/course_payments/CoursePaymentsRemoteDataSourceImpl.kt @@ -5,7 +5,7 @@ import org.solovyev.android.checkout.Purchase import org.solovyev.android.checkout.Sku import org.stepik.android.data.course_payments.source.CoursePaymentsRemoteDataSource import org.stepik.android.domain.course_payments.model.CoursePayment -import org.stepik.android.domain.course_payments.model.PromoCode +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode import org.stepik.android.remote.course_payments.model.CoursePaymentRequest import org.stepik.android.remote.course_payments.model.CoursePaymentsResponse import org.stepik.android.remote.course_payments.model.PromoCodeRequest @@ -47,9 +47,9 @@ constructor( } } - override fun checkPromoCodeValidity(courseId: Long, name: String): Single = + override fun checkDeeplinkPromoCodeValidity(courseId: Long, name: String): Single = coursePaymentService - .checkPromoCodeValidity(PromoCodeRequest( + .checkDeeplinkPromoCodeValidity(PromoCodeRequest( course = courseId, name = name )) diff --git a/app/src/main/java/org/stepik/android/remote/course_payments/service/CoursePaymentService.kt b/app/src/main/java/org/stepik/android/remote/course_payments/service/CoursePaymentService.kt index c7dcbfe88d..75bea40656 100644 --- a/app/src/main/java/org/stepik/android/remote/course_payments/service/CoursePaymentService.kt +++ b/app/src/main/java/org/stepik/android/remote/course_payments/service/CoursePaymentService.kt @@ -1,7 +1,7 @@ package org.stepik.android.remote.course_payments.service import io.reactivex.Single -import org.stepik.android.domain.course_payments.model.PromoCode +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode import org.stepik.android.remote.course_payments.model.CoursePaymentRequest import org.stepik.android.remote.course_payments.model.CoursePaymentsResponse import org.stepik.android.remote.course_payments.model.PromoCodeRequest @@ -22,7 +22,7 @@ interface CoursePaymentService { ): Single @POST("api/promo-codes/check") - fun checkPromoCodeValidity( + fun checkDeeplinkPromoCodeValidity( @Body promoCodeRequest: PromoCodeRequest - ): Single + ): Single } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course/mapper/DisplayPriceMapper.kt b/app/src/main/java/org/stepik/android/view/course/mapper/DisplayPriceMapper.kt index 9de7381ed5..98006fc5e2 100644 --- a/app/src/main/java/org/stepik/android/view/course/mapper/DisplayPriceMapper.kt +++ b/app/src/main/java/org/stepik/android/view/course/mapper/DisplayPriceMapper.kt @@ -1,6 +1,10 @@ package org.stepik.android.view.course.mapper import android.content.Context +import android.text.SpannedString +import androidx.core.text.buildSpannedString +import androidx.core.text.scale +import androidx.core.text.strikeThrough import org.stepic.droid.R import javax.inject.Inject @@ -22,4 +26,18 @@ constructor( else -> "$price $currencyCode" } + + fun mapToDiscountedDisplayPriceSpannedString(originalDisplayPrice: String, currencyCode: String, promoPrice: String): SpannedString { + val promoDisplayPrice = mapToDisplayPrice(currencyCode, promoPrice) + return buildSpannedString { + append(context.getString(R.string.course_payments_purchase_in_web_with_price_promo)) + append(promoDisplayPrice) + append(" ") + scale(0.9f) { + strikeThrough { + append(originalDisplayPrice) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course/model/CoursePromoCodeInfo.kt b/app/src/main/java/org/stepik/android/view/course/model/CoursePromoCodeInfo.kt new file mode 100644 index 0000000000..fd1a5ef1d7 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/course/model/CoursePromoCodeInfo.kt @@ -0,0 +1,7 @@ +package org.stepik.android.view.course.model + +data class CoursePromoCodeInfo( + val currencyCode: String, + val price: String, + val hasPromo: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course/resolver/CoursePromoCodeResolver.kt b/app/src/main/java/org/stepik/android/view/course/resolver/CoursePromoCodeResolver.kt new file mode 100644 index 0000000000..32372d1f67 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/course/resolver/CoursePromoCodeResolver.kt @@ -0,0 +1,25 @@ +package org.stepik.android.view.course.resolver + +import org.stepic.droid.util.DateTimeHelper +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode +import org.stepik.android.domain.course_payments.model.DefaultPromoCode +import org.stepik.android.model.Course +import org.stepik.android.view.course.model.CoursePromoCodeInfo +import javax.inject.Inject + +class CoursePromoCodeResolver +@Inject +constructor() { + fun resolvePromoCodeInfo(deeplinkPromoCode: DeeplinkPromoCode, defaultPromoCode: DefaultPromoCode, course: Course): CoursePromoCodeInfo = + when { + deeplinkPromoCode != DeeplinkPromoCode.EMPTY -> + CoursePromoCodeInfo(deeplinkPromoCode.currencyCode, deeplinkPromoCode.price, true) + + defaultPromoCode != DefaultPromoCode.EMPTY && + (defaultPromoCode.defaultPromoCodeExpireDate == null || defaultPromoCode.defaultPromoCodeExpireDate.time > DateTimeHelper.nowUtc()) && course.currencyCode != null -> + CoursePromoCodeInfo(course.currencyCode!!, defaultPromoCode.defaultPromoCodePrice, true) + + else -> + CoursePromoCodeInfo("", "", false) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course/ui/delegates/CourseHeaderDelegate.kt b/app/src/main/java/org/stepik/android/view/course/ui/delegates/CourseHeaderDelegate.kt index db102de141..d2aef97a60 100644 --- a/app/src/main/java/org/stepik/android/view/course/ui/delegates/CourseHeaderDelegate.kt +++ b/app/src/main/java/org/stepik/android/view/course/ui/delegates/CourseHeaderDelegate.kt @@ -2,7 +2,6 @@ package org.stepik.android.view.course.ui.delegates import android.app.Activity import android.text.SpannableString -import android.text.SpannedString import android.text.style.ForegroundColorSpan import android.view.Menu import android.view.MenuItem @@ -10,7 +9,6 @@ import android.view.View import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.text.buildSpannedString -import androidx.core.text.scale import androidx.core.text.strikeThrough import androidx.core.view.ViewCompat import androidx.core.view.isVisible @@ -39,14 +37,13 @@ import org.stepik.android.domain.course.analytic.batch.BuyCoursePressedAnalyticB import org.stepik.android.domain.course.model.CourseHeaderData import org.stepik.android.domain.course.model.EnrollmentState import org.stepik.android.domain.course_continue.analytic.CourseContinuePressedEvent -import org.stepik.android.domain.course_payments.model.DefaultPromoCode -import org.stepik.android.domain.course_payments.model.PromoCode import org.stepik.android.presentation.course.CoursePresenter import org.stepik.android.presentation.course_continue.model.CourseContinueInteractionSource import org.stepik.android.presentation.user_courses.model.UserCourseAction import org.stepik.android.presentation.wishlist.model.WishlistAction import org.stepik.android.view.base.ui.extension.ColorExtensions import org.stepik.android.view.course.mapper.DisplayPriceMapper +import org.stepik.android.view.course.resolver.CoursePromoCodeResolver import org.stepik.android.view.ui.delegate.ViewStateDelegate import ru.nobird.android.core.model.safeCast import ru.nobird.android.view.base.ui.extension.getAllQueryParameters @@ -61,6 +58,7 @@ constructor( @Assisted private val coursePresenter: CoursePresenter, private val discountButtonAppearanceSplitTest: DiscountButtonAppearanceSplitTest, private val displayPriceMapper: DisplayPriceMapper, + private val coursePromoCodeResolver: CoursePromoCodeResolver, @Assisted private val courseViewSource: CourseViewSource, @Assisted private val isAuthorized: Boolean, @Assisted private val mustShowCourseBenefits: Boolean, @@ -206,25 +204,18 @@ constructor( courseStatsDelegate.setStats(courseHeaderData.stats) } - val (currencyCode, promoPrice, hasPromo) = when { - courseHeaderData.promoCode != PromoCode.EMPTY -> - Triple(courseHeaderData.promoCode.currencyCode, courseHeaderData.promoCode.price, true) - - courseHeaderData.defaultPromoCode != DefaultPromoCode.EMPTY && - (courseHeaderData.defaultPromoCode.defaultPromoCodeExpireDate == null || courseHeaderData.defaultPromoCode.defaultPromoCodeExpireDate.time > DateTimeHelper.nowUtc()) && - courseHeaderData.course.currencyCode != null -> - Triple(courseHeaderData.course.currencyCode!!, courseHeaderData.defaultPromoCode.defaultPromoCodePrice, true) - - else -> - Triple("", "", false) - } + val (currencyCode, promoPrice, hasPromo) = coursePromoCodeResolver.resolvePromoCodeInfo( + courseHeaderData.deeplinkPromoCode, + courseHeaderData.defaultPromoCode, + courseHeaderData.course + ) val courseDisplayPrice = courseHeaderData.course.displayPrice courseBuyInWebAction.text = if (courseDisplayPrice != null) { if (hasPromo) { - getPurchaseButtonText(courseDisplayPrice, currencyCode, promoPrice) + displayPriceMapper.mapToDiscountedDisplayPriceSpannedString(courseDisplayPrice, currencyCode, promoPrice) } else { getString(R.string.course_payments_purchase_in_web_with_price, courseDisplayPrice) } @@ -293,20 +284,6 @@ constructor( shareCourseMenuItem?.isVisible = true } - private fun getPurchaseButtonText(originalDisplayPrice: String, currencyCode: String, promoPrice: String): SpannedString { - val promoDisplayPrice = displayPriceMapper.mapToDisplayPrice(currencyCode, promoPrice) - return buildSpannedString { - append(courseActivity.getString(R.string.course_payments_purchase_in_web_with_price_promo)) - append(promoDisplayPrice) - append(" ") - scale(0.9f) { - strikeThrough { - append(originalDisplayPrice) - } - } - } - } - fun showCourseShareTooltip() { val menuItemView = courseActivity .courseToolbar diff --git a/app/src/main/java/org/stepik/android/view/course_complete/ui/dialog/CourseCompleteBottomSheetDialogFragment.kt b/app/src/main/java/org/stepik/android/view/course_complete/ui/dialog/CourseCompleteBottomSheetDialogFragment.kt index 11100eea69..c9f213d398 100644 --- a/app/src/main/java/org/stepik/android/view/course_complete/ui/dialog/CourseCompleteBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/stepik/android/view/course_complete/ui/dialog/CourseCompleteBottomSheetDialogFragment.kt @@ -157,7 +157,7 @@ class CourseCompleteBottomSheetDialogFragment : BottomSheetDialogFragment(), val progress = score * 100 / cost return when { - progress < 20f && !courseCompleteInfo.course.hasCertificate -> { + progress < 20f && !courseCompleteInfo.course.withCertificate -> { setupCertificateNotIssued( courseCompleteInfo = courseCompleteInfo, headerImage = R.drawable.ic_tak_demo_lesson, @@ -167,7 +167,7 @@ class CourseCompleteBottomSheetDialogFragment : BottomSheetDialogFragment(), secondaryActionStringRes = R.string.course_complete_action_back_to_assignments ) } - progress < 20f && courseCompleteInfo.course.hasCertificate -> { + progress < 20f && courseCompleteInfo.course.withCertificate -> { val courseScore = score.toLong() when { (courseCompleteInfo.course.certificateRegularThreshold != 0L && courseScore < courseCompleteInfo.course.certificateRegularThreshold) || @@ -213,7 +213,7 @@ class CourseCompleteBottomSheetDialogFragment : BottomSheetDialogFragment(), CourseCompleteDialogViewInfo.EMPTY } } - progress >= 20f && progress < 80f && !courseCompleteInfo.course.hasCertificate -> { + progress >= 20f && progress < 80f && !courseCompleteInfo.course.withCertificate -> { setupCertificateNotIssued( courseCompleteInfo = courseCompleteInfo, headerImage = R.drawable.ic_tak_neutral, @@ -223,7 +223,7 @@ class CourseCompleteBottomSheetDialogFragment : BottomSheetDialogFragment(), secondaryActionStringRes = R.string.course_complete_action_back_to_assignments ) } - progress >= 20f && progress < 80f && courseCompleteInfo.course.hasCertificate -> { + progress >= 20f && progress < 80f && courseCompleteInfo.course.withCertificate -> { val courseScore = score.toLong() when { (courseCompleteInfo.course.certificateRegularThreshold != 0L && courseScore < courseCompleteInfo.course.certificateRegularThreshold) || @@ -269,7 +269,7 @@ class CourseCompleteBottomSheetDialogFragment : BottomSheetDialogFragment(), CourseCompleteDialogViewInfo.EMPTY } } - progress >= 80f && !courseCompleteInfo.course.hasCertificate -> { + progress >= 80f && !courseCompleteInfo.course.withCertificate -> { val (primaryAction, secondaryAction) = if (courseCompleteInfo.hasReview) { -1 to R.string.course_complete_action_find_new_course } else { @@ -286,7 +286,7 @@ class CourseCompleteBottomSheetDialogFragment : BottomSheetDialogFragment(), ) } - progress >= 80f && courseCompleteInfo.course.hasCertificate -> { + progress >= 80f && courseCompleteInfo.course.withCertificate -> { val courseScore = score.toLong() when { (courseCompleteInfo.course.certificateRegularThreshold != 0L && courseScore < courseCompleteInfo.course.certificateRegularThreshold) || diff --git a/app/src/main/java/org/stepik/android/view/course_list/ui/delegate/CoursePropertiesDelegate.kt b/app/src/main/java/org/stepik/android/view/course_list/ui/delegate/CoursePropertiesDelegate.kt index f98d763390..a7538ac933 100644 --- a/app/src/main/java/org/stepik/android/view/course_list/ui/delegate/CoursePropertiesDelegate.kt +++ b/app/src/main/java/org/stepik/android/view/course_list/ui/delegate/CoursePropertiesDelegate.kt @@ -99,7 +99,7 @@ class CoursePropertiesDelegate( private fun setCertificate(course: Course) { val isEnrolled = course.enrollment > 0L - val needShow = course.hasCertificate && !isEnrolled + val needShow = course.withCertificate && !isEnrolled courseCertificateImage.isVisible = needShow courseCertificateText.isVisible = needShow } diff --git a/app/src/main/java/org/stepik/android/view/download/ui/activity/DownloadActivity.kt b/app/src/main/java/org/stepik/android/view/download/ui/activity/DownloadActivity.kt index f48f009ba9..ec61379d56 100644 --- a/app/src/main/java/org/stepik/android/view/download/ui/activity/DownloadActivity.kt +++ b/app/src/main/java/org/stepik/android/view/download/ui/activity/DownloadActivity.kt @@ -86,9 +86,9 @@ class DownloadActivity : FragmentActivityBase(), DownloadView, RemoveCachedConte downloadPresenter.fetchStorage() downloadPresenter.fetchDownloadedCourses() - TextViewCompat.setCompoundDrawableTintList(downloadsOtherApps, ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_on_surface_alpha_12))) + TextViewCompat.setCompoundDrawableTintList(downloadsOtherApps, ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_overlay_yellow))) TextViewCompat.setCompoundDrawableTintList(downloadsStepik, ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_overlay_green))) - TextViewCompat.setCompoundDrawableTintList(downloadsFree, ColorStateList.valueOf(ContextCompat.getColor(this, R.color.grey04))) + TextViewCompat.setCompoundDrawableTintList(downloadsFree, ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_elevation_overlay_2dp))) } private fun injectComponent() { diff --git a/app/src/main/java/org/stepik/android/view/lesson/ui/dialog/LessonDemoCompleteBottomSheetDialogFragment.kt b/app/src/main/java/org/stepik/android/view/lesson/ui/dialog/LessonDemoCompleteBottomSheetDialogFragment.kt index 98be19951d..b8795920ef 100644 --- a/app/src/main/java/org/stepik/android/view/lesson/ui/dialog/LessonDemoCompleteBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/stepik/android/view/lesson/ui/dialog/LessonDemoCompleteBottomSheetDialogFragment.kt @@ -13,7 +13,11 @@ import org.stepic.droid.R import org.stepic.droid.base.App import org.stepic.droid.core.ScreenManager import org.stepik.android.domain.course.analytic.CourseViewSource +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode +import org.stepik.android.domain.course_payments.model.DefaultPromoCode import org.stepik.android.model.Course +import org.stepik.android.view.course.mapper.DisplayPriceMapper +import org.stepik.android.view.course.resolver.CoursePromoCodeResolver import org.stepik.android.view.course.routing.CourseScreenTab import ru.nobird.android.view.base.ui.extension.argument import javax.inject.Inject @@ -22,8 +26,6 @@ class LessonDemoCompleteBottomSheetDialogFragment : BottomSheetDialogFragment() companion object { const val TAG = "LessonDemoCompleteBottomSheetDialog" - const val ARG_COURSE = "course" - fun newInstance(course: Course): DialogFragment = LessonDemoCompleteBottomSheetDialogFragment().apply { this.course = course @@ -35,6 +37,12 @@ class LessonDemoCompleteBottomSheetDialogFragment : BottomSheetDialogFragment() @Inject lateinit var screenManager: ScreenManager + @Inject + lateinit var displayPriceMapper: DisplayPriceMapper + + @Inject + lateinit var coursePromoCodeResolver: CoursePromoCodeResolver + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) App.component().inject(this) @@ -52,7 +60,30 @@ class LessonDemoCompleteBottomSheetDialogFragment : BottomSheetDialogFragment() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) demoCompleteTitle.text = getString(R.string.demo_complete_title, course.title) - demoCompleteAction.text = getString(R.string.demo_complete_purchase_action, course.displayPrice) + + val courseDisplayPrice = course.displayPrice + val (currencyCode, promoPrice, hasPromo) = coursePromoCodeResolver.resolvePromoCodeInfo( + DeeplinkPromoCode.EMPTY, // TODO Deeplink promo code will be passed as a parameter to newInstance + DefaultPromoCode( + course.defaultPromoCodeName ?: "", + course.defaultPromoCodePrice ?: "", + course.defaultPromoCodeDiscount ?: "", + course.defaultPromoCodeExpireDate + ), + course + ) + + demoCompleteAction.text = + if (courseDisplayPrice != null) { + if (hasPromo) { + displayPriceMapper.mapToDiscountedDisplayPriceSpannedString(courseDisplayPrice, currencyCode, promoPrice) + } else { + getString(R.string.course_payments_purchase_in_web_with_price, courseDisplayPrice) + } + } else { + getString(R.string.course_payments_purchase_in_web) + } + demoCompleteAction.setOnClickListener { screenManager.showCourseFromNavigationDialog(requireContext(), course.id, CourseViewSource.LessonDemoDialog, CourseScreenTab.INFO, true) } diff --git a/app/src/main/java/org/stepik/android/view/user_reviews/ui/fragment/UserReviewsFragment.kt b/app/src/main/java/org/stepik/android/view/user_reviews/ui/fragment/UserReviewsFragment.kt index 01d9089d37..d710708850 100644 --- a/app/src/main/java/org/stepik/android/view/user_reviews/ui/fragment/UserReviewsFragment.kt +++ b/app/src/main/java/org/stepik/android/view/user_reviews/ui/fragment/UserReviewsFragment.kt @@ -86,7 +86,7 @@ class UserReviewsFragment : Fragment(R.layout.fragment_user_reviews), ReduxView< screenManager.showCourseDescription(requireContext(), course, CourseViewSource.UserReviews) }, onEditReviewClicked = { courseReview, course -> - analytic.report(EditCourseReviewPressedAnalyticEvent(course.id, course.title.toString(), CourseReviewViewSource.COURSE_REVIEWS_SOURCE)) + analytic.report(EditCourseReviewPressedAnalyticEvent(course.id, course.title.toString(), CourseReviewViewSource.USER_REVIEWS_SOURCE)) showCourseReviewEditDialog(courseReview.course, courseReview, -1f) }, onRemoveReviewClicked = { courseReview -> userReviewsViewModel.onNewMessage(UserReviewsFeature.Message.DeletedReviewUserReviews(courseReview)) } diff --git a/app/src/main/res/drawable/downloads_progress.xml b/app/src/main/res/drawable/downloads_progress.xml index a45f3697d3..5016bc3872 100644 --- a/app/src/main/res/drawable/downloads_progress.xml +++ b/app/src/main/res/drawable/downloads_progress.xml @@ -25,8 +25,8 @@ diff --git a/app/src/sharedTest/java/org/stepik/android/migration_wrapper/MigrationWrappers.kt b/app/src/sharedTest/java/org/stepik/android/migration_wrapper/MigrationWrappers.kt index 87b28d7650..afa502c9b3 100644 --- a/app/src/sharedTest/java/org/stepik/android/migration_wrapper/MigrationWrappers.kt +++ b/app/src/sharedTest/java/org/stepik/android/migration_wrapper/MigrationWrappers.kt @@ -19,6 +19,8 @@ object MigrationWrappers { object : MigrationWrapper(MigrationFrom66To67) {}, object : MigrationWrapper(MigrationFrom67To68) {}, MigrationWrapperFrom68To69(MigrationFrom68To69), - object : MigrationWrapper(MigrationFrom69To70) {} + object : MigrationWrapper(MigrationFrom69To70) {}, + // TODO Multiple tests on a single table fail, must research + object : MigrationWrapper(MigrationFrom70To71) {} ) } \ No newline at end of file diff --git a/app/src/stageDebuggable/assets/configs/config_dev.json b/app/src/stageDebuggable/assets/configs/config_dev.json new file mode 100644 index 0000000000..76696ba174 Binary files /dev/null and b/app/src/stageDebuggable/assets/configs/config_dev.json differ diff --git a/app/src/stageDebuggable/assets/configs/config_production.json b/app/src/stageDebuggable/assets/configs/config_production.json new file mode 100644 index 0000000000..aba426afa9 Binary files /dev/null and b/app/src/stageDebuggable/assets/configs/config_production.json differ diff --git a/app/src/stageDebuggable/assets/configs/config_release.json b/app/src/stageDebuggable/assets/configs/config_release.json new file mode 100644 index 0000000000..3739585412 Binary files /dev/null and b/app/src/stageDebuggable/assets/configs/config_release.json differ diff --git a/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointInfo.kt b/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointInfo.kt new file mode 100644 index 0000000000..e62d5caddb --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointInfo.kt @@ -0,0 +1,16 @@ +package org.stepic.droid.configuration + +import com.google.gson.annotations.SerializedName + +data class EndpointInfo( + @SerializedName("api_host_url") + val apiHostUrl: String, + @SerializedName("oauth_client_id") + val oauthClientId: String, + @SerializedName("oauth_client_secret") + val oauthClientSecret: String, + @SerializedName("oauth_client_id_social") + val oauthClientIdSocial: String, + @SerializedName("oauth_client_secret_social") + val oauthClientSecretSocial: String +) \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointInfoFactory.kt b/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointInfoFactory.kt new file mode 100644 index 0000000000..fcfd42846d --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointInfoFactory.kt @@ -0,0 +1,37 @@ +package org.stepic.droid.configuration + +import android.content.Context +import com.google.gson.Gson +import org.stepic.droid.preferences.SharedPreferenceHelper +import org.stepik.android.domain.debug.model.EndpointConfig +import java.io.InputStreamReader +import java.nio.charset.Charset +import javax.inject.Inject + +class EndpointInfoFactory +@Inject +constructor( + private val context: Context, + private val gson: Gson, + private val sharedPreferenceHelper: SharedPreferenceHelper +) { + companion object { + private const val ENDPOINT_DEV_CONFIG = "config_dev.json" + private const val ENDPOINT_PRODUCTION_CONFIG = "config_production.json" + private const val ENDPOINT_RELEASE_CONFIG = "config_release.json" + } + fun createEndpointInfo(): EndpointInfo { + val fileName = + when (EndpointConfig.values()[sharedPreferenceHelper.endpointConfig]) { + EndpointConfig.DEV -> + ENDPOINT_DEV_CONFIG + EndpointConfig.PRODUCTION -> + ENDPOINT_PRODUCTION_CONFIG + EndpointConfig.RELEASE -> + ENDPOINT_RELEASE_CONFIG + } + return context.assets.open("configs/${fileName}").use { + gson.fromJson(InputStreamReader(it, Charset.defaultCharset()), EndpointInfo::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointResolverImpl.kt b/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointResolverImpl.kt index 6daef3f749..4096db28ea 100644 --- a/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointResolverImpl.kt +++ b/app/src/stageDebuggable/java/org/stepic/droid/configuration/EndpointResolverImpl.kt @@ -6,14 +6,23 @@ import javax.inject.Inject class EndpointResolverImpl @Inject constructor( - private val config: Config + endpointInfoFactory: EndpointInfoFactory ) : EndpointResolver { + + private val endpointInfo: EndpointInfo = endpointInfoFactory.createEndpointInfo() + override fun getOAuthClientId(type: TokenType): String = - config.getOAuthClientId(type) + when (type) { + TokenType.SOCIAL -> endpointInfo.oauthClientIdSocial + TokenType.LOGIN_PASSWORD -> endpointInfo.oauthClientId + } override fun getBaseUrl(): String = - config.baseUrl + endpointInfo.apiHostUrl override fun getOAuthClientSecret(type: TokenType): String = - config.getOAuthClientSecret(type) + when (type) { + TokenType.SOCIAL -> endpointInfo.oauthClientSecretSocial + TokenType.LOGIN_PASSWORD -> endpointInfo.oauthClientSecret + } } \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/domain/debug/interactor/DebugInteractor.kt b/app/src/stageDebuggable/java/org/stepik/android/domain/debug/interactor/DebugInteractor.kt new file mode 100644 index 0000000000..abaa8ef5b5 --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepik/android/domain/debug/interactor/DebugInteractor.kt @@ -0,0 +1,53 @@ +package org.stepik.android.domain.debug.interactor + +import com.facebook.login.LoginManager +import com.google.firebase.iid.FirebaseInstanceId +import com.vk.api.sdk.VK +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.rxkotlin.Singles +import org.stepic.droid.core.StepikLogoutManager +import org.stepic.droid.preferences.SharedPreferenceHelper +import org.stepik.android.domain.debug.model.EndpointConfig +import org.stepik.android.domain.debug.model.DebugSettings +import javax.inject.Inject + +class DebugInteractor +@Inject +constructor( + private val firebaseInstanceId: FirebaseInstanceId, + private val sharedPreferenceHelper: SharedPreferenceHelper, + private val logoutManager: StepikLogoutManager +) { + + fun fetchDebugSettings(): Single = + Singles.zip( + getFirebaseToken(), + getEndpointConfig() + ) { fcmToken, endpointConfig -> + DebugSettings(fcmToken, endpointConfig, endpointConfigSelection = endpointConfig.ordinal) + } + + fun updateEndpointConfig(endpointConfig: EndpointConfig): Completable = + Completable.fromAction { + sharedPreferenceHelper.putEndpointConfig(endpointConfig.ordinal) + }.andThen( + logoutManager.logoutCompletable { + LoginManager.getInstance().logOut() + VK.logout() + } + ) + + private fun getFirebaseToken(): Single = + Single.create { emitter -> + firebaseInstanceId + .instanceId + .addOnSuccessListener { instanceIdResult -> emitter.onSuccess(instanceIdResult.token) } + .addOnFailureListener(emitter::onError) + } + + private fun getEndpointConfig(): Single = + Single.fromCallable { + EndpointConfig.values()[sharedPreferenceHelper.endpointConfig] + } +} \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/domain/debug/model/DebugSettings.kt b/app/src/stageDebuggable/java/org/stepik/android/domain/debug/model/DebugSettings.kt new file mode 100644 index 0000000000..84e0976eb4 --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepik/android/domain/debug/model/DebugSettings.kt @@ -0,0 +1,7 @@ +package org.stepik.android.domain.debug.model + +data class DebugSettings( + val fcmToken: String, + val currentEndpointConfig: EndpointConfig, + val endpointConfigSelection: Int +) diff --git a/app/src/stageDebuggable/java/org/stepik/android/domain/debug/model/EndpointConfig.kt b/app/src/stageDebuggable/java/org/stepik/android/domain/debug/model/EndpointConfig.kt new file mode 100644 index 0000000000..6f816639c4 --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepik/android/domain/debug/model/EndpointConfig.kt @@ -0,0 +1,7 @@ +package org.stepik.android.domain.debug.model + +enum class EndpointConfig { + DEV, + PRODUCTION, + RELEASE +} \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/DebugFeature.kt b/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/DebugFeature.kt new file mode 100644 index 0000000000..99ca5f7732 --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/DebugFeature.kt @@ -0,0 +1,30 @@ +package org.stepik.android.presentation.debug + +import org.stepik.android.domain.debug.model.EndpointConfig +import org.stepik.android.domain.debug.model.DebugSettings + +interface DebugFeature { + sealed class State { + object Idle : State() + object Loading : State() + object Error : State() + data class Content(val fcmToken: String, val currentEndpointConfig: EndpointConfig, val endpointConfigSelection: Int) : State() + } + + sealed class Message { + data class InitMessage(val forceUpdate: Boolean = false) : Message() + data class FetchDebugSettingsSuccess(val debugSettings: DebugSettings) : Message() + object FetchDebugSettingsFailure : Message() + data class RadioButtonSelectionMessage(val position: Int) : Message() + object ApplySettingsMessage : Message() + object ApplySettingsSuccess : Message() + } + + sealed class Action { + object FetchDebugSettings : Action() + data class UpdateEndpointConfig(val endpointConfig: EndpointConfig) : Action() + sealed class ViewAction : Action() { + object RestartApplication : ViewAction() + } + } +} \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/DebugViewModel.kt b/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/DebugViewModel.kt new file mode 100644 index 0000000000..991e3063bd --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/DebugViewModel.kt @@ -0,0 +1,8 @@ +package org.stepik.android.presentation.debug + +import ru.nobird.android.presentation.redux.container.ReduxViewContainer +import ru.nobird.android.view.redux.viewmodel.ReduxViewModel + +class DebugViewModel( + reduxViewContainer: ReduxViewContainer +) : ReduxViewModel(reduxViewContainer) \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/dispatcher/DebugActionDispatcher.kt b/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/dispatcher/DebugActionDispatcher.kt new file mode 100644 index 0000000000..e289ccda39 --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/dispatcher/DebugActionDispatcher.kt @@ -0,0 +1,48 @@ +package org.stepik.android.presentation.debug.dispatcher + +import io.reactivex.Scheduler +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.subscribeBy +import org.stepic.droid.di.qualifiers.BackgroundScheduler +import org.stepic.droid.di.qualifiers.MainScheduler +import org.stepik.android.domain.debug.interactor.DebugInteractor +import org.stepik.android.presentation.debug.DebugFeature +import ru.nobird.android.domain.rx.emptyOnErrorStub +import ru.nobird.android.presentation.redux.dispatcher.RxActionDispatcher +import javax.inject.Inject + +class DebugActionDispatcher +@Inject +constructor( + private val debugInteractor: DebugInteractor, + @BackgroundScheduler + private val backgroundScheduler: Scheduler, + @MainScheduler + private val mainScheduler: Scheduler +) : RxActionDispatcher() { + override fun handleAction(action: DebugFeature.Action) { + when (action) { + is DebugFeature.Action.FetchDebugSettings -> { + compositeDisposable += debugInteractor + .fetchDebugSettings() + .subscribeOn(backgroundScheduler) + .observeOn(mainScheduler) + .subscribeBy( + onSuccess = { onNewMessage(DebugFeature.Message.FetchDebugSettingsSuccess(it)) }, + onError = { onNewMessage(DebugFeature.Message.FetchDebugSettingsFailure) } + ) + } + + is DebugFeature.Action.UpdateEndpointConfig -> { + compositeDisposable += debugInteractor + .updateEndpointConfig(action.endpointConfig) + .subscribeOn(backgroundScheduler) + .observeOn(mainScheduler) + .subscribeBy( + onComplete = { onNewMessage(DebugFeature.Message.ApplySettingsSuccess) }, + onError = emptyOnErrorStub + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/reducer/DebugReducer.kt b/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/reducer/DebugReducer.kt new file mode 100644 index 0000000000..f71958c71b --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepik/android/presentation/debug/reducer/DebugReducer.kt @@ -0,0 +1,64 @@ +package org.stepik.android.presentation.debug.reducer + +import org.stepik.android.domain.debug.model.EndpointConfig +import org.stepik.android.presentation.debug.DebugFeature.State +import org.stepik.android.presentation.debug.DebugFeature.Message +import org.stepik.android.presentation.debug.DebugFeature.Action +import ru.nobird.android.presentation.redux.reducer.StateReducer +import javax.inject.Inject + +class DebugReducer +@Inject +constructor() : StateReducer { + override fun reduce(state: State, message: Message): Pair> = + when (message) { + is Message.InitMessage -> { + if (state is State.Idle || state is State.Error && message.forceUpdate) { + State.Loading to setOf(Action.FetchDebugSettings) + } else { + null + } + } + + is Message.FetchDebugSettingsSuccess -> { + if (state is State.Loading) { + State.Content(message.debugSettings.fcmToken, message.debugSettings.currentEndpointConfig, message.debugSettings.endpointConfigSelection) to emptySet() + } else { + null + } + } + + is Message.FetchDebugSettingsFailure -> { + if (state is State.Loading) { + State.Error to emptySet() + } else { + null + } + } + + is Message.RadioButtonSelectionMessage -> { + if (state is State.Content) { + state.copy(endpointConfigSelection = message.position) to emptySet() + } else { + null + } + } + + is Message.ApplySettingsMessage -> { + if (state is State.Content) { + val updatedEndpointConfig = EndpointConfig.values()[state.endpointConfigSelection] + state to setOf(Action.UpdateEndpointConfig(updatedEndpointConfig)) + } else { + null + } + } + + is Message.ApplySettingsSuccess -> { + if (state is State.Content) { + state to setOf(Action.ViewAction.RestartApplication) + } else { + null + } + } + } ?: state to emptySet() +} \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/view/debug/ui/fragment/DebugFragment.kt b/app/src/stageDebuggable/java/org/stepik/android/view/debug/ui/fragment/DebugFragment.kt new file mode 100644 index 0000000000..e8e622b3bb --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepik/android/view/debug/ui/fragment/DebugFragment.kt @@ -0,0 +1,118 @@ +package org.stepik.android.view.debug.ui.fragment + +import android.os.Bundle +import android.view.View +import android.widget.RadioButton +import android.widget.Toast +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.view.get +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import org.stepic.droid.R +import org.stepic.droid.base.App +import org.stepic.droid.util.copyTextToClipboard +import org.stepik.android.presentation.debug.DebugFeature +import org.stepik.android.presentation.debug.DebugViewModel +import org.stepik.android.view.ui.delegate.ViewStateDelegate +import ru.nobird.android.presentation.redux.container.ReduxView +import ru.nobird.android.view.redux.ui.extension.reduxViewModel +import javax.inject.Inject +import android.content.Intent +import android.content.Context +import androidx.core.view.isVisible +import by.kirich1409.viewbindingdelegate.viewBinding +import org.stepic.droid.databinding.FragmentDebugBinding + +class DebugFragment : Fragment(R.layout.fragment_debug), ReduxView { + companion object { + fun newInstance(): Fragment = + DebugFragment() + } + + @Inject + internal lateinit var viewModelFactory: ViewModelProvider.Factory + + private val debugViewModel: DebugViewModel by reduxViewModel(this) { viewModelFactory } + + private val viewStateDelegate = ViewStateDelegate() + + private val debugBinding: FragmentDebugBinding by viewBinding(FragmentDebugBinding::bind) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + injectComponent() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + debugBinding.appBarLayoutBinding.viewCenteredToolbarBinding.centeredToolbarTitle.setText(R.string.debug_toolbar_title) + initViewStateDelegate() + debugViewModel.onNewMessage(DebugFeature.Message.InitMessage()) + + debugBinding.debugFcmTokenValue.setOnLongClickListener { + val textToCopy = (it as AppCompatTextView).text.toString() + requireContext().copyTextToClipboard( + textToCopy = textToCopy, + toastMessage = getString(R.string.copied_to_clipboard_toast) + ) + true + } + + debugBinding.debugEndpointRadioGroup.setOnCheckedChangeListener { group, checkedId -> + val checkedRadioButton = group.findViewById(checkedId) + val position = group.indexOfChild(checkedRadioButton) + debugViewModel.onNewMessage(DebugFeature.Message.RadioButtonSelectionMessage(position)) + } + + debugBinding.debugApplySettingsAction.setOnClickListener { + debugViewModel.onNewMessage(DebugFeature.Message.ApplySettingsMessage) + } + + debugBinding.debugLoadingError.tryAgain.setOnClickListener { + debugViewModel.onNewMessage(DebugFeature.Message.InitMessage(forceUpdate = true)) + } + } + + private fun injectComponent() { + App.component() + .debugComponentBuilder() + .build() + .inject(this) + } + + private fun initViewStateDelegate() { + viewStateDelegate.addState() + viewStateDelegate.addState(debugBinding.debugProgressBar.loadProgressbarOnEmptyScreen) + viewStateDelegate.addState(debugBinding.debugLoadingError.errorNoConnection) + viewStateDelegate.addState(debugBinding.debugContent) + } + + override fun onAction(action: DebugFeature.Action.ViewAction) { + if (action is DebugFeature.Action.ViewAction.RestartApplication) { + Toast.makeText(requireContext(), R.string.debug_restarting_message, Toast.LENGTH_SHORT).show() + view?.postDelayed({ triggerApplicationRestart(requireContext()) }, 1500) + } + } + + override fun render(state: DebugFeature.State) { + viewStateDelegate.switchState(state) + if (state is DebugFeature.State.Content) { + debugBinding.debugFcmTokenValue.text = state.fcmToken + setRadioButtonSelection(state.endpointConfigSelection) + debugBinding.debugApplySettingsAction.isVisible = state.currentEndpointConfig.ordinal != state.endpointConfigSelection + } + } + + private fun triggerApplicationRestart(context: Context) { + val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) + val componentName = intent?.component + val mainIntent = Intent.makeRestartActivityTask(componentName) + context.startActivity(mainIntent) + Runtime.getRuntime().exit(0) + } + + private fun setRadioButtonSelection(itemPosition: Int) { + val targetRadioButton = debugBinding.debugEndpointRadioGroup[itemPosition] as RadioButton + targetRadioButton.isChecked = true + } +} \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/view/debug/ui/fragment/DebugMenu.kt b/app/src/stageDebuggable/java/org/stepik/android/view/debug/ui/fragment/DebugMenu.kt index 531c3050e2..860d3d8407 100644 --- a/app/src/stageDebuggable/java/org/stepik/android/view/debug/ui/fragment/DebugMenu.kt +++ b/app/src/stageDebuggable/java/org/stepik/android/view/debug/ui/fragment/DebugMenu.kt @@ -5,6 +5,6 @@ import androidx.fragment.app.Fragment interface DebugMenu { companion object { const val TAG = "DebugFragment" - fun newInstance(): Fragment = Fragment() + fun newInstance(): Fragment = DebugFragment.newInstance() } } \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/view/injection/debug/DebugComponent.kt b/app/src/stageDebuggable/java/org/stepik/android/view/injection/debug/DebugComponent.kt index 9f5c09d58e..5f3be89477 100644 --- a/app/src/stageDebuggable/java/org/stepik/android/view/injection/debug/DebugComponent.kt +++ b/app/src/stageDebuggable/java/org/stepik/android/view/injection/debug/DebugComponent.kt @@ -1,11 +1,16 @@ package org.stepik.android.view.injection.debug import dagger.Subcomponent +import org.stepik.android.view.debug.ui.fragment.DebugFragment -@Subcomponent +@Subcomponent(modules = [ + DebugPresentationModule::class +]) interface DebugComponent { @Subcomponent.Builder interface Builder { fun build(): DebugComponent } + + fun inject(debugFragment: DebugFragment) } \ No newline at end of file diff --git a/app/src/stageDebuggable/java/org/stepik/android/view/injection/debug/DebugPresentationModule.kt b/app/src/stageDebuggable/java/org/stepik/android/view/injection/debug/DebugPresentationModule.kt new file mode 100644 index 0000000000..185b48b20a --- /dev/null +++ b/app/src/stageDebuggable/java/org/stepik/android/view/injection/debug/DebugPresentationModule.kt @@ -0,0 +1,33 @@ +package org.stepik.android.view.injection.debug + +import androidx.lifecycle.ViewModel +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import org.stepik.android.presentation.base.injection.ViewModelKey +import org.stepik.android.presentation.debug.DebugFeature +import org.stepik.android.presentation.debug.DebugViewModel +import org.stepik.android.presentation.debug.dispatcher.DebugActionDispatcher +import org.stepik.android.presentation.debug.reducer.DebugReducer +import ru.nobird.android.presentation.redux.container.wrapWithViewContainer +import ru.nobird.android.presentation.redux.dispatcher.wrapWithActionDispatcher +import ru.nobird.android.presentation.redux.feature.ReduxFeature + +@Module +object DebugPresentationModule { + /** + * Presentation + */ + @Provides + @IntoMap + @ViewModelKey(DebugViewModel::class) + internal fun provideDebugPresenter( + debugReducer: DebugReducer, + debugActionDispatcher: DebugActionDispatcher + ): ViewModel = + DebugViewModel( + ReduxFeature(DebugFeature.State.Idle, debugReducer) + .wrapWithActionDispatcher(debugActionDispatcher) + .wrapWithViewContainer() + ) +} \ No newline at end of file diff --git a/app/src/stageDebuggable/res/layout/fragment_debug.xml b/app/src/stageDebuggable/res/layout/fragment_debug.xml new file mode 100644 index 0000000000..2410acbf3c --- /dev/null +++ b/app/src/stageDebuggable/res/layout/fragment_debug.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/org/stepik/android/domain/course/model/CourseHeaderDataTest.kt b/app/src/test/java/org/stepik/android/domain/course/model/CourseHeaderDataTest.kt index 027a061ba4..96370411d8 100644 --- a/app/src/test/java/org/stepik/android/domain/course/model/CourseHeaderDataTest.kt +++ b/app/src/test/java/org/stepik/android/domain/course/model/CourseHeaderDataTest.kt @@ -5,7 +5,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.stepic.droid.testUtils.assertThatObjectParcelable import org.stepik.android.domain.course_payments.model.DefaultPromoCode -import org.stepik.android.domain.course_payments.model.PromoCode +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode import org.stepik.android.domain.wishlist.model.WishlistEntity import org.stepik.android.model.Course import org.stepik.android.model.Progress @@ -37,7 +37,7 @@ class CourseHeaderDataTest { isWishlisted = false ), localSubmissionsCount = 5, - promoCode = PromoCode("200", "RUB"), + deeplinkPromoCode = DeeplinkPromoCode("200", "RUB"), defaultPromoCode = DefaultPromoCode.EMPTY, isWishlistUpdating = false, wishlistEntity = WishlistEntity(-1, emptyList()) diff --git a/dependencies.gradle b/dependencies.gradle index 7add165d95..2f32e1d5d5 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ ext.versions = [ - code : 2221, - name : '1.189', + code : 2222, + name : '1.190', minSdk : 16, targetSdk : 30, diff --git a/model/src/main/java/org/stepik/android/model/Course.kt b/model/src/main/java/org/stepik/android/model/Course.kt index ddedaa2282..515092ffce 100644 --- a/model/src/main/java/org/stepik/android/model/Course.kt +++ b/model/src/main/java/org/stepik/android/model/Course.kt @@ -86,6 +86,8 @@ data class Course( val isCertificateAutoIssued: Boolean = false, @SerializedName("is_certificate_issued") val isCertificateIssued: Boolean = false, + @SerializedName("with_certificate") + val withCertificate: Boolean = false, @SerializedName("last_deadline") val lastDeadline: String? = null, @@ -142,14 +144,6 @@ data class Course( @SerializedName("default_promo_code_expire_date") val defaultPromoCodeExpireDate: Date? = null ) : Progressable, Parcelable, Identifiable { - - val hasCertificate: Boolean - get() = certificate?.let { - val hasText = it.isNotEmpty() - val anyCertificateThreshold = certificateRegularThreshold > 0 || certificateDistinctionThreshold > 0 - anyCertificateThreshold && (hasText || isCertificateAutoIssued || isCertificateIssued) - } ?: false - companion object { const val SCHEDULE_TYPE_ENDED = "ended" const val SCHEDULE_TYPE_UPCOMMING = "upcoming"