diff --git a/app/src/main/java/org/openedx/app/AnalyticsManager.kt b/app/src/main/java/org/openedx/app/AnalyticsManager.kt index 4e0f42a91..cef46916c 100644 --- a/app/src/main/java/org/openedx/app/AnalyticsManager.kt +++ b/app/src/main/java/org/openedx/app/AnalyticsManager.kt @@ -9,11 +9,7 @@ import org.openedx.auth.presentation.AuthAnalytics import org.openedx.core.config.Config import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.presentation.IAPAnalytics -import org.openedx.core.presentation.IAPAnalyticsEvent -import org.openedx.core.presentation.IAPAnalyticsKeys -import org.openedx.core.presentation.IAPAnalyticsScreen import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics -import org.openedx.core.presentation.iap.IAPFlow import org.openedx.course.presentation.CourseAnalytics import org.openedx.dashboard.presentation.DashboardAnalytics import org.openedx.discovery.presentation.DiscoveryAnalytics @@ -195,29 +191,6 @@ class AnalyticsManager( put(Key.TOPIC_NAME.keyName, topicName) }) } - - override fun logIAPEvent( - event: IAPAnalyticsEvent, - params: MutableMap, - screenName: String - ) { - logEvent( - event = event.eventName, - params = params.apply { - put(IAPAnalyticsKeys.NAME.key, event.biValue) - put(IAPAnalyticsKeys.SCREEN_NAME.key, screenName) - put( - IAPAnalyticsKeys.IAP_FLOW_TYPE.key, - if (screenName == IAPAnalyticsScreen.PROFILE.screenName) { - IAPFlow.RESTORE.value - } else { - IAPFlow.SILENT.value - } - ) - put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key) - } - ) - } } enum class Event(val eventName: String) { diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index a0ecfc5e0..71bde6fba 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -16,7 +16,6 @@ import org.openedx.core.data.repository.iap.IAPRepository import org.openedx.core.domain.interactor.IAPInteractor import org.openedx.core.domain.model.iap.PurchaseFlowData import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectDialogViewModel -import org.openedx.core.presentation.iap.IAPFlow import org.openedx.core.presentation.iap.IAPViewModel import org.openedx.core.presentation.settings.video.VideoQualityViewModel import org.openedx.core.ui.WindowSize @@ -451,9 +450,8 @@ val screenModule = module { single { IAPRepository(get()) } factory { IAPInteractor(get(), get(), get(), get(), get()) } - viewModel { (iapFlow: IAPFlow, purchaseFlowData: PurchaseFlowData) -> + viewModel { (purchaseFlowData: PurchaseFlowData) -> IAPViewModel( - iapFlow = iapFlow, purchaseFlowData = purchaseFlowData, get(), get(), diff --git a/core/src/main/java/org/openedx/core/domain/model/iap/PurchaseFlowData.kt b/core/src/main/java/org/openedx/core/domain/model/iap/PurchaseFlowData.kt index 1102e2348..48233d299 100644 --- a/core/src/main/java/org/openedx/core/domain/model/iap/PurchaseFlowData.kt +++ b/core/src/main/java/org/openedx/core/domain/model/iap/PurchaseFlowData.kt @@ -5,6 +5,7 @@ import kotlinx.parcelize.Parcelize @Parcelize data class PurchaseFlowData( + var iapFlow: IAPFlow? = null, var screenName: String? = null, var courseId: String? = null, var courseName: String? = null, @@ -22,6 +23,7 @@ data class PurchaseFlowData( var flowStartTime: Long = 0 fun reset() { + iapFlow = null screenName = null courseId = null courseName = null @@ -36,3 +38,19 @@ data class PurchaseFlowData( flowStartTime = 0 } } + +enum class IAPFlow(val value: String) { + RESTORE("restore"), + SILENT("silent"), + USER_INITIATED("user_initiated"); + + fun value(): String { + return this.name.lowercase() + } +} + +enum class IAPFlowSource(val screen: String) { + COURSE_ENROLLMENT("course_enrollment"), + COURSE_DASHBOARD("course_dashboard"), + PROFILE("profile"), +} diff --git a/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt b/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt index 9f683587a..c8b346300 100644 --- a/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt +++ b/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt @@ -1,11 +1,7 @@ package org.openedx.core.presentation interface IAPAnalytics { - fun logIAPEvent( - event: IAPAnalyticsEvent, - params: MutableMap = mutableMapOf(), - screenName: String, - ) + fun logEvent(event: String, params: Map) fun logScreenEvent(screenName: String, params: Map) } @@ -74,9 +70,3 @@ enum class IAPAnalyticsKeys(val key: String) { SCREEN_NAME("screen_name"), ERROR_ALERT_TYPE("error_alert_type"), } - -enum class IAPAnalyticsScreen(val screenName: String) { - COURSE_ENROLLMENT("course_enrollment"), - COURSE_DASHBOARD("course_dashboard"), - PROFILE("profile"), -} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/IAPDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/IAPDialogFragment.kt index 2e4955e42..a136b8cba 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/IAPDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/IAPDialogFragment.kt @@ -32,12 +32,11 @@ import androidx.fragment.app.DialogFragment import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.R +import org.openedx.core.domain.model.iap.IAPFlow import org.openedx.core.domain.model.iap.ProductInfo import org.openedx.core.domain.model.iap.PurchaseFlowData import org.openedx.core.extension.parcelable -import org.openedx.core.extension.serializable import org.openedx.core.presentation.iap.IAPAction -import org.openedx.core.presentation.iap.IAPFlow import org.openedx.core.presentation.iap.IAPLoaderType import org.openedx.core.presentation.iap.IAPRequestType import org.openedx.core.presentation.iap.IAPUIState @@ -54,7 +53,6 @@ class IAPDialogFragment : DialogFragment() { private val iapViewModel by viewModel { parametersOf( - requireArguments().serializable(ARG_IAP_FLOW), requireArguments().parcelable(ARG_PURCHASE_FLOW_DATA) ) } @@ -236,7 +234,6 @@ class IAPDialogFragment : DialogFragment() { companion object { const val TAG = "IAPDialogFragment" - private const val ARG_IAP_FLOW = "iap_flow" private const val ARG_PURCHASE_FLOW_DATA = "purchase_flow_data" fun newInstance( @@ -246,10 +243,11 @@ class IAPDialogFragment : DialogFragment() { courseName: String = "", isSelfPaced: Boolean = false, componentId: String? = null, - productInfo: ProductInfo? = null + productInfo: ProductInfo? = null, ): IAPDialogFragment { val fragment = IAPDialogFragment() val purchaseFlowData = PurchaseFlowData().apply { + this.iapFlow = iapFlow this.screenName = screenName this.courseId = courseId this.courseName = courseName @@ -259,7 +257,6 @@ class IAPDialogFragment : DialogFragment() { } fragment.arguments = bundleOf( - ARG_IAP_FLOW to iapFlow, ARG_PURCHASE_FLOW_DATA to purchaseFlowData ) return fragment diff --git a/core/src/main/java/org/openedx/core/presentation/iap/IAPEventLogger.kt b/core/src/main/java/org/openedx/core/presentation/iap/IAPEventLogger.kt index 6dfa06eff..0d5e0c16f 100644 --- a/core/src/main/java/org/openedx/core/presentation/iap/IAPEventLogger.kt +++ b/core/src/main/java/org/openedx/core/presentation/iap/IAPEventLogger.kt @@ -1,8 +1,11 @@ package org.openedx.core.presentation.iap import com.android.billingclient.api.BillingClient +import org.openedx.core.domain.model.iap.IAPFlow import org.openedx.core.domain.model.iap.PurchaseFlowData import org.openedx.core.exception.iap.IAPException +import org.openedx.core.extension.isNull +import org.openedx.core.extension.isTrue import org.openedx.core.extension.nonZero import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.IAPAnalytics @@ -12,23 +15,24 @@ import org.openedx.core.utils.TimeUtils class IAPEventLogger( private val analytics: IAPAnalytics, - private val purchaseFlowData: PurchaseFlowData, + private val purchaseFlowData: PurchaseFlowData? = null, + private val isSilentIAPFlow: Boolean? = null, ) { fun upgradeNowClickedEvent() { logIAPEvent(IAPAnalyticsEvent.IAP_UPGRADE_NOW_CLICKED) } fun upgradeSuccessEvent() { - val elapsedTime = TimeUtils.getCurrentTime() - purchaseFlowData.flowStartTime + val elapsedTime = TimeUtils.getCurrentTime() - (purchaseFlowData?.flowStartTime ?: 0L) logIAPEvent(IAPAnalyticsEvent.IAP_COURSE_UPGRADE_SUCCESS, buildMap { put(IAPAnalyticsKeys.ELAPSED_TIME.key, elapsedTime) - }.toMutableMap()) + }) } private fun purchaseErrorEvent(error: String) { logIAPEvent(IAPAnalyticsEvent.IAP_PAYMENT_ERROR, buildMap { put(IAPAnalyticsKeys.ERROR.key, error) - }.toMutableMap()) + }) } private fun canceledByUserEvent() { @@ -38,13 +42,13 @@ class IAPEventLogger( private fun courseUpgradeErrorEvent(error: String) { logIAPEvent(IAPAnalyticsEvent.IAP_COURSE_UPGRADE_ERROR, buildMap { put(IAPAnalyticsKeys.ERROR.key, error) - }.toMutableMap()) + }) } private fun priceLoadErrorEvent(error: String) { logIAPEvent(IAPAnalyticsEvent.IAP_PRICE_LOAD_ERROR, buildMap { put(IAPAnalyticsKeys.ERROR.key, error) - }.toMutableMap()) + }) } fun logExceptionEvent(iapException: IAPException) { @@ -74,14 +78,48 @@ class IAPEventLogger( logIAPEvent(IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, buildMap { put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, alertType) put(IAPAnalyticsKeys.ERROR_ACTION.key, action) - }.toMutableMap()) + }) + } + + fun logRestorePurchasesClickedEvent() { + logIAPEvent(IAPAnalyticsEvent.IAP_RESTORE_PURCHASE_CLICKED) + } + + fun logUnfulfilledPurchaseInitiatedEvent() { + logIAPEvent(IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED) + } + + fun logGetHelpEvent() { + logIAPEvent( + event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, + params = buildMap { + put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) + put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_GET_HELP.action) + }) + } + + fun logIAPCancelEvent() { + logIAPEvent( + event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, + params = buildMap { + put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) + put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_CLOSE.action) + }) + } + + fun onRestorePurchaseCancel() { + logIAPEvent( + event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, + params = buildMap { + put(IAPAnalyticsKeys.ACTION.key, IAPAction.ACTION_CLOSE.action) + }) } fun loadIAPScreenEvent() { val event = IAPAnalyticsEvent.IAP_VALUE_PROP_VIEWED val params = buildMap { put(IAPAnalyticsKeys.NAME.key, event.biValue) - purchaseFlowData.screenName?.takeIfNotEmpty()?.let { screenName -> + purchaseFlowData?.screenName?.takeIfNotEmpty()?.let { screenName -> put(IAPAnalyticsKeys.SCREEN_NAME.key, screenName) } putAll(getIAPEventParams()) @@ -89,43 +127,66 @@ class IAPEventLogger( analytics.logScreenEvent(screenName = event.eventName, params = params) } - private fun getIAPEventParams(): MutableMap { + private fun getIAPEventParams(): Map { + if (purchaseFlowData.isNull() || purchaseFlowData?.courseId.isNullOrEmpty()) return emptyMap() + return buildMap { - purchaseFlowData.takeIf { it.courseId.isNullOrBlank().not() }?.let { - put(IAPAnalyticsKeys.COURSE_ID.key, purchaseFlowData.courseId) + purchaseFlowData?.apply { + put(IAPAnalyticsKeys.COURSE_ID.key, courseId) put( IAPAnalyticsKeys.PACING.key, - if (purchaseFlowData.isSelfPaced == true) IAPAnalyticsKeys.SELF.key else IAPAnalyticsKeys.INSTRUCTOR.key + if (isSelfPaced.isTrue()) IAPAnalyticsKeys.SELF.key else IAPAnalyticsKeys.INSTRUCTOR.key ) + productInfo?.lmsUSDPrice?.nonZero()?.let { lmsUSDPrice -> + put(IAPAnalyticsKeys.LMS_USD_PRICE.key, lmsUSDPrice) + } + price.nonZero()?.let { localizedPrice -> + put(IAPAnalyticsKeys.LOCALIZED_PRICE.key, localizedPrice) + } + currencyCode.takeIfNotEmpty()?.let { currencyCode -> + put(IAPAnalyticsKeys.CURRENCY_CODE.key, currencyCode) + } + componentId?.takeIfNotEmpty()?.let { componentId -> + put(IAPAnalyticsKeys.COMPONENT_ID.key, componentId) + } + iapFlow?.let { iapFlow -> + put(IAPAnalyticsKeys.IAP_FLOW_TYPE.key, iapFlow.value) + } + put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key) + screenName?.takeIfNotEmpty()?.let { screenName -> + put(IAPAnalyticsKeys.SCREEN_NAME.key, screenName) + } } - purchaseFlowData.productInfo?.lmsUSDPrice?.nonZero()?.let { lmsUSDPrice -> - put(IAPAnalyticsKeys.LMS_USD_PRICE.key, lmsUSDPrice) - } - purchaseFlowData.price.nonZero()?.let { localizedPrice -> - put(IAPAnalyticsKeys.LOCALIZED_PRICE.key, localizedPrice) - } - purchaseFlowData.currencyCode.takeIfNotEmpty()?.let { currencyCode -> - put(IAPAnalyticsKeys.CURRENCY_CODE.key, currencyCode) - } - purchaseFlowData.componentId?.takeIf { it.isNotBlank() }?.let { componentId -> - put(IAPAnalyticsKeys.COMPONENT_ID.key, componentId) - } + } + } + + private fun getUnfulfilledIAPEventParams(): Map { + if (isSilentIAPFlow.isNull()) return emptyMap() + + return buildMap { put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key) - }.toMutableMap() + purchaseFlowData?.screenName?.takeIfNotEmpty()?.let { screenName -> + put(IAPAnalyticsKeys.SCREEN_NAME.key, screenName) + } + put( + IAPAnalyticsKeys.IAP_FLOW_TYPE.key, + if (isSilentIAPFlow.isTrue()) IAPFlow.SILENT.value else IAPFlow.RESTORE.value + ) + } } private fun logIAPEvent( event: IAPAnalyticsEvent, - params: MutableMap = mutableMapOf(), + params: Map = mutableMapOf(), ) { - params.apply { - put(IAPAnalyticsKeys.NAME.key, event.biValue) - putAll(getIAPEventParams()) - } - analytics.logIAPEvent( - event = event, - params = params, - screenName = purchaseFlowData.screenName.orEmpty() + analytics.logEvent( + event = event.eventName, + params = buildMap { + put(IAPAnalyticsKeys.NAME.key, event.biValue) + putAll(params) + putAll(getIAPEventParams()) + putAll(getUnfulfilledIAPEventParams()) + }, ) } } diff --git a/core/src/main/java/org/openedx/core/presentation/iap/IAPUIState.kt b/core/src/main/java/org/openedx/core/presentation/iap/IAPUIState.kt index 95bced19f..73e0ebf6d 100644 --- a/core/src/main/java/org/openedx/core/presentation/iap/IAPUIState.kt +++ b/core/src/main/java/org/openedx/core/presentation/iap/IAPUIState.kt @@ -17,16 +17,6 @@ enum class IAPLoaderType { PRICE, PURCHASE_FLOW, FULL_SCREEN, RESTORE_PURCHASES } -enum class IAPFlow(val value: String) { - RESTORE("restore"), - SILENT("silent"), - USER_INITIATED("user_initiated"); - - fun value(): String { - return this.name.lowercase() - } -} - enum class IAPAction(val action: String) { ACTION_USER_INITIATED("user_initiated"), ACTION_GET_HELP("get_help"), diff --git a/core/src/main/java/org/openedx/core/presentation/iap/IAPViewModel.kt b/core/src/main/java/org/openedx/core/presentation/iap/IAPViewModel.kt index 56190d82c..981e6b107 100644 --- a/core/src/main/java/org/openedx/core/presentation/iap/IAPViewModel.kt +++ b/core/src/main/java/org/openedx/core/presentation/iap/IAPViewModel.kt @@ -20,6 +20,8 @@ import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.domain.interactor.IAPInteractor +import org.openedx.core.domain.model.iap.IAPFlow +import org.openedx.core.domain.model.iap.IAPFlowSource import org.openedx.core.domain.model.iap.PurchaseFlowData import org.openedx.core.exception.iap.IAPException import org.openedx.core.module.billing.BillingProcessor @@ -33,7 +35,6 @@ import org.openedx.core.system.notifier.UpdateCourseData import org.openedx.core.utils.TimeUtils class IAPViewModel( - iapFlow: IAPFlow, private val purchaseFlowData: PurchaseFlowData, private val iapInteractor: IAPInteractor, private val analytics: IAPAnalytics, @@ -91,7 +92,7 @@ class IAPViewModel( }.distinctUntilChanged().launchIn(viewModelScope) } - when (iapFlow) { + when (purchaseFlowData.iapFlow) { IAPFlow.USER_INITIATED -> { eventLogger.loadIAPScreenEvent() loadPrice() @@ -102,6 +103,8 @@ class IAPViewModel( purchaseFlowData.flowStartTime = TimeUtils.getCurrentTime() updateCourseData() } + + else -> {} } } @@ -237,8 +240,8 @@ class IAPViewModel( private fun updateCourseData() { viewModelScope.launch(Dispatchers.IO) { - purchaseFlowData.courseId?.let { courseId -> - iapNotifier.send(UpdateCourseData(courseId)) + purchaseFlowData.courseId?.let { + iapNotifier.send(UpdateCourseData(IAPFlowSource.COURSE_DASHBOARD.screen == purchaseData.screenName)) } } } diff --git a/core/src/main/java/org/openedx/core/system/notifier/UpdateCourseData.kt b/core/src/main/java/org/openedx/core/system/notifier/UpdateCourseData.kt index 3ea3a19b1..533798939 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/UpdateCourseData.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/UpdateCourseData.kt @@ -1,3 +1,6 @@ package org.openedx.core.system.notifier -data class UpdateCourseData(val courseId: String) : IAPEvent +data class UpdateCourseData( + val isPurchasedFromCourseDashboard: Boolean = false, + val isExpiredCoursePurchase: Boolean = false, +) : IAPEvent diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index 258b2392b..829781ac2 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -71,13 +71,13 @@ import org.koin.androidx.compose.koinViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.domain.model.CourseAccessError +import org.openedx.core.domain.model.iap.IAPFlow +import org.openedx.core.domain.model.iap.IAPFlowSource import org.openedx.core.extension.isTrue import org.openedx.core.extension.takeIfNotEmpty -import org.openedx.core.presentation.IAPAnalyticsScreen import org.openedx.core.presentation.dialog.IAPDialogFragment import org.openedx.core.presentation.global.viewBinding import org.openedx.core.presentation.iap.IAPAction -import org.openedx.core.presentation.iap.IAPFlow import org.openedx.core.presentation.iap.IAPRequestType import org.openedx.core.presentation.iap.IAPUIState import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialog @@ -425,7 +425,7 @@ fun CourseDashboard( ) { IAPDialogFragment.newInstance( iapFlow = IAPFlow.USER_INITIATED, - screenName = IAPAnalyticsScreen.COURSE_DASHBOARD.screenName, + screenName = IAPFlowSource.COURSE_DASHBOARD.screen, courseId = viewModel.courseId, courseName = viewModel.courseName, isSelfPaced = viewModel.courseDetails?.courseInfoOverview?.isSelfPaced.isTrue(), @@ -934,7 +934,7 @@ private fun SetupCourseAccessErrorButtons( ) { IAPDialogFragment.newInstance( iapFlow = IAPFlow.USER_INITIATED, - screenName = IAPAnalyticsScreen.COURSE_DASHBOARD.screenName, + screenName = IAPFlowSource.COURSE_DASHBOARD.screen, courseId = viewModel.courseId, courseName = viewModel.courseName, isSelfPaced = viewModel.courseDetails?.courseInfoOverview?.isSelfPaced.isTrue(), diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index 4779f7f65..1f212af6c 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.ImageProcessor -import org.openedx.core.R import org.openedx.core.SingleEventLiveData import org.openedx.core.UIMessage import org.openedx.core.config.Config @@ -34,6 +33,8 @@ import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.interactor.IAPInteractor import org.openedx.core.domain.model.CourseAccessError import org.openedx.core.domain.model.CourseEnrollmentDetails +import org.openedx.core.domain.model.iap.IAPFlow +import org.openedx.core.domain.model.iap.IAPFlowSource import org.openedx.core.domain.model.iap.PurchaseFlowData import org.openedx.core.exception.iap.IAPException import org.openedx.core.extension.isFalse @@ -43,7 +44,6 @@ import org.openedx.core.module.billing.BillingProcessor import org.openedx.core.module.billing.getCourseSku import org.openedx.core.module.billing.getPriceAmount import org.openedx.core.presentation.IAPAnalytics -import org.openedx.core.presentation.IAPAnalyticsScreen import org.openedx.core.presentation.iap.IAPAction import org.openedx.core.presentation.iap.IAPEventLogger import org.openedx.core.presentation.iap.IAPLoaderType @@ -57,6 +57,7 @@ import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent import org.openedx.core.system.notifier.CourseCompletionSet +import org.openedx.core.system.notifier.CourseDataUpdated import org.openedx.core.system.notifier.CourseDatesShifted import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier @@ -225,13 +226,16 @@ class CourseContainerViewModel( iapNotifier.notifier.onEach { event -> when (event) { is UpdateCourseData -> { - fetchCourseDetails(true) + fetchCourseDetails( + isIAPFlow = event.isPurchasedFromCourseDashboard, + isExpiredCoursePurchase = event.isExpiredCoursePurchase + ) } } }.distinctUntilChanged().launchIn(viewModelScope) } - fun fetchCourseDetails(isIAPFlow: Boolean = false) { + fun fetchCourseDetails(isIAPFlow: Boolean = false, isExpiredCoursePurchase: Boolean = false) { if (isIAPFlow.not()) { courseDashboardViewed() } @@ -255,7 +259,8 @@ class CourseContainerViewModel( courseName = courseInfoOverview.name isSelfPaced = courseInfoOverview.isSelfPaced productInfo = courseInfoOverview.productInfo - screenName = IAPAnalyticsScreen.COURSE_DASHBOARD.screenName + screenName = IAPFlowSource.COURSE_DASHBOARD.screen + iapFlow = IAPFlow.USER_INITIATED } loadPrice() _courseAccessStatus.value = @@ -279,12 +284,20 @@ class CourseContainerViewModel( delay(500L) courseNotifier.send(CourseOpenBlock(resumeBlockId)) } - _dataReady.value = true if (isIAPFlow) { - eventLogger.upgradeSuccessEvent() - _uiMessage.emit(UIMessage.ToastMessage(resourceManager.getString(R.string.iap_success_message))) + if (isExpiredCoursePurchase) { + eventLogger.upgradeSuccessEvent() + _uiMessage.emit( + UIMessage.ToastMessage( + resourceManager.getString(CoreR.string.iap_success_message) + ) + ) + } else { + iapNotifier.send(CourseDataUpdated()) + } _iapState.value = IAPUIState.CourseDataUpdated } + _dataReady.value = true } } ?: run { _courseAccessStatus.value = CourseAccessError.UNKNOWN @@ -444,8 +457,13 @@ class CourseContainerViewModel( private fun updateCourseData() { viewModelScope.launch(Dispatchers.IO) { - purchaseFlowData.courseId?.let { courseId -> - iapNotifier.send(UpdateCourseData(courseId)) + purchaseFlowData.courseId?.let { + iapNotifier.send( + UpdateCourseData( + isPurchasedFromCourseDashboard = true, + isExpiredCoursePurchase = true + ) + ) } } } diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index 7a56f7ce4..a55162106 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -270,7 +270,7 @@ class CourseContainerViewModelTest { ) } returns Unit every { - analytics.logScreenEvent( + courseAnalytics.logScreenEvent( CourseAnalyticsEvent.HOME_TAB.eventName, any() ) @@ -286,7 +286,7 @@ class CourseContainerViewModelTest { ) } verify(exactly = 1) { - analytics.logScreenEvent( + courseAnalytics.logScreenEvent( CourseAnalyticsEvent.HOME_TAB.eventName, any() ) @@ -325,7 +325,7 @@ class CourseContainerViewModelTest { ) } returns Unit every { - analytics.logScreenEvent( + courseAnalytics.logScreenEvent( CourseAnalyticsEvent.HOME_TAB.eventName, any() ) @@ -341,7 +341,7 @@ class CourseContainerViewModelTest { ) } verify(exactly = 1) { - analytics.logScreenEvent( + courseAnalytics.logScreenEvent( CourseAnalyticsEvent.HOME_TAB.eventName, any() ) @@ -381,7 +381,7 @@ class CourseContainerViewModelTest { ) } returns Unit every { - analytics.logScreenEvent( + courseAnalytics.logScreenEvent( CourseAnalyticsEvent.HOME_TAB.eventName, any() ) @@ -396,7 +396,7 @@ class CourseContainerViewModelTest { ) } verify(exactly = 1) { - analytics.logScreenEvent( + courseAnalytics.logScreenEvent( CourseAnalyticsEvent.HOME_TAB.eventName, any() ) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt index b0c5a4e11..7ceab5669 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -23,15 +23,14 @@ import org.openedx.core.config.Config import org.openedx.core.data.model.CourseEnrollments import org.openedx.core.domain.interactor.IAPInteractor import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.domain.model.iap.IAPFlow +import org.openedx.core.domain.model.iap.IAPFlowSource import org.openedx.core.exception.iap.IAPException import org.openedx.core.extension.isInternetError import org.openedx.core.presentation.IAPAnalytics -import org.openedx.core.presentation.IAPAnalyticsEvent -import org.openedx.core.presentation.IAPAnalyticsKeys -import org.openedx.core.presentation.IAPAnalyticsScreen import org.openedx.core.presentation.dialog.IAPDialogFragment import org.openedx.core.presentation.iap.IAPAction -import org.openedx.core.presentation.iap.IAPFlow +import org.openedx.core.presentation.iap.IAPEventLogger import org.openedx.core.presentation.iap.IAPRequestType import org.openedx.core.presentation.iap.IAPUIState import org.openedx.core.system.ResourceManager @@ -89,6 +88,8 @@ class DashboardGalleryViewModel( val iapUiState: SharedFlow get() = _iapUiState.asSharedFlow() + private val eventLogger = IAPEventLogger(analytics = iapAnalytics, isSilentIAPFlow = true) + private var isLoading = false init { @@ -180,7 +181,7 @@ class DashboardGalleryViewModel( if (course != null) { IAPDialogFragment.newInstance( iapFlow = IAPFlow.USER_INITIATED, - screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, + screenName = IAPFlowSource.COURSE_ENROLLMENT.screen, courseId = course.course.id, courseName = course.course.name, isSelfPaced = course.course.isSelfPaced, @@ -195,7 +196,7 @@ class DashboardGalleryViewModel( IAPAction.ACTION_COMPLETION -> { IAPDialogFragment.newInstance( IAPFlow.SILENT, - IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName + IAPFlowSource.COURSE_ENROLLMENT.screen ).show( fragmentManager, IAPDialogFragment.TAG @@ -212,7 +213,7 @@ class DashboardGalleryViewModel( } IAPAction.ACTION_ERROR_CLOSE -> { - logIAPCancelEvent() + eventLogger.logIAPCancelEvent() clearIAPState() } @@ -242,7 +243,7 @@ class DashboardGalleryViewModel( iapNotifier.notifier.onEach { event -> when (event) { is UpdateCourseData -> { - updateCourses(isIAPFlow = true) + updateCourses(isIAPFlow = event.isPurchasedFromCourseDashboard.not()) } } }.distinctUntilChanged().launchIn(viewModelScope) @@ -252,10 +253,7 @@ class DashboardGalleryViewModel( viewModelScope.launch(Dispatchers.IO) { iapInteractor.detectUnfulfilledPurchase( onSuccess = { - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED, - screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, - ) + eventLogger.logUnfulfilledPurchaseInitiatedEvent() _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) }, onFailure = { @@ -275,25 +273,7 @@ class DashboardGalleryViewModel( private fun showFeedbackScreen(message: String) { iapInteractor.showFeedbackScreen(context, message) - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, - params = buildMap { - put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) - put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_GET_HELP.action) - }.toMutableMap(), - screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, - ) - } - - private fun logIAPCancelEvent() { - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, - params = buildMap { - put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) - put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_CLOSE.action) - }.toMutableMap(), - screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, - ) + eventLogger.logGetHelpEvent() } private fun clearIAPState() { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt index 0f5f9284a..028497268 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -24,15 +24,14 @@ import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.interactor.IAPInteractor import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.domain.model.iap.IAPFlow +import org.openedx.core.domain.model.iap.IAPFlowSource import org.openedx.core.exception.iap.IAPException import org.openedx.core.extension.isInternetError import org.openedx.core.presentation.IAPAnalytics -import org.openedx.core.presentation.IAPAnalyticsEvent -import org.openedx.core.presentation.IAPAnalyticsKeys -import org.openedx.core.presentation.IAPAnalyticsScreen import org.openedx.core.presentation.dialog.IAPDialogFragment import org.openedx.core.presentation.iap.IAPAction -import org.openedx.core.presentation.iap.IAPFlow +import org.openedx.core.presentation.iap.IAPEventLogger import org.openedx.core.presentation.iap.IAPRequestType import org.openedx.core.presentation.iap.IAPUIState import org.openedx.core.system.ResourceManager @@ -99,6 +98,8 @@ class DashboardListViewModel( val appUpgradeEvent: LiveData get() = _appUpgradeEvent + private val eventLogger = IAPEventLogger(analytics = iapAnalytics, isSilentIAPFlow = true) + override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) viewModelScope.launch { @@ -112,7 +113,7 @@ class DashboardListViewModel( iapNotifier.notifier.onEach { event -> when (event) { is UpdateCourseData -> { - updateCourses(true) + updateCourses(isIAPFlow = event.isPurchasedFromCourseDashboard.not()) } } }.distinctUntilChanged().launchIn(viewModelScope) @@ -184,7 +185,7 @@ class DashboardListViewModel( if (course != null) { IAPDialogFragment.newInstance( iapFlow = IAPFlow.USER_INITIATED, - screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, + screenName = IAPFlowSource.COURSE_ENROLLMENT.screen, courseId = course.course.id, courseName = course.course.name, isSelfPaced = course.course.isSelfPaced, @@ -199,7 +200,7 @@ class DashboardListViewModel( IAPAction.ACTION_COMPLETION -> { IAPDialogFragment.newInstance( IAPFlow.SILENT, - IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName + IAPFlowSource.COURSE_ENROLLMENT.screen ).show( fragmentManager, IAPDialogFragment.TAG @@ -216,7 +217,7 @@ class DashboardListViewModel( } IAPAction.ACTION_ERROR_CLOSE -> { - logIAPCancelEvent() + eventLogger.logIAPCancelEvent() clearIAPState() } @@ -302,10 +303,7 @@ class DashboardListViewModel( viewModelScope.launch(Dispatchers.IO) { iapInteractor.detectUnfulfilledPurchase( onSuccess = { - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED, - screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, - ) + eventLogger.logUnfulfilledPurchaseInitiatedEvent() _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) }, onFailure = { @@ -325,25 +323,7 @@ class DashboardListViewModel( private fun showFeedbackScreen(message: String) { iapInteractor.showFeedbackScreen(context, message) - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, - params = buildMap { - put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) - put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_GET_HELP.action) - }.toMutableMap(), - screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, - ) - } - - private fun logIAPCancelEvent() { - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, - params = buildMap { - put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) - put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_CLOSE.action) - }.toMutableMap(), - screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, - ) + eventLogger.logGetHelpEvent() } private fun clearIAPState() { diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt index 399a1c559..f4bc7b2c6 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt @@ -10,10 +10,10 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import org.koin.androidx.viewmodel.ext.android.viewModel -import org.openedx.core.presentation.IAPAnalyticsScreen +import org.openedx.core.domain.model.iap.IAPFlow +import org.openedx.core.domain.model.iap.IAPFlowSource import org.openedx.core.presentation.dialog.IAPDialogFragment import org.openedx.core.presentation.iap.IAPAction -import org.openedx.core.presentation.iap.IAPFlow import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme @@ -113,7 +113,7 @@ class SettingsFragment : Fragment() { onIAPAction = { action, iapException -> when (action) { IAPAction.ACTION_ERROR_CLOSE -> { - viewModel.logIAPCancelEvent() + viewModel.eventLogger.logIAPCancelEvent() viewModel.clearIAPState() } @@ -126,7 +126,7 @@ class SettingsFragment : Fragment() { IAPAction.ACTION_RESTORE -> { IAPDialogFragment.newInstance( IAPFlow.RESTORE, - IAPAnalyticsScreen.PROFILE.screenName + IAPFlowSource.PROFILE.screen ).show( requireActivity().supportFragmentManager, IAPDialogFragment.TAG diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt index 5b4fded04..78b7d7495 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt @@ -25,11 +25,8 @@ import org.openedx.core.exception.iap.IAPException import org.openedx.core.extension.isInternetError import org.openedx.core.module.DownloadWorkerController import org.openedx.core.presentation.IAPAnalytics -import org.openedx.core.presentation.IAPAnalyticsEvent -import org.openedx.core.presentation.IAPAnalyticsKeys -import org.openedx.core.presentation.IAPAnalyticsScreen import org.openedx.core.presentation.global.AppData -import org.openedx.core.presentation.iap.IAPAction +import org.openedx.core.presentation.iap.IAPEventLogger import org.openedx.core.presentation.iap.IAPLoaderType import org.openedx.core.presentation.iap.IAPRequestType import org.openedx.core.presentation.iap.IAPUIState @@ -84,6 +81,8 @@ class SettingsViewModel( val appUpgradeEvent: StateFlow get() = _appUpgradeEvent.asStateFlow() + val eventLogger = IAPEventLogger(analytics = iapAnalytics, isSilentIAPFlow = false) + val isLogistrationEnabled get() = config.isPreLoginExperienceEnabled() private val configuration @@ -241,10 +240,7 @@ class SettingsViewModel( } fun restorePurchase() { - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_RESTORE_PURCHASE_CLICKED, - screenName = IAPAnalyticsScreen.PROFILE.screenName, - ) + eventLogger.logRestorePurchasesClickedEvent() viewModelScope.launch(Dispatchers.IO) { val userId = corePreferences.user?.id ?: return@launch @@ -256,10 +252,7 @@ class SettingsViewModel( iapInteractor.processUnfulfilledPurchase(userId) }.onSuccess { if (it) { - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED, - screenName = IAPAnalyticsScreen.PROFILE.screenName, - ) + eventLogger.logUnfulfilledPurchaseInitiatedEvent() _iapUiState.emit(IAPUIState.PurchasesFulfillmentCompleted) } else { _iapUiState.emit(IAPUIState.FakePurchasesFulfillmentCompleted) @@ -280,40 +273,13 @@ class SettingsViewModel( } } - fun logIAPCancelEvent() { - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, - buildMap { - put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_RESTORE.action) - put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_CLOSE.action) - }.toMutableMap(), - screenName = IAPAnalyticsScreen.PROFILE.screenName, - ) - } - fun showFeedbackScreen(context: Context, message: String) { iapInteractor.showFeedbackScreen(context, message) - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, - params = buildMap { - put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) - put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_GET_HELP.action) - }.toMutableMap(), - screenName = IAPAnalyticsScreen.PROFILE.screenName, - ) + eventLogger.logGetHelpEvent() } fun onRestorePurchaseCancel() { - iapAnalytics.logIAPEvent( - event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, - params = buildMap { - put( - IAPAnalyticsKeys.ACTION.key, - IAPAction.ACTION_CLOSE.action - ) - }.toMutableMap(), - screenName = IAPAnalyticsScreen.PROFILE.screenName, - ) + eventLogger.onRestorePurchaseCancel() } fun clearIAPState() {