diff --git a/app/src/main/java/org/stepic/droid/analytic/Analytic.java b/app/src/main/java/org/stepic/droid/analytic/Analytic.java index 828159af8e..55ac7f60ac 100644 --- a/app/src/main/java/org/stepic/droid/analytic/Analytic.java +++ b/app/src/main/java/org/stepic/droid/analytic/Analytic.java @@ -5,6 +5,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.stepik.android.domain.base.analytic.AnalyticEvent; +import org.stepik.android.domain.base.analytic.UserProperty; import java.util.Map; @@ -361,10 +362,13 @@ interface Traces { void setStreaksNotificationsEnabled(boolean isEnabled); void setTeachingCoursesCount(int coursesCount); void setGoogleServicesAvailable(boolean isAvailable); + + void reportAmplitudeEvent(@NotNull String eventName, @Nullable Map params); void reportAmplitudeEvent(@NotNull String eventName); void report(@NotNull AnalyticEvent analyticEvent); + void reportUserProperty(@NotNull UserProperty userProperty); void setUserProperty(@NotNull String name, @NotNull String value); diff --git a/app/src/main/java/org/stepic/droid/analytic/AnalyticImpl.kt b/app/src/main/java/org/stepic/droid/analytic/AnalyticImpl.kt index cd7720c412..d3b06933e5 100644 --- a/app/src/main/java/org/stepic/droid/analytic/AnalyticImpl.kt +++ b/app/src/main/java/org/stepic/droid/analytic/AnalyticImpl.kt @@ -18,10 +18,13 @@ import com.yandex.metrica.profile.UserProfile import org.json.JSONObject import org.stepic.droid.base.App import org.stepic.droid.configuration.Config +import org.stepic.droid.configuration.RemoteConfig import org.stepic.droid.di.AppSingleton import org.stepic.droid.util.isARSupported import org.stepik.android.domain.base.analytic.AnalyticEvent import org.stepik.android.domain.base.analytic.AnalyticSource +import org.stepik.android.domain.base.analytic.UserProperty +import org.stepik.android.domain.base.analytic.UserPropertySource import ru.nobird.android.view.base.ui.extension.isNightModeEnabled import java.util.HashMap import java.util.Locale @@ -36,6 +39,8 @@ constructor( private val stepikAnalytic: StepikAnalytic ) : Analytic { private companion object { + private const val FIREBASE_USER_PROPERTY_NAME_LIMIT = 24 + private const val FIREBASE_USER_PROPERTY_VALUE_LIMIT = 36 private const val FIREBASE_LENGTH_LIMIT = 40 inline fun updateYandexUserProfile(mutation: UserProfile.Builder.() -> Unit) { @@ -69,9 +74,9 @@ constructor( apply(Attribute.customBoolean(AmplitudeAnalytic.Properties.IS_AR_SUPPORTED).withValue(context.isARSupported())) } - firebaseAnalytics.setUserProperty(AmplitudeAnalytic.Properties.PUSH_PERMISSION, if (isNotificationsEnabled) "granted" else "not_granted") - firebaseAnalytics.setUserProperty(AmplitudeAnalytic.Properties.IS_NIGHT_MODE_ENABLED, context.isNightModeEnabled().toString()) - firebaseAnalytics.setUserProperty(AmplitudeAnalytic.Properties.IS_AR_SUPPORTED, context.isARSupported().toString()) + setFirebaseUserProperty(AmplitudeAnalytic.Properties.PUSH_PERMISSION, if (isNotificationsEnabled) "granted" else "not_granted") + setFirebaseUserProperty(AmplitudeAnalytic.Properties.IS_NIGHT_MODE_ENABLED, context.isNightModeEnabled().toString()) + setFirebaseUserProperty(AmplitudeAnalytic.Properties.IS_AR_SUPPORTED, context.isARSupported().toString()) } // Amplitude properties @@ -89,38 +94,38 @@ constructor( override fun setCoursesCount(coursesCount: Int) { amplitude.identify(Identify().set(AmplitudeAnalytic.Properties.COURSES_COUNT, coursesCount)) updateYandexUserProfile { apply(Attribute.customNumber(AmplitudeAnalytic.Properties.COURSES_COUNT).withValue(coursesCount.toDouble())) } - firebaseAnalytics.setUserProperty(AmplitudeAnalytic.Properties.COURSES_COUNT, coursesCount.toString()) + setFirebaseUserProperty(AmplitudeAnalytic.Properties.COURSES_COUNT, coursesCount.toString()) } override fun setSubmissionsCount(submissionsCount: Long, delta: Long) { amplitude.identify(Identify().set(AmplitudeAnalytic.Properties.SUBMISSIONS_COUNT, submissionsCount + delta)) updateYandexUserProfile { apply(Attribute.customCounter(AmplitudeAnalytic.Properties.SUBMISSIONS_COUNT).withDelta(delta.toDouble())) } - firebaseAnalytics.setUserProperty(AmplitudeAnalytic.Properties.SUBMISSIONS_COUNT, (submissionsCount + delta).toString()) + setFirebaseUserProperty(AmplitudeAnalytic.Properties.SUBMISSIONS_COUNT, (submissionsCount + delta).toString()) } override fun setScreenOrientation(orientation: Int) { val orientationName = if (orientation == Configuration.ORIENTATION_PORTRAIT) "portrait" else "landscape" amplitude.identify(Identify().set(AmplitudeAnalytic.Properties.SCREEN_ORIENTATION, orientationName)) updateYandexUserProfile { apply(Attribute.customString(AmplitudeAnalytic.Properties.SCREEN_ORIENTATION).withValue(orientationName)) } - firebaseAnalytics.setUserProperty(AmplitudeAnalytic.Properties.SCREEN_ORIENTATION, orientationName) + setFirebaseUserProperty(AmplitudeAnalytic.Properties.SCREEN_ORIENTATION, orientationName) } override fun setStreaksNotificationsEnabled(isEnabled: Boolean) { amplitude.identify(Identify().set(AmplitudeAnalytic.Properties.STREAKS_NOTIFICATIONS_ENABLED, if (isEnabled) "enabled" else "disabled")) updateYandexUserProfile { apply(Attribute.customBoolean(AmplitudeAnalytic.Properties.STREAKS_NOTIFICATIONS_ENABLED).withValue(isEnabled)) } - firebaseAnalytics.setUserProperty(AmplitudeAnalytic.Properties.STREAKS_NOTIFICATIONS_ENABLED.substring(0, 24), if (isEnabled) "enabled" else "disabled") + setFirebaseUserProperty(AmplitudeAnalytic.Properties.STREAKS_NOTIFICATIONS_ENABLED, if (isEnabled) "enabled" else "disabled") } override fun setTeachingCoursesCount(coursesCount: Int) { amplitude.identify(Identify().set(AmplitudeAnalytic.Properties.TEACHING_COURSES_COUNT, coursesCount)) updateYandexUserProfile { apply(Attribute.customNumber(AmplitudeAnalytic.Properties.TEACHING_COURSES_COUNT).withValue(coursesCount.toDouble())) } - firebaseAnalytics.setUserProperty(AmplitudeAnalytic.Properties.TEACHING_COURSES_COUNT, coursesCount.toString()) + setFirebaseUserProperty(AmplitudeAnalytic.Properties.TEACHING_COURSES_COUNT, coursesCount.toString()) } override fun setGoogleServicesAvailable(isAvailable: Boolean) { amplitude.identify(Identify().set(AmplitudeAnalytic.Properties.IS_GOOGLE_SERVICES_AVAILABLE, isAvailable.toString())) updateYandexUserProfile { apply(Attribute.customBoolean(AmplitudeAnalytic.Properties.IS_GOOGLE_SERVICES_AVAILABLE).withValue(isAvailable)) } - firebaseAnalytics.setUserProperty(AmplitudeAnalytic.Properties.IS_GOOGLE_SERVICES_AVAILABLE.substring(0, 24), isAvailable.toString()) + setFirebaseUserProperty(AmplitudeAnalytic.Properties.IS_GOOGLE_SERVICES_AVAILABLE, isAvailable.toString()) } override fun report(analyticEvent: AnalyticEvent) { @@ -149,6 +154,44 @@ constructor( } } + override fun reportUserProperty(userProperty: UserProperty) { + if (UserPropertySource.YANDEX in userProperty.sources) { + val userProfileUpdate = + when (val value = userProperty.value) { + is String -> + Attribute.customString(userProperty.name).withValue(value) + is Boolean -> + Attribute.customBoolean(userProperty.name).withValue(value) + is Number -> + Attribute.customNumber(userProperty.name).withValue(value.toDouble()) + else -> + throw IllegalArgumentException("Invalid argument type") + } + updateYandexUserProfile { apply(userProfileUpdate) } + } + + if (UserPropertySource.AMPLITUDE in userProperty.sources) { + val identify = + when (val value = userProperty.value) { + is String -> + Identify().set(userProperty.name, value) + is Boolean -> + Identify().set(userProperty.name, value) + is Long -> + Identify().set(userProperty.name, value) + is Double -> + Identify().set(userProperty.name, value) + else -> + throw IllegalArgumentException("Invalid argument type") + } + amplitude.identify(identify) + } + + if (UserPropertySource.FIREBASE in userProperty.sources) { + setFirebaseUserProperty(userProperty.name, userProperty.value.toString()) + } + } + override fun reportAmplitudeEvent(eventName: String) = reportAmplitudeEvent(eventName, null) override fun reportAmplitudeEvent(eventName: String, params: Map?) { syncAmplitudeProperties() @@ -171,7 +214,7 @@ constructor( amplitude.identify(Identify().set(name, value)) updateYandexUserProfile { apply(Attribute.customString(name).withValue(value)) } firebaseCrashlytics.setCustomKey(name, value) - firebaseAnalytics.setUserProperty(name, value) + setFirebaseUserProperty(name, value) } // End of amplitude properties @@ -226,6 +269,10 @@ constructor( reportEvent(eventName, bundle) } + private fun setFirebaseUserProperty(name: String, value: String) { + firebaseAnalytics.setUserProperty(name.take(FIREBASE_USER_PROPERTY_NAME_LIMIT), value.take(FIREBASE_USER_PROPERTY_VALUE_LIMIT)) + } + private fun castStringToFirebaseEvent(eventName: String): String { val firebaseEventName = eventName .decapitalize(Locale.ENGLISH) diff --git a/app/src/main/java/org/stepic/droid/configuration/RemoteConfig.kt b/app/src/main/java/org/stepic/droid/configuration/RemoteConfig.kt index 480a9a2918..d00243fba5 100644 --- a/app/src/main/java/org/stepic/droid/configuration/RemoteConfig.kt +++ b/app/src/main/java/org/stepic/droid/configuration/RemoteConfig.kt @@ -5,14 +5,12 @@ object RemoteConfig { const val MIN_DELAY_RATE_DIALOG_SEC = "min_delay_rate_dialog_sec" const val SHOW_STREAK_DIALOG_AFTER_LOGIN = "show_streak_dialog_after_login" - const val SHOW_NOTIFICATIONS_BADGES = "show_notifications_badges" const val ADAPTIVE_COURSES = "adaptive_courses_android" const val ADAPTIVE_BACKEND_URL = "adaptive_backend_url" const val IS_LOCAL_SUBMISSIONS_ENABLED = "is_local_submissions_enabled" - const val IS_PEER_REVIEW_ENABLED = "is_peer_review_enabled" - const val IS_DISABLED_STEPS_SUPPORTED = "is_disabled_steps_supported" const val SEARCH_QUERY_PARAMS_ANDROID = "search_query_params_android" const val IS_NEW_HOME_SCREEN_ENABLED = "is_new_home_screen_enabled" const val PERSONALIZED_ONBOARDING_COURSE_LISTS = "personalized_onboarding_course_lists" const val IS_COURSE_REVENUE_AVAILABLE_ANDROID = "is_course_revenue_available_android" + const val PURCHASE_FLOW_ANDROID = "purchase_flow_android" } diff --git a/app/src/main/java/org/stepic/droid/configuration/analytic/AdaptiveBackendUrlUserProperty.kt b/app/src/main/java/org/stepic/droid/configuration/analytic/AdaptiveBackendUrlUserProperty.kt new file mode 100644 index 0000000000..06139dd845 --- /dev/null +++ b/app/src/main/java/org/stepic/droid/configuration/analytic/AdaptiveBackendUrlUserProperty.kt @@ -0,0 +1,12 @@ +package org.stepic.droid.configuration.analytic + +import org.stepic.droid.configuration.RemoteConfig +import org.stepik.android.domain.base.analytic.UserProperty + +class AdaptiveBackendUrlUserProperty(adaptiveBackendUrl: String) : UserProperty { + override val name: String = + RemoteConfig.PREFIX + RemoteConfig.ADAPTIVE_BACKEND_URL + + override val value: String = + adaptiveBackendUrl +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/configuration/analytic/AdaptiveCoursesUserProperty.kt b/app/src/main/java/org/stepic/droid/configuration/analytic/AdaptiveCoursesUserProperty.kt new file mode 100644 index 0000000000..ee873c4ea6 --- /dev/null +++ b/app/src/main/java/org/stepic/droid/configuration/analytic/AdaptiveCoursesUserProperty.kt @@ -0,0 +1,12 @@ +package org.stepic.droid.configuration.analytic + +import org.stepic.droid.configuration.RemoteConfig +import org.stepik.android.domain.base.analytic.UserProperty + +class AdaptiveCoursesUserProperty(adaptiveCourses: String) : UserProperty { + override val name: String = + RemoteConfig.PREFIX + RemoteConfig.ADAPTIVE_COURSES + + override val value: String = + adaptiveCourses +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/configuration/analytic/CourseRevenueAvailableUserProperty.kt b/app/src/main/java/org/stepic/droid/configuration/analytic/CourseRevenueAvailableUserProperty.kt new file mode 100644 index 0000000000..fb54b889e8 --- /dev/null +++ b/app/src/main/java/org/stepic/droid/configuration/analytic/CourseRevenueAvailableUserProperty.kt @@ -0,0 +1,12 @@ +package org.stepic.droid.configuration.analytic + +import org.stepic.droid.configuration.RemoteConfig +import org.stepik.android.domain.base.analytic.UserProperty + +class CourseRevenueAvailableUserProperty(isCourseRevenueAvailable: Boolean) : UserProperty { + override val name: String = + RemoteConfig.PREFIX + RemoteConfig.IS_COURSE_REVENUE_AVAILABLE_ANDROID + + override val value: Boolean = + isCourseRevenueAvailable +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/configuration/analytic/LocalSubmissionsEnabledUserProperty.kt b/app/src/main/java/org/stepic/droid/configuration/analytic/LocalSubmissionsEnabledUserProperty.kt new file mode 100644 index 0000000000..005342a48d --- /dev/null +++ b/app/src/main/java/org/stepic/droid/configuration/analytic/LocalSubmissionsEnabledUserProperty.kt @@ -0,0 +1,12 @@ +package org.stepic.droid.configuration.analytic + +import org.stepic.droid.configuration.RemoteConfig +import org.stepik.android.domain.base.analytic.UserProperty + +class LocalSubmissionsEnabledUserProperty(isLocalSubmissionsEnabled: Boolean) : UserProperty { + override val name: String = + RemoteConfig.PREFIX + RemoteConfig.IS_LOCAL_SUBMISSIONS_ENABLED + + override val value: Boolean = + isLocalSubmissionsEnabled +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/configuration/analytic/MinDelayRateDialogUserProperty.kt b/app/src/main/java/org/stepic/droid/configuration/analytic/MinDelayRateDialogUserProperty.kt new file mode 100644 index 0000000000..ea7c45fbaf --- /dev/null +++ b/app/src/main/java/org/stepic/droid/configuration/analytic/MinDelayRateDialogUserProperty.kt @@ -0,0 +1,12 @@ +package org.stepic.droid.configuration.analytic + +import org.stepic.droid.configuration.RemoteConfig +import org.stepik.android.domain.base.analytic.UserProperty + +class MinDelayRateDialogUserProperty(delay: Long) : UserProperty { + override val name: String = + RemoteConfig.PREFIX + RemoteConfig.MIN_DELAY_RATE_DIALOG_SEC + + override val value: Long = + delay +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/configuration/analytic/NewHomeScreenEnabledUserProperty.kt b/app/src/main/java/org/stepic/droid/configuration/analytic/NewHomeScreenEnabledUserProperty.kt new file mode 100644 index 0000000000..2ac8464313 --- /dev/null +++ b/app/src/main/java/org/stepic/droid/configuration/analytic/NewHomeScreenEnabledUserProperty.kt @@ -0,0 +1,12 @@ +package org.stepic.droid.configuration.analytic + +import org.stepic.droid.configuration.RemoteConfig +import org.stepik.android.domain.base.analytic.UserProperty + +class NewHomeScreenEnabledUserProperty(isNewHomeScreenEnabled: Boolean) : UserProperty { + override val name: String = + RemoteConfig.PREFIX + RemoteConfig.IS_NEW_HOME_SCREEN_ENABLED + + override val value: Boolean = + isNewHomeScreenEnabled +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/configuration/analytic/PersonalizedOnboardingCourseListsUserProperty.kt b/app/src/main/java/org/stepic/droid/configuration/analytic/PersonalizedOnboardingCourseListsUserProperty.kt new file mode 100644 index 0000000000..191e27c420 --- /dev/null +++ b/app/src/main/java/org/stepic/droid/configuration/analytic/PersonalizedOnboardingCourseListsUserProperty.kt @@ -0,0 +1,14 @@ +package org.stepic.droid.configuration.analytic + +import org.stepic.droid.configuration.RemoteConfig +import org.stepik.android.domain.base.analytic.UserProperty + +class PersonalizedOnboardingCourseListsUserProperty( + personalizedOnboardingCourseLists: String +) : UserProperty { + override val name: String = + RemoteConfig.PREFIX + RemoteConfig.PERSONALIZED_ONBOARDING_COURSE_LISTS + + override val value: String = + personalizedOnboardingCourseLists +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/configuration/analytic/SearchQueryParamsUserProperty.kt b/app/src/main/java/org/stepic/droid/configuration/analytic/SearchQueryParamsUserProperty.kt new file mode 100644 index 0000000000..0995d8a6eb --- /dev/null +++ b/app/src/main/java/org/stepic/droid/configuration/analytic/SearchQueryParamsUserProperty.kt @@ -0,0 +1,12 @@ +package org.stepic.droid.configuration.analytic + +import org.stepic.droid.configuration.RemoteConfig +import org.stepik.android.domain.base.analytic.UserProperty + +class SearchQueryParamsUserProperty(searchQueryParams: String) : UserProperty { + override val name: String = + RemoteConfig.PREFIX + RemoteConfig.SEARCH_QUERY_PARAMS_ANDROID + + override val value: String = + searchQueryParams +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/configuration/analytic/ShowStreakDialogAfterLoginUserProperty.kt b/app/src/main/java/org/stepic/droid/configuration/analytic/ShowStreakDialogAfterLoginUserProperty.kt new file mode 100644 index 0000000000..2481cab81b --- /dev/null +++ b/app/src/main/java/org/stepic/droid/configuration/analytic/ShowStreakDialogAfterLoginUserProperty.kt @@ -0,0 +1,12 @@ +package org.stepic.droid.configuration.analytic + +import org.stepic.droid.configuration.RemoteConfig +import org.stepik.android.domain.base.analytic.UserProperty + +class ShowStreakDialogAfterLoginUserProperty(showStreak: Boolean) : UserProperty { + override val name: String = + RemoteConfig.PREFIX + RemoteConfig.SHOW_STREAK_DIALOG_AFTER_LOGIN + + override val value: Boolean = + showStreak +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/core/presenters/SplashPresenter.kt b/app/src/main/java/org/stepic/droid/core/presenters/SplashPresenter.kt index 6a1d659173..6a2eec3349 100644 --- a/app/src/main/java/org/stepic/droid/core/presenters/SplashPresenter.kt +++ b/app/src/main/java/org/stepic/droid/core/presenters/SplashPresenter.kt @@ -16,6 +16,7 @@ import org.stepic.droid.analytic.Analytic import org.stepic.droid.analytic.experiments.DeferredAuthSplitTest import org.stepic.droid.analytic.experiments.OnboardingSplitTestVersion2 import org.stepic.droid.configuration.RemoteConfig +import org.stepic.droid.configuration.analytic.* import org.stepic.droid.core.GoogleApiChecker import org.stepic.droid.core.StepikDevicePoster import org.stepic.droid.core.presenters.contracts.SplashView @@ -173,12 +174,7 @@ constructor( } else { analytic.reportEvent(Analytic.RemoteConfig.FETCHED_UNSUCCESSFUL) } - - analytic - .setUserProperty( - RemoteConfig.PREFIX + RemoteConfig.IS_LOCAL_SUBMISSIONS_ENABLED, - firebaseRemoteConfig[RemoteConfig.IS_LOCAL_SUBMISSIONS_ENABLED].asBoolean().toString() - ) + logRemoteConfig() } try { Tasks.await(remoteConfigTask) @@ -188,6 +184,18 @@ constructor( } } + private fun logRemoteConfig() { + analytic.reportUserProperty(MinDelayRateDialogUserProperty(firebaseRemoteConfig[RemoteConfig.MIN_DELAY_RATE_DIALOG_SEC].asLong())) + analytic.reportUserProperty(ShowStreakDialogAfterLoginUserProperty(firebaseRemoteConfig[RemoteConfig.SHOW_STREAK_DIALOG_AFTER_LOGIN].asBoolean())) + analytic.reportUserProperty(AdaptiveCoursesUserProperty(firebaseRemoteConfig[RemoteConfig.ADAPTIVE_COURSES].asString())) + analytic.reportUserProperty(AdaptiveBackendUrlUserProperty(firebaseRemoteConfig[RemoteConfig.ADAPTIVE_BACKEND_URL].asString())) + analytic.reportUserProperty(LocalSubmissionsEnabledUserProperty(firebaseRemoteConfig[RemoteConfig.IS_LOCAL_SUBMISSIONS_ENABLED].asBoolean())) + analytic.reportUserProperty(SearchQueryParamsUserProperty(firebaseRemoteConfig[RemoteConfig.SEARCH_QUERY_PARAMS_ANDROID].asString())) + analytic.reportUserProperty(NewHomeScreenEnabledUserProperty(firebaseRemoteConfig[RemoteConfig.IS_NEW_HOME_SCREEN_ENABLED].asBoolean())) + analytic.reportUserProperty(PersonalizedOnboardingCourseListsUserProperty(firebaseRemoteConfig[RemoteConfig.PERSONALIZED_ONBOARDING_COURSE_LISTS].asString())) + analytic.reportUserProperty(CourseRevenueAvailableUserProperty(firebaseRemoteConfig[RemoteConfig.IS_COURSE_REVENUE_AVAILABLE_ANDROID].asBoolean())) + } + private fun countNumberOfLaunches() { val numberOfLaunches = sharedPreferenceHelper.incrementNumberOfLaunches() //after first increment it is 0, because of default value is -1. diff --git a/app/src/main/java/org/stepic/droid/di/AppCoreComponent.kt b/app/src/main/java/org/stepic/droid/di/AppCoreComponent.kt index 113c908071..12f8893e74 100644 --- a/app/src/main/java/org/stepic/droid/di/AppCoreComponent.kt +++ b/app/src/main/java/org/stepic/droid/di/AppCoreComponent.kt @@ -72,6 +72,7 @@ import org.stepik.android.view.injection.course_list.user.CourseListUserComponen import org.stepik.android.view.injection.course_list.visited.CourseListVisitedComponent import org.stepik.android.view.injection.course_list.wishlist.CourseListWishComponent import org.stepik.android.view.injection.course_payments.CoursePaymentsDataModule +import org.stepik.android.view.injection.course_purchase.CoursePurchaseComponent import org.stepik.android.view.injection.course_reviews.ComposeCourseReviewComponent import org.stepik.android.view.injection.course_search.CourseSearchComponent import org.stepik.android.view.injection.debug.DebugComponent @@ -286,6 +287,8 @@ interface AppCoreComponent { fun courseSearchComponentBuilder(): CourseSearchComponent.Builder + fun coursePurchaseComponentBuilder(): CoursePurchaseComponent.Builder + fun inject(someActivity: FragmentActivityBase) fun inject(adapter: StepikRadioGroupAdapter) diff --git a/app/src/main/java/org/stepic/droid/notifications/badges/NotificationsBadgesManager.kt b/app/src/main/java/org/stepic/droid/notifications/badges/NotificationsBadgesManager.kt index 1e4183f8fa..f66eedabf8 100644 --- a/app/src/main/java/org/stepic/droid/notifications/badges/NotificationsBadgesManager.kt +++ b/app/src/main/java/org/stepic/droid/notifications/badges/NotificationsBadgesManager.kt @@ -64,7 +64,7 @@ constructor( @MainThread private fun updateCounter(count: Int) { - if (firebaseRemoteConfig[RemoteConfig.SHOW_NOTIFICATIONS_BADGES].asBoolean() && count != 0) { + if (count != 0) { ShortcutBadger.applyCount(context, count) listenerContainer.asIterable().forEach { it.onBadgeCountChanged(count) diff --git a/app/src/main/java/org/stepik/android/domain/base/analytic/UserProperty.kt b/app/src/main/java/org/stepik/android/domain/base/analytic/UserProperty.kt new file mode 100644 index 0000000000..8b54aa6188 --- /dev/null +++ b/app/src/main/java/org/stepik/android/domain/base/analytic/UserProperty.kt @@ -0,0 +1,11 @@ +package org.stepik.android.domain.base.analytic + +import java.util.EnumSet + +interface UserProperty { + val name: String + val value: Any + + val sources: EnumSet + get() = EnumSet.allOf(UserPropertySource::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/base/analytic/UserPropertySource.kt b/app/src/main/java/org/stepik/android/domain/base/analytic/UserPropertySource.kt new file mode 100644 index 0000000000..782f47c634 --- /dev/null +++ b/app/src/main/java/org/stepik/android/domain/base/analytic/UserPropertySource.kt @@ -0,0 +1,5 @@ +package org.stepik.android.domain.base.analytic + +enum class UserPropertySource { + AMPLITUDE, YANDEX, FIREBASE +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/course/analytic/CourseViewSource.kt b/app/src/main/java/org/stepik/android/domain/course/analytic/CourseViewSource.kt index ac39a5a65e..e7754bacd9 100644 --- a/app/src/main/java/org/stepik/android/domain/course/analytic/CourseViewSource.kt +++ b/app/src/main/java/org/stepik/android/domain/course/analytic/CourseViewSource.kt @@ -125,6 +125,11 @@ sealed class CourseViewSource : AnalyticEvent, Serializable { "user_reviews" } + object CoursePurchase : CourseViewSource() { + override val name: String = + "course_purchase" + } + object Unknown : CourseViewSource() { override val name: String = "unknown" diff --git a/app/src/main/java/org/stepik/android/domain/course_payments/model/DeeplinkPromoCode.kt b/app/src/main/java/org/stepik/android/domain/course_payments/model/DeeplinkPromoCode.kt index 171939a9a3..87f4af5607 100644 --- a/app/src/main/java/org/stepik/android/domain/course_payments/model/DeeplinkPromoCode.kt +++ b/app/src/main/java/org/stepik/android/domain/course_payments/model/DeeplinkPromoCode.kt @@ -1,17 +1,15 @@ package org.stepik.android.domain.course_payments.model import android.os.Parcelable -import com.google.gson.annotations.SerializedName import kotlinx.android.parcel.Parcelize @Parcelize data class DeeplinkPromoCode( - @SerializedName("price") + val name: String, val price: String, - @SerializedName("currency_code") val currencyCode: String ) : Parcelable { companion object { - val EMPTY = DeeplinkPromoCode("", "") + val EMPTY = DeeplinkPromoCode("", "", "") } } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/course_purchase/interactor/CoursePurchaseInteractor.kt b/app/src/main/java/org/stepik/android/domain/course_purchase/interactor/CoursePurchaseInteractor.kt new file mode 100644 index 0000000000..c50594f230 --- /dev/null +++ b/app/src/main/java/org/stepik/android/domain/course_purchase/interactor/CoursePurchaseInteractor.kt @@ -0,0 +1,16 @@ +package org.stepik.android.domain.course_purchase.interactor + +import io.reactivex.Single +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode +import org.stepik.android.domain.course_payments.repository.CoursePaymentsRepository +import javax.inject.Inject + +class CoursePurchaseInteractor +@Inject +constructor( + private val coursePaymentsRepository: CoursePaymentsRepository +) { + fun checkPromoCodeValidity(courseId: Long, promoCodeName: String): Single = + coursePaymentsRepository + .checkDeeplinkPromoCodeValidity(courseId, promoCodeName) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/course_purchase/CoursePurchaseFeature.kt b/app/src/main/java/org/stepik/android/presentation/course_purchase/CoursePurchaseFeature.kt new file mode 100644 index 0000000000..5228b9bd09 --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/course_purchase/CoursePurchaseFeature.kt @@ -0,0 +1,63 @@ +package org.stepik.android.presentation.course_purchase + +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode +import org.stepik.android.domain.wishlist.model.WishlistEntity +import org.stepik.android.domain.wishlist.model.WishlistOperationData +import org.stepik.android.model.Course +import org.stepik.android.presentation.course_purchase.model.CoursePurchaseData +import org.stepik.android.view.course.model.CoursePromoCodeInfo + +interface CoursePurchaseFeature { + sealed class State { + object Idle : State() + data class Content( + val coursePurchaseData: CoursePurchaseData, + val promoCodeState: PromoCodeState, + val wishlistState: WishlistState + ) : State() + } + + sealed class Message { + data class InitMessage(val coursePurchaseData: CoursePurchaseData, val initialCoursePromoCodeInfo: CoursePromoCodeInfo) : Message() + + /** + * Wishlist messages + */ + object WishlistAddMessage : Message() + data class WishlistAddSuccess(val wishlistEntity: WishlistEntity) : Message() + object WishlistAddFailure : Message() + + /** + * PromoCode + */ + object PromoCodeEditingMessage : Message() + data class PromoCodeCheckMessage(val text: String) : Message() + data class PromoCodeValidMessage(val deeplinkPromoCode: DeeplinkPromoCode) : Message() + object PromoCodeInvalidMessage : Message() + } + + sealed class Action { + data class AddToWishlist( + val course: Course, + val wishlistEntity: WishlistEntity, + val wishlistOperationData: WishlistOperationData + ) : Action() + + data class CheckPromoCode(val courseId: Long, val promoCodeName: String) : Action() + sealed class ViewAction : Action() + } + + sealed class PromoCodeState { + object Idle : PromoCodeState() + object Editing : PromoCodeState() + data class Checking(val text: String) : PromoCodeState() + data class Valid(val text: String, val coursePromoCodeInfo: CoursePromoCodeInfo) : PromoCodeState() + object Invalid : PromoCodeState() + } + + sealed class WishlistState { + object Idle : WishlistState() + object Adding : WishlistState() + object Wishlisted : WishlistState() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/course_purchase/CoursePurchaseViewModel.kt b/app/src/main/java/org/stepik/android/presentation/course_purchase/CoursePurchaseViewModel.kt new file mode 100644 index 0000000000..2802ae32cf --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/course_purchase/CoursePurchaseViewModel.kt @@ -0,0 +1,8 @@ +package org.stepik.android.presentation.course_purchase + +import ru.nobird.android.presentation.redux.container.ReduxViewContainer +import ru.nobird.android.view.redux.viewmodel.ReduxViewModel + +class CoursePurchaseViewModel( + reduxViewContainer: ReduxViewContainer +) : ReduxViewModel(reduxViewContainer) \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/course_purchase/dispatcher/CoursePurchaseActionDispatcher.kt b/app/src/main/java/org/stepik/android/presentation/course_purchase/dispatcher/CoursePurchaseActionDispatcher.kt new file mode 100644 index 0000000000..36466be390 --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/course_purchase/dispatcher/CoursePurchaseActionDispatcher.kt @@ -0,0 +1,62 @@ +package org.stepik.android.presentation.course_purchase.dispatcher + +import io.reactivex.Scheduler +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.subscribeBy +import org.stepic.droid.analytic.Analytic +import org.stepic.droid.di.qualifiers.BackgroundScheduler +import org.stepic.droid.di.qualifiers.MainScheduler +import org.stepik.android.domain.course.analytic.CourseViewSource +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode +import org.stepik.android.domain.course_purchase.interactor.CoursePurchaseInteractor +import org.stepik.android.domain.wishlist.analytic.CourseWishlistAddedEvent +import org.stepik.android.domain.wishlist.interactor.WishlistInteractor +import org.stepik.android.presentation.course_purchase.CoursePurchaseFeature +import ru.nobird.android.presentation.redux.dispatcher.RxActionDispatcher +import javax.inject.Inject + +class CoursePurchaseActionDispatcher +@Inject +constructor( + private val analytic: Analytic, + private val wishlistInteractor: WishlistInteractor, + private val coursePurchaseInteractor: CoursePurchaseInteractor, + @BackgroundScheduler + private val backgroundScheduler: Scheduler, + @MainScheduler + private val mainScheduler: Scheduler +) : RxActionDispatcher() { + override fun handleAction(action: CoursePurchaseFeature.Action) { + when (action) { + is CoursePurchaseFeature.Action.AddToWishlist -> { + compositeDisposable += wishlistInteractor + .updateWishlistWithOperation(action.wishlistEntity, action.wishlistOperationData) + .subscribeOn(backgroundScheduler) + .observeOn(mainScheduler) + .subscribeBy( + onSuccess = { + analytic.report(CourseWishlistAddedEvent(action.course, CourseViewSource.CoursePurchase)) + onNewMessage(CoursePurchaseFeature.Message.WishlistAddSuccess(it)) + }, + onError = { onNewMessage(CoursePurchaseFeature.Message.WishlistAddFailure) } + ) + } + is CoursePurchaseFeature.Action.CheckPromoCode -> { + compositeDisposable += coursePurchaseInteractor + .checkPromoCodeValidity(action.courseId, action.promoCodeName) + .subscribeOn(backgroundScheduler) + .observeOn(mainScheduler) + .subscribeBy( + onSuccess = { + if (it == DeeplinkPromoCode.EMPTY) { + onNewMessage(CoursePurchaseFeature.Message.PromoCodeInvalidMessage) + } else { + onNewMessage(CoursePurchaseFeature.Message.PromoCodeValidMessage(it)) + } + }, + onError = { onNewMessage(CoursePurchaseFeature.Message.PromoCodeInvalidMessage) } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/course_purchase/model/CoursePurchaseData.kt b/app/src/main/java/org/stepik/android/presentation/course_purchase/model/CoursePurchaseData.kt new file mode 100644 index 0000000000..a5588ac730 --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/course_purchase/model/CoursePurchaseData.kt @@ -0,0 +1,19 @@ +package org.stepik.android.presentation.course_purchase.model + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +import org.stepik.android.domain.course.model.CourseStats +import org.stepik.android.domain.course_payments.model.DeeplinkPromoCode +import org.stepik.android.domain.course_payments.model.DefaultPromoCode +import org.stepik.android.domain.wishlist.model.WishlistEntity +import org.stepik.android.model.Course + +@Parcelize +data class CoursePurchaseData( + val course: Course, + val stats: CourseStats, + val deeplinkPromoCode: DeeplinkPromoCode, + val defaultPromoCode: DefaultPromoCode, + val wishlistEntity: WishlistEntity, + val isWishlisted: Boolean +) : Parcelable diff --git a/app/src/main/java/org/stepik/android/presentation/course_purchase/reducer/CoursePurchaseReducer.kt b/app/src/main/java/org/stepik/android/presentation/course_purchase/reducer/CoursePurchaseReducer.kt new file mode 100644 index 0000000000..a121e3487e --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/course_purchase/reducer/CoursePurchaseReducer.kt @@ -0,0 +1,93 @@ +package org.stepik.android.presentation.course_purchase.reducer + +import org.stepik.android.domain.course_payments.model.DefaultPromoCode +import org.stepik.android.domain.wishlist.model.WishlistOperationData +import org.stepik.android.presentation.course_purchase.CoursePurchaseFeature.State +import org.stepik.android.presentation.course_purchase.CoursePurchaseFeature.Message +import org.stepik.android.presentation.course_purchase.CoursePurchaseFeature.Action +import org.stepik.android.presentation.course_purchase.CoursePurchaseFeature +import org.stepik.android.presentation.wishlist.model.WishlistAction +import org.stepik.android.view.course.resolver.CoursePromoCodeResolver +import ru.nobird.android.core.model.mutate +import ru.nobird.android.presentation.redux.reducer.StateReducer +import javax.inject.Inject + +class CoursePurchaseReducer +@Inject +constructor( + private val coursePromoCodeResolver: CoursePromoCodeResolver +) : StateReducer { + override fun reduce(state: State, message: Message): Pair> = + when (message) { + is Message.InitMessage -> { + if (state is State.Idle) { + val promoCodeState = if (message.initialCoursePromoCodeInfo.hasPromo) { + CoursePurchaseFeature.PromoCodeState.Valid(message.initialCoursePromoCodeInfo.name, message.initialCoursePromoCodeInfo) + } else { + CoursePurchaseFeature.PromoCodeState.Idle + } + val wishlistState = if (message.coursePurchaseData.isWishlisted) { + CoursePurchaseFeature.WishlistState.Wishlisted + } else { + CoursePurchaseFeature.WishlistState.Idle + } + State.Content(message.coursePurchaseData, promoCodeState, wishlistState) to emptySet() + } else { + null + } + } + is Message.WishlistAddMessage -> { + if (state is State.Content) { + val wishlistEntity = state.coursePurchaseData.wishlistEntity.copy(courses = state.coursePurchaseData.wishlistEntity.courses.mutate { add(0, state.coursePurchaseData.course.id) }) + val wishlistOperationData = WishlistOperationData(state.coursePurchaseData.course.id, WishlistAction.ADD) + state.copy(wishlistState = CoursePurchaseFeature.WishlistState.Adding)to setOf(Action.AddToWishlist(state.coursePurchaseData.course, wishlistEntity, wishlistOperationData)) + } else { + null + } + } + is Message.WishlistAddSuccess -> { + if (state is State.Content) { + val updatedCoursePurchaseData = state.coursePurchaseData.copy(wishlistEntity = message.wishlistEntity, isWishlisted = true) + state.copy(coursePurchaseData = updatedCoursePurchaseData, wishlistState = CoursePurchaseFeature.WishlistState.Wishlisted) to emptySet() + } else { + null + } + } + is Message.WishlistAddFailure -> { + if (state is State.Content) { + state.copy(wishlistState = CoursePurchaseFeature.WishlistState.Idle) to emptySet() + } else { + null + } + } + is Message.PromoCodeEditingMessage -> { + if (state is State.Content) { + state.copy(promoCodeState = CoursePurchaseFeature.PromoCodeState.Editing) to emptySet() + } else { + null + } + } + is Message.PromoCodeCheckMessage -> { + if (state is State.Content && state.promoCodeState is CoursePurchaseFeature.PromoCodeState.Editing) { + state.copy(promoCodeState = CoursePurchaseFeature.PromoCodeState.Checking(message.text)) to setOf(Action.CheckPromoCode(state.coursePurchaseData.course.id, message.text)) + } else { + null + } + } + is Message.PromoCodeValidMessage -> { + if (state is State.Content && state.promoCodeState is CoursePurchaseFeature.PromoCodeState.Checking) { + val coursePromoCodeInfo = coursePromoCodeResolver.resolvePromoCodeInfo(message.deeplinkPromoCode, DefaultPromoCode.EMPTY, state.coursePurchaseData.course) + state.copy(promoCodeState = CoursePurchaseFeature.PromoCodeState.Valid(state.promoCodeState.text, coursePromoCodeInfo)) to emptySet() + } else { + null + } + } + is Message.PromoCodeInvalidMessage -> { + if (state is State.Content && state.promoCodeState is CoursePurchaseFeature.PromoCodeState.Checking) { + state.copy(promoCodeState = CoursePurchaseFeature.PromoCodeState.Invalid) to emptySet() + } else { + null + } + } + } ?: state to emptySet() +} \ 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 8a989004b1..ce930825c2 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 @@ -49,8 +49,8 @@ constructor( override fun checkDeeplinkPromoCodeValidity(courseId: Long, name: String): Single = coursePaymentService - .checkDeeplinkPromoCodeValidity(PromoCodeRequest( - course = courseId, - name = name - )) + .checkDeeplinkPromoCodeValidity(PromoCodeRequest(course = courseId, name = name)) + .map { response -> + DeeplinkPromoCode(name, response.price, response.currencyCode) + } } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/course_payments/model/PromoCodeResponse.kt b/app/src/main/java/org/stepik/android/remote/course_payments/model/PromoCodeResponse.kt new file mode 100644 index 0000000000..7bb271ea4c --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/course_payments/model/PromoCodeResponse.kt @@ -0,0 +1,13 @@ +package org.stepik.android.remote.course_payments.model + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class PromoCodeResponse( + @SerializedName("price") + val price: String, + @SerializedName("currency_code") + val currencyCode: String +) : Parcelable \ No newline at end of file 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 75bea40656..fab512f208 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,10 +1,10 @@ package org.stepik.android.remote.course_payments.service import io.reactivex.Single -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 +import org.stepik.android.remote.course_payments.model.PromoCodeResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -24,5 +24,5 @@ interface CoursePaymentService { @POST("api/promo-codes/check") 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/model/CoursePromoCodeInfo.kt b/app/src/main/java/org/stepik/android/view/course/model/CoursePromoCodeInfo.kt index fd1a5ef1d7..87c86ae1c5 100644 --- 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 @@ -1,6 +1,7 @@ package org.stepik.android.view.course.model data class CoursePromoCodeInfo( + val name: String, val currencyCode: String, val price: String, val hasPromo: Boolean 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 index 32372d1f67..39651d49ab 100644 --- 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 @@ -13,13 +13,13 @@ constructor() { fun resolvePromoCodeInfo(deeplinkPromoCode: DeeplinkPromoCode, defaultPromoCode: DefaultPromoCode, course: Course): CoursePromoCodeInfo = when { deeplinkPromoCode != DeeplinkPromoCode.EMPTY -> - CoursePromoCodeInfo(deeplinkPromoCode.currencyCode, deeplinkPromoCode.price, true) + CoursePromoCodeInfo(deeplinkPromoCode.name, 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) + CoursePromoCodeInfo(defaultPromoCode.defaultPromoCodeName, course.currencyCode!!, defaultPromoCode.defaultPromoCodePrice, true) else -> - CoursePromoCodeInfo("", "", false) + CoursePromoCodeInfo("", "", "", false) } } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course/ui/activity/CourseActivity.kt b/app/src/main/java/org/stepik/android/view/course/ui/activity/CourseActivity.kt index 954ae84bb9..f78686d657 100644 --- a/app/src/main/java/org/stepik/android/view/course/ui/activity/CourseActivity.kt +++ b/app/src/main/java/org/stepik/android/view/course/ui/activity/CourseActivity.kt @@ -56,6 +56,7 @@ import org.stepik.android.view.course.routing.getPromoCodeFromDeepLink import org.stepik.android.view.course.ui.adapter.CoursePagerAdapter import org.stepik.android.view.course.ui.delegates.CourseHeaderDelegate import org.stepik.android.view.course_content.ui.fragment.CourseContentFragment +import org.stepik.android.view.course_purchase.ui.dialog.CoursePurchaseBottomSheetDialogFragment import org.stepik.android.view.course_reviews.ui.fragment.CourseReviewsFragment import org.stepik.android.view.course_search.dialog.CourseSearchDialogFragment import org.stepik.android.view.fragment_pager.FragmentDelegateScrollStateChangeListener @@ -221,7 +222,13 @@ class CourseActivity : FragmentActivityBase(), CourseView, InAppWebViewDialogFra CourseSearchDialogFragment .newInstance(courseId, course?.title.orEmpty()) .showIfNotExists(supportFragmentManager, CourseSearchDialogFragment.TAG) - } + }, + coursePurchaseFlowAction = { + CoursePurchaseBottomSheetDialogFragment + .newInstance(it) + .showIfNotExists(supportFragmentManager, CoursePurchaseBottomSheetDialogFragment.TAG) + }, + currentPurchaseFlow = firebaseRemoteConfig[RemoteConfig.PURCHASE_FLOW_ANDROID].asString() ) uiCheckout = Checkout.forActivity(this, billing) 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 ef85058591..fc5e93b735 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 @@ -39,6 +39,7 @@ import org.stepik.android.domain.course.model.EnrollmentState import org.stepik.android.domain.course_continue.analytic.CourseContinuePressedEvent import org.stepik.android.presentation.course.CoursePresenter import org.stepik.android.presentation.course_continue.model.CourseContinueInteractionSource +import org.stepik.android.presentation.course_purchase.model.CoursePurchaseData 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 @@ -65,11 +66,16 @@ constructor( @Assisted private val showCourseBenefitsAction: () -> Unit, @Assisted onSubmissionCountClicked: () -> Unit, @Assisted isLocalSubmissionsEnabled: Boolean, - @Assisted private val showSearchCourseAction: () -> Unit + @Assisted private val showSearchCourseAction: () -> Unit, + @Assisted private val coursePurchaseFlowAction: (CoursePurchaseData) -> Unit, + @Assisted private val currentPurchaseFlow: String ) { companion object { private val CourseHeaderData.enrolledState: EnrollmentState.Enrolled? get() = stats.enrollmentState.safeCast() + + private const val PURCHASE_FLOW_IAP = "iap" + private const val PURCHASE_FLOW_WEB = "web" } var courseHeaderData: CourseHeaderData? = null @@ -138,8 +144,41 @@ constructor( } } - courseBuyInWebAction.setOnClickListener { buyInWebAction() } - courseBuyInWebActionDiscounted.setOnClickListener { buyInWebAction() } + courseBuyInWebAction.setOnClickListener { + if (currentPurchaseFlow == PURCHASE_FLOW_IAP) { + courseHeaderData?.let { + val coursePurchaseData = CoursePurchaseData( + it.course, + it.stats, + it.deeplinkPromoCode, + it.defaultPromoCode, + it.wishlistEntity, + it.stats.isWishlisted + ) + coursePurchaseFlowAction(coursePurchaseData) + } + } else { + buyInWebAction() + } + } + + courseBuyInWebActionDiscounted.setOnClickListener { + if (currentPurchaseFlow == PURCHASE_FLOW_IAP) { + courseHeaderData?.let { + val coursePurchaseData = CoursePurchaseData( + it.course, + it.stats, + it.deeplinkPromoCode, + it.defaultPromoCode, + it.wishlistEntity, + it.stats.isWishlisted + ) + coursePurchaseFlowAction(coursePurchaseData) + } + } else { + buyInWebAction() + } + } courseBuyInAppAction.setOnClickListener { courseHeaderData?.let { headerData -> @@ -210,7 +249,7 @@ constructor( courseStatsDelegate.setStats(courseHeaderData.stats) } - val (currencyCode, promoPrice, hasPromo) = coursePromoCodeResolver.resolvePromoCodeInfo( + val (_, currencyCode, promoPrice, hasPromo) = coursePromoCodeResolver.resolvePromoCodeInfo( courseHeaderData.deeplinkPromoCode, courseHeaderData.defaultPromoCode, courseHeaderData.course diff --git a/app/src/main/java/org/stepik/android/view/course_purchase/delegate/PromoCodeViewDelegate.kt b/app/src/main/java/org/stepik/android/view/course_purchase/delegate/PromoCodeViewDelegate.kt new file mode 100644 index 0000000000..c2fbfe904a --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/course_purchase/delegate/PromoCodeViewDelegate.kt @@ -0,0 +1,148 @@ +package org.stepik.android.view.course_purchase.delegate + +import android.graphics.drawable.AnimationDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.RippleDrawable +import android.text.Editable +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged +import org.stepic.droid.R +import org.stepic.droid.databinding.BottomSheetDialogCoursePurchaseBinding +import org.stepik.android.presentation.course_purchase.CoursePurchaseFeature +import org.stepik.android.presentation.course_purchase.model.CoursePurchaseData +import org.stepik.android.view.course.mapper.DisplayPriceMapper +import org.stepik.android.presentation.course_purchase.CoursePurchaseFeature.PromoCodeState +import org.stepik.android.presentation.course_purchase.CoursePurchaseViewModel +import org.stepik.android.view.step_quiz_choice.ui.delegate.LayerListDrawableDelegate +import ru.nobird.android.view.base.ui.extension.getDrawableCompat + +class PromoCodeViewDelegate( + coursePurchaseBinding: BottomSheetDialogCoursePurchaseBinding, + private val coursePurchaseViewModel: CoursePurchaseViewModel, + private val coursePurchaseData: CoursePurchaseData, + private val displayPriceMapper: DisplayPriceMapper +) { + private val context = coursePurchaseBinding.root.context + private val coursePromoCodeAction = coursePurchaseBinding.coursePromoCodeAction + private val coursePromoCodeContainer = coursePurchaseBinding.coursePurchasePromoCodeInputContainer + private val coursePromoCodeInput = coursePurchaseBinding.coursePurchasePromoCodeInput + private val coursePromoCodeDismiss = coursePurchaseBinding.coursePurchasePromoCodeInputDismiss + private val coursePromoCodeSubmitAction = coursePurchaseBinding.coursePurchasePromoCodeSubmitAction + private val coursePurchasePromoCodeResultMessage = coursePurchaseBinding.coursePurchasePromoCodeResultMessage + private val coursePurchaseBuyAction = coursePurchaseBinding.coursePurchaseBuyAction + + private val layerListDrawableDelegate = LayerListDrawableDelegate( + listOf( + R.id.idle_state, + R.id.loading_state, + R.id.invalid_state, + R.id.valid_state + ), + (coursePromoCodeSubmitAction.background as RippleDrawable).findDrawableByLayerId(R.id.promo_code_layer_list) as LayerDrawable + ) + + init { + coursePromoCodeAction.setOnClickListener { + coursePurchaseViewModel.onNewMessage(CoursePurchaseFeature.Message.PromoCodeEditingMessage) + } + coursePromoCodeInput.doAfterTextChanged { text: Editable? -> + val length = text?.length ?: 0 + coursePromoCodeDismiss.isVisible = length != 0 + coursePromoCodeSubmitAction.isVisible = length != 0 + } + coursePromoCodeDismiss.setOnClickListener { + coursePromoCodeInput.setText("") + coursePurchaseViewModel.onNewMessage(CoursePurchaseFeature.Message.PromoCodeEditingMessage) + } + coursePromoCodeSubmitAction.setOnClickListener { coursePurchaseViewModel.onNewMessage(CoursePurchaseFeature.Message.PromoCodeCheckMessage(coursePromoCodeInput.text.toString())) } + } + + fun render(state: PromoCodeState) { + coursePromoCodeAction.isVisible = state is PromoCodeState.Idle + coursePromoCodeContainer.isVisible = state !is PromoCodeState.Idle + coursePromoCodeDismiss.isEnabled = state !is PromoCodeState.Checking + coursePromoCodeSubmitAction.isEnabled = state is PromoCodeState.Editing + coursePromoCodeInput.isEnabled = state is PromoCodeState.Editing + + val courseDisplayPrice = coursePurchaseData.course.displayPrice + + coursePurchaseBuyAction.text = + if (courseDisplayPrice != null) { + if (state is PromoCodeState.Valid) { + displayPriceMapper.mapToDiscountedDisplayPriceSpannedString(courseDisplayPrice, state.coursePromoCodeInfo.currencyCode, state.coursePromoCodeInfo.price) + } else { + context.getString(R.string.course_payments_purchase_in_web_with_price, courseDisplayPrice) + } + } else { + context.getString(R.string.course_payments_purchase_in_web) + } + + coursePurchasePromoCodeResultMessage.isVisible = state is PromoCodeState.Checking || state is PromoCodeState.Valid || state is PromoCodeState.Invalid + + val (messageRes, colorRes) = getPromoCodeResultMessage(state) + if (messageRes != -1 && colorRes != -1) { + coursePurchasePromoCodeResultMessage.text = context.getString(messageRes) + coursePurchasePromoCodeResultMessage.setTextColor(AppCompatResources.getColorStateList(context, colorRes)) + } + + coursePromoCodeSubmitAction.setImageDrawable(getDrawableForSubmitAction(state)) + setEditTextFromState(state) + layerListDrawableDelegate.showLayer(getBackgroundLayer(state)) + } + + private fun getPromoCodeResultMessage(promoCodeState: PromoCodeState): Pair = + when (promoCodeState) { + is PromoCodeState.Idle, is PromoCodeState.Editing -> + -1 to -1 + is PromoCodeState.Checking -> + R.string.course_purchase_promocode_checking to R.color.color_overlay_violet + is PromoCodeState.Valid -> + R.string.course_purchase_promocode_valid to R.color.color_overlay_green + is PromoCodeState.Invalid -> + R.string.course_purchase_promocode_invalid to R.color.color_overlay_red + } + + private fun setEditTextFromState(state: PromoCodeState) { + when (state) { + is PromoCodeState.Checking -> + coursePromoCodeInput.setText(state.text) + is PromoCodeState.Valid -> + coursePromoCodeInput.setText(state.text) + else -> + return + } + } + + private fun getDrawableForSubmitAction(state: PromoCodeState): Drawable? = + when (state) { + is PromoCodeState.Idle, is PromoCodeState.Editing -> + AppCompatResources.getDrawable(context, R.drawable.ic_arrow_forward) + is PromoCodeState.Checking -> { + val evaluationDrawable = AnimationDrawable() + evaluationDrawable.addFrame(context.getDrawableCompat(R.drawable.ic_step_quiz_evaluation_frame_1), 200) + evaluationDrawable.addFrame(context.getDrawableCompat(R.drawable.ic_step_quiz_evaluation_frame_2), 200) + evaluationDrawable.addFrame(context.getDrawableCompat(R.drawable.ic_step_quiz_evaluation_frame_3), 200) + evaluationDrawable.isOneShot = false + evaluationDrawable.start() + evaluationDrawable + } + is PromoCodeState.Invalid -> + AppCompatResources.getDrawable(context, R.drawable.ic_step_quiz_wrong) + is PromoCodeState.Valid -> + AppCompatResources.getDrawable(context, R.drawable.ic_step_quiz_correct) + } + + private fun getBackgroundLayer(state: PromoCodeState): Int = + when (state) { + is PromoCodeState.Idle, is PromoCodeState.Editing -> + R.id.idle_state + is PromoCodeState.Checking -> + R.id.loading_state + is PromoCodeState.Invalid -> + R.id.invalid_state + is PromoCodeState.Valid -> + R.id.valid_state + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course_purchase/delegate/WishlistViewDelegate.kt b/app/src/main/java/org/stepik/android/view/course_purchase/delegate/WishlistViewDelegate.kt new file mode 100644 index 0000000000..d841ae8cc4 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/course_purchase/delegate/WishlistViewDelegate.kt @@ -0,0 +1,47 @@ +package org.stepik.android.view.course_purchase.delegate + +import android.graphics.drawable.AnimationDrawable +import com.google.android.material.button.MaterialButton +import org.stepic.droid.R +import org.stepik.android.presentation.course_purchase.CoursePurchaseFeature.WishlistState +import ru.nobird.android.view.base.ui.extension.getDrawableCompat + +class WishlistViewDelegate( + private val wishlistButton: MaterialButton +) { + companion object { + private const val EVALUATION_FRAME_DURATION_MS = 250 + } + + private val context = wishlistButton.context + + fun render(state: WishlistState) { + val messageResId = + when (state) { + WishlistState.Idle -> + R.string.course_purchase_wishlist_add + WishlistState.Adding -> + R.string.course_purchase_wishlist_adding + WishlistState.Wishlisted -> + R.string.course_purchase_wishlist_added + } + wishlistButton.isEnabled = state is WishlistState.Idle + wishlistButton.setText(messageResId) + resolveButtonDrawable(state) + } + + private fun resolveButtonDrawable(state: WishlistState) { + if (state is WishlistState.Adding) { + val evaluationDrawable = AnimationDrawable() + evaluationDrawable.addFrame(context.getDrawableCompat(R.drawable.ic_step_quiz_evaluation_frame_1), EVALUATION_FRAME_DURATION_MS) + evaluationDrawable.addFrame(context.getDrawableCompat(R.drawable.ic_step_quiz_evaluation_frame_2), EVALUATION_FRAME_DURATION_MS) + evaluationDrawable.addFrame(context.getDrawableCompat(R.drawable.ic_step_quiz_evaluation_frame_3), EVALUATION_FRAME_DURATION_MS) + evaluationDrawable.isOneShot = false + + wishlistButton.setCompoundDrawablesWithIntrinsicBounds(evaluationDrawable, null, null, null) + evaluationDrawable.start() + } else { + wishlistButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course_purchase/ui/dialog/CoursePurchaseBottomSheetDialogFragment.kt b/app/src/main/java/org/stepik/android/view/course_purchase/ui/dialog/CoursePurchaseBottomSheetDialogFragment.kt new file mode 100644 index 0000000000..20ba889f2c --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/course_purchase/ui/dialog/CoursePurchaseBottomSheetDialogFragment.kt @@ -0,0 +1,164 @@ +package org.stepik.android.view.course_purchase.ui.dialog + +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.core.text.buildSpannedString +import androidx.core.text.inSpans +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import by.kirich1409.viewbindingdelegate.viewBinding +import com.bumptech.glide.Glide +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.stepic.droid.R +import org.stepic.droid.base.App +import org.stepic.droid.databinding.BottomSheetDialogCoursePurchaseBinding +import org.stepik.android.presentation.course_purchase.CoursePurchaseFeature +import org.stepik.android.presentation.course_purchase.CoursePurchaseViewModel +import org.stepik.android.presentation.course_purchase.model.CoursePurchaseData +import org.stepik.android.view.course.mapper.DisplayPriceMapper +import org.stepik.android.view.course.resolver.CoursePromoCodeResolver +import org.stepik.android.view.course_purchase.delegate.PromoCodeViewDelegate +import org.stepik.android.view.course_purchase.delegate.WishlistViewDelegate +import org.stepik.android.view.in_app_web_view.ui.dialog.InAppWebViewDialogFragment +import ru.nobird.android.presentation.redux.container.ReduxView +import ru.nobird.android.view.base.ui.extension.argument +import ru.nobird.android.view.base.ui.extension.showIfNotExists +import ru.nobird.android.view.redux.ui.extension.reduxViewModel +import javax.inject.Inject + +class CoursePurchaseBottomSheetDialogFragment : + BottomSheetDialogFragment(), + ReduxView { + companion object { + const val TAG = "CoursePurchaseBottomSheetDialogFragment" + + fun newInstance(coursePurchaseData: CoursePurchaseData): DialogFragment = + CoursePurchaseBottomSheetDialogFragment().apply { + this.coursePurchaseData = coursePurchaseData + } + } + + @Inject + internal lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + internal lateinit var displayPriceMapper: DisplayPriceMapper + + @Inject + internal lateinit var coursePromoCodeResolver: CoursePromoCodeResolver + + private var coursePurchaseData: CoursePurchaseData by argument() + + private val coursePurchaseViewModel: CoursePurchaseViewModel by reduxViewModel(this) { viewModelFactory } + private val coursePurchaseBinding: BottomSheetDialogCoursePurchaseBinding by viewBinding(BottomSheetDialogCoursePurchaseBinding::bind) + + private lateinit var promoCodeViewDelegate: PromoCodeViewDelegate + private lateinit var wishlistViewDelegate: WishlistViewDelegate + + private fun injectComponent() { + App + .component() + .coursePurchaseComponentBuilder() + .build() + .inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + injectComponent() + setStyle(DialogFragment.STYLE_NO_TITLE, R.style.TopCornersRoundedBottomSheetDialog) + val initialCoursePromoCodeInfo = coursePromoCodeResolver.resolvePromoCodeInfo(coursePurchaseData.deeplinkPromoCode, coursePurchaseData.defaultPromoCode, coursePurchaseData.course) + coursePurchaseViewModel.onNewMessage(CoursePurchaseFeature.Message.InitMessage(coursePurchaseData, initialCoursePromoCodeInfo)) + } + + override fun onStart() { + super.onStart() + (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflater.inflate(R.layout.bottom_sheet_dialog_course_purchase, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + promoCodeViewDelegate = PromoCodeViewDelegate(coursePurchaseBinding, coursePurchaseViewModel, coursePurchaseData, displayPriceMapper) + wishlistViewDelegate = WishlistViewDelegate(coursePurchaseBinding.coursePurchaseWishlistAction) + coursePurchaseBinding.coursePurchaseWishlistAction.setOnClickListener { + coursePurchaseViewModel.onNewMessage(CoursePurchaseFeature.Message.WishlistAddMessage) + } + + coursePurchaseBinding.coursePurchaseCourseTitle.text = coursePurchaseData.course.title.orEmpty() + Glide + .with(requireContext()) + .asBitmap() + .load(coursePurchaseData.course.cover) + .placeholder(R.drawable.general_placeholder) + .fitCenter() + .into(coursePurchaseBinding.coursePurchaseCourseIcon) + + val userAgreementLinkSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + val userAgreementUrl = getString(R.string.course_purchase_commission_url) + + InAppWebViewDialogFragment + .newInstance(getString(R.string.course_purchase_commission_web_view_title), userAgreementUrl, isProvideAuth = false) + .showIfNotExists(childFragmentManager, InAppWebViewDialogFragment.TAG) + } + } + coursePurchaseBinding.coursePurchaseCommissionNotice.text = buildSpannedString { + append(getString(R.string.course_purchase_commission_information_part_1)) + inSpans(userAgreementLinkSpan) { + append(getString(R.string.course_purchase_commission_information_part_2)) + } + append(getString(R.string.full_stop)) + } + coursePurchaseBinding.coursePurchaseCommissionNotice.movementMethod = LinkMovementMethod.getInstance() + } + + override fun onAction(action: CoursePurchaseFeature.Action.ViewAction) { + // no op + } + + override fun render(state: CoursePurchaseFeature.State) { + if (state is CoursePurchaseFeature.State.Content) { + promoCodeViewDelegate.render(state.promoCodeState) + wishlistViewDelegate.render(state.wishlistState) + val buyActionButtonColor = getBuyActionColor(state.promoCodeState) + val (strokeColor, textColor) = getWishlistActionColor(state) + + coursePurchaseBinding.coursePurchaseBuyAction.setBackgroundColor(ContextCompat.getColor(requireContext(), buyActionButtonColor)) + coursePurchaseBinding.coursePurchaseWishlistAction.strokeColor = AppCompatResources.getColorStateList(requireContext(), strokeColor) + coursePurchaseBinding.coursePurchaseWishlistAction.setTextColor(AppCompatResources.getColorStateList(requireContext(), textColor)) + } + } + + private fun getBuyActionColor(promoCodeState: CoursePurchaseFeature.PromoCodeState): Int = + if (promoCodeState is CoursePurchaseFeature.PromoCodeState.Valid) { + R.color.color_overlay_green + } else { + R.color.color_overlay_violet + } + + private fun getWishlistActionColor(state: CoursePurchaseFeature.State.Content): Pair = + if (state.promoCodeState is CoursePurchaseFeature.PromoCodeState.Valid) { + if (state.wishlistState is CoursePurchaseFeature.WishlistState.Wishlisted) { + R.color.color_overlay_green_alpha_12 to R.color.color_overlay_green + } else { + R.color.color_overlay_green to R.color.color_overlay_green + } + } else { + if (state.wishlistState is CoursePurchaseFeature.WishlistState.Wishlisted) { + R.color.color_overlay_violet_alpha_12 to R.color.color_overlay_violet + } else { + R.color.color_overlay_violet to R.color.color_overlay_violet + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course_revenue/ui/delegate/CourseBenefitSummaryViewDelegate.kt b/app/src/main/java/org/stepik/android/view/course_revenue/ui/delegate/CourseBenefitSummaryViewDelegate.kt index cdfd00cd91..c8b59bc98a 100644 --- a/app/src/main/java/org/stepik/android/view/course_revenue/ui/delegate/CourseBenefitSummaryViewDelegate.kt +++ b/app/src/main/java/org/stepik/android/view/course_revenue/ui/delegate/CourseBenefitSummaryViewDelegate.kt @@ -58,6 +58,7 @@ class CourseBenefitSummaryViewDelegate( courseBenefitExperimentDisclaimer.text = buildSpannedString { bold { append(context.getString(R.string.course_benefits_contact_support_part_1)) } append(context.getString(R.string.course_benefits_contact_support_part_2)) + color(ContextCompat.getColor(context, R.color.color_overlay_violet)) { append(context.getString(R.string.course_benefits_contact_support_part_3)) } diff --git a/app/src/main/java/org/stepik/android/view/injection/course/CourseHeaderDelegateFactory.kt b/app/src/main/java/org/stepik/android/view/injection/course/CourseHeaderDelegateFactory.kt index 0fcc35c979..2db61bdf8d 100644 --- a/app/src/main/java/org/stepik/android/view/injection/course/CourseHeaderDelegateFactory.kt +++ b/app/src/main/java/org/stepik/android/view/injection/course/CourseHeaderDelegateFactory.kt @@ -4,6 +4,7 @@ import android.app.Activity import dagger.assisted.AssistedFactory import org.stepik.android.domain.course.analytic.CourseViewSource import org.stepik.android.presentation.course.CoursePresenter +import org.stepik.android.presentation.course_purchase.model.CoursePurchaseData import org.stepik.android.view.course.ui.delegates.CourseHeaderDelegate @AssistedFactory @@ -17,6 +18,8 @@ interface CourseHeaderDelegateFactory { showCourseRevenueAction: () -> Unit, onSubmissionCountClicked: () -> Unit, isLocalSubmissionsEnabled: Boolean, - showCourseSearchAction: () -> Unit + showCourseSearchAction: () -> Unit, + coursePurchaseFlowAction: (CoursePurchaseData) -> Unit, + currentPurchaseFlow: String ): CourseHeaderDelegate } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/injection/course_purchase/CoursePurchaseComponent.kt b/app/src/main/java/org/stepik/android/view/injection/course_purchase/CoursePurchaseComponent.kt new file mode 100644 index 0000000000..e44283f8f0 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/injection/course_purchase/CoursePurchaseComponent.kt @@ -0,0 +1,18 @@ +package org.stepik.android.view.injection.course_purchase + +import dagger.Subcomponent +import org.stepik.android.view.course_purchase.ui.dialog.CoursePurchaseBottomSheetDialogFragment +import org.stepik.android.view.injection.wishlist.WishlistDataModule + +@Subcomponent(modules = [ + CoursePurchasePresentationModule::class, + WishlistDataModule::class +]) +interface CoursePurchaseComponent { + @Subcomponent.Builder + interface Builder { + fun build(): CoursePurchaseComponent + } + + fun inject(coursePurchaseBottomSheetDialogFragment: CoursePurchaseBottomSheetDialogFragment) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/injection/course_purchase/CoursePurchasePresentationModule.kt b/app/src/main/java/org/stepik/android/view/injection/course_purchase/CoursePurchasePresentationModule.kt new file mode 100644 index 0000000000..397601e0f7 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/injection/course_purchase/CoursePurchasePresentationModule.kt @@ -0,0 +1,30 @@ +package org.stepik.android.view.injection.course_purchase + +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.course_purchase.CoursePurchaseFeature +import org.stepik.android.presentation.course_purchase.CoursePurchaseViewModel +import org.stepik.android.presentation.course_purchase.dispatcher.CoursePurchaseActionDispatcher +import org.stepik.android.presentation.course_purchase.reducer.CoursePurchaseReducer +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 CoursePurchasePresentationModule { + @Provides + @IntoMap + @ViewModelKey(CoursePurchaseViewModel::class) + internal fun provideCoursePurchasePresenter( + coursePurchaseReducer: CoursePurchaseReducer, + coursePurchaseActionDispatcher: CoursePurchaseActionDispatcher + ): ViewModel = + CoursePurchaseViewModel( + ReduxFeature(CoursePurchaseFeature.State.Idle, coursePurchaseReducer) + .wrapWithActionDispatcher(coursePurchaseActionDispatcher) + .wrapWithViewContainer() + ) +} \ No newline at end of file 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 b8795920ef..9268ef98e6 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 @@ -62,7 +62,7 @@ class LessonDemoCompleteBottomSheetDialogFragment : BottomSheetDialogFragment() demoCompleteTitle.text = getString(R.string.demo_complete_title, course.title) val courseDisplayPrice = course.displayPrice - val (currencyCode, promoPrice, hasPromo) = coursePromoCodeResolver.resolvePromoCodeInfo( + val (_, currencyCode, promoPrice, hasPromo) = coursePromoCodeResolver.resolvePromoCodeInfo( DeeplinkPromoCode.EMPTY, // TODO Deeplink promo code will be passed as a parameter to newInstance DefaultPromoCode( course.defaultPromoCodeName ?: "", diff --git a/app/src/main/java/org/stepik/android/view/step/ui/fragment/StepFragment.kt b/app/src/main/java/org/stepik/android/view/step/ui/fragment/StepFragment.kt index 77ade0e163..eda06d77fb 100644 --- a/app/src/main/java/org/stepik/android/view/step/ui/fragment/StepFragment.kt +++ b/app/src/main/java/org/stepik/android/view/step/ui/fragment/StepFragment.kt @@ -29,7 +29,6 @@ import org.stepic.droid.analytic.AmplitudeAnalytic import org.stepic.droid.analytic.Analytic import org.stepic.droid.base.App import org.stepic.droid.configuration.EndpointResolver -import org.stepic.droid.configuration.RemoteConfig import org.stepic.droid.core.ScreenManager import org.stepic.droid.persistence.model.StepPersistentWrapper import org.stepic.droid.ui.dialogs.LoadingProgressDialogFragment @@ -376,8 +375,7 @@ class StepFragment : Fragment(R.layout.fragment_step), StepView, val isNeedReloadQuiz = stepWrapper.step.block != state.stepWrapper.step.block || stepWrapper.step.isEnabled != state.stepWrapper.step.isEnabled - val isStepDisabled = remoteConfig.getBoolean(RemoteConfig.IS_DISABLED_STEPS_SUPPORTED) && - state.stepWrapper.step.isEnabled == false + val isStepDisabled = state.stepWrapper.step.isEnabled == false val isStepUnavailable = isStepDisabled && !lessonData.lesson.isTeacher @@ -410,8 +408,7 @@ class StepFragment : Fragment(R.layout.fragment_step), StepView, } private fun isStepContentNextVisible(stepWrapper: StepPersistentWrapper, lessonData: LessonData): Boolean { - val isStepDisabled = remoteConfig.getBoolean(RemoteConfig.IS_DISABLED_STEPS_SUPPORTED) && - stepWrapper.step.isEnabled == false + val isStepDisabled = stepWrapper.step.isEnabled == false val isStepNotLast = stepWrapper.step.position < lessonData.lesson.steps.size diff --git a/app/src/main/java/org/stepik/android/view/step_quiz/ui/factory/StepQuizFragmentFactoryImpl.kt b/app/src/main/java/org/stepik/android/view/step_quiz/ui/factory/StepQuizFragmentFactoryImpl.kt index c1ebc71bd7..94f0c3d595 100644 --- a/app/src/main/java/org/stepik/android/view/step_quiz/ui/factory/StepQuizFragmentFactoryImpl.kt +++ b/app/src/main/java/org/stepik/android/view/step_quiz/ui/factory/StepQuizFragmentFactoryImpl.kt @@ -1,8 +1,6 @@ package org.stepik.android.view.step_quiz.ui.factory import androidx.fragment.app.Fragment -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import org.stepic.droid.configuration.RemoteConfig import org.stepic.droid.persistence.model.StepPersistentWrapper import org.stepic.droid.util.AppConstants import org.stepik.android.domain.lesson.model.LessonData @@ -22,16 +20,14 @@ import javax.inject.Inject class StepQuizFragmentFactoryImpl @Inject -constructor( - private val firebaseRemoteConfig: FirebaseRemoteConfig -) : StepQuizFragmentFactory { +constructor() : StepQuizFragmentFactory { override fun createStepQuizFragment(stepPersistentWrapper: StepPersistentWrapper, lessonData: LessonData): Fragment { val instructionType = stepPersistentWrapper.step.instructionType.takeIf { stepPersistentWrapper.step.actions?.doReview != null } val blockName = stepPersistentWrapper.step.block?.name - return if (instructionType != null && firebaseRemoteConfig.getBoolean(RemoteConfig.IS_PEER_REVIEW_ENABLED)) { + return if (instructionType != null) { when { lessonData.lesson.isTeacher && blockName in StepQuizReviewTeacherFragment.supportedQuizTypes -> StepQuizReviewTeacherFragment.newInstance(stepPersistentWrapper.step.id, instructionType) diff --git a/app/src/main/res/drawable/bg_course_purchase_promo_code_submit.xml b/app/src/main/res/drawable/bg_course_purchase_promo_code_submit.xml new file mode 100644 index 0000000000..a7e8f098e7 --- /dev/null +++ b/app/src/main/res/drawable/bg_course_purchase_promo_code_submit.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000000..eb6575272b --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/bottom_sheet_dialog_course_purchase.xml b/app/src/main/res/layout/bottom_sheet_dialog_course_purchase.xml new file mode 100644 index 0000000000..21e4f54c44 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_dialog_course_purchase.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 9f69bfa716..4b644e9c8e 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -1032,4 +1032,20 @@ Па дадзеным запыце\nнічога не знойдзена " — Крок %d" Пошук па курсе %s + + + Карыстацкае пагадненне + У кошт уключана 15% камісія Google Play.\nАплачваючы доступ да гэтага курсу вы згаджаецеся з умовамі + карыстацкага пагаднення + https://welcome.stepik.org/ru/payment-terms + + Увядзіце промокод + У мяне ёсць промокод + Праверка промокода + Няправільны промокод + Промокод ужыты + + Дадаць у Спіс жаданняў + Дадаем + Курс дададзены ў Спіс жаданняў diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a3ea48ba03..c44a7f4446 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1169,4 +1169,20 @@ По данному запросу\nничего не найдено " — Шаг %d" Поиск по курсу %s + + + Пользовательское соглашение + "В цену включена 15% комиссия Google Play.\nОплачивая доступ к этому курсу вы соглашаетесь с условиями " + пользовательского соглашения + https://welcome.stepik.org/ru/payment-terms + + Введите промокод + У меня есть промокод + Проверка промокода + Неверный промокод + Промокод применен + + Добавить в Список желаний + Добавляем + Курс добавлен в Список желаний diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4131aec02c..0e948807f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1240,4 +1240,21 @@ No results were\nfound for this request. " — Step %d" Search in course %s + + + + User Agreement + The price includes 15% commission from Google Play.\nBy paying for access to this course you agree to the terms + user agreement + https://welcome.stepik.org/en/payment-terms + + Enter promo code + I have a promo code + Checking promo code + Invalid promo code + Promo code applied + + Add to Wishlist + Adding + Course added to Wishlist diff --git a/app/src/main/res/xml/remote_config_defaults.xml b/app/src/main/res/xml/remote_config_defaults.xml index 579753b126..ee1c06dd92 100644 --- a/app/src/main/res/xml/remote_config_defaults.xml +++ b/app/src/main/res/xml/remote_config_defaults.xml @@ -1,9 +1,5 @@ - - continue_course_experiment_enabled - false - min_delay_rate_dialog_sec 86400 @@ -12,10 +8,6 @@ show_streak_dialog_after_login true - - show_notifications_badges - false - adaptive_courses_android @@ -28,14 +20,6 @@ is_local_submissions_enabled false - - is_peer_review_enabled - false - - - is_disabled_steps_supported - false - search_query_params_android {"is_popular": true, "is_public": true} @@ -52,4 +36,8 @@ is_course_revenue_available_android false + + purchase_flow_android + web + \ 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 96370411d8..24ed05b1be 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 @@ -37,7 +37,7 @@ class CourseHeaderDataTest { isWishlisted = false ), localSubmissionsCount = 5, - deeplinkPromoCode = DeeplinkPromoCode("200", "RUB"), + deeplinkPromoCode = DeeplinkPromoCode("CODE", "200", "RUB"), defaultPromoCode = DefaultPromoCode.EMPTY, isWishlistUpdating = false, wishlistEntity = WishlistEntity(-1, emptyList()) diff --git a/dependencies.gradle b/dependencies.gradle index b1cdd560a1..c7b4ef36b8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ ext.versions = [ - code : 2226, - name : '1.194', + code : 2227, + name : '1.195', minSdk : 21, targetSdk : 30,