From 73f72f095f85bb2cbc6361e5d771d5c8d6f30fc9 Mon Sep 17 00:00:00 2001 From: k1rill Date: Wed, 17 Jul 2024 12:22:17 +0300 Subject: [PATCH] fix: Add upgrade banner in the FC47 Primary Course card view --- .../java/org/openedx/app/AnalyticsManager.kt | 23 +++ .../java/org/openedx/app/di/ScreenModule.kt | 11 +- .../core/data/model/CourseEnrollments.kt | 1 + .../core/domain/interactor/IAPInteractor.kt | 45 +++++ .../openedx/core/presentation/IAPAnalytics.kt | 6 +- .../core/presentation/iap/IAPViewModel.kt | 39 ++-- .../openedx/core/ui/UpgradeToAccessView.kt | 75 ++++++-- .../presentation/DashboardGalleryView.kt | 80 ++++++++- .../presentation/DashboardGalleryViewModel.kt | 167 ++++++++++++++++- .../presentation/DashboardListFragment.kt | 55 +----- .../presentation/DashboardListViewModel.kt | 169 +++++++++++------- .../presentation/DashboardViewModelTest.kt | 24 +-- .../presentation/settings/SettingsFragment.kt | 10 +- .../settings/SettingsViewModel.kt | 69 +++---- 14 files changed, 556 insertions(+), 218 deletions(-) diff --git a/app/src/main/java/org/openedx/app/AnalyticsManager.kt b/app/src/main/java/org/openedx/app/AnalyticsManager.kt index b0adbcb22..ad624e7a5 100644 --- a/app/src/main/java/org/openedx/app/AnalyticsManager.kt +++ b/app/src/main/java/org/openedx/app/AnalyticsManager.kt @@ -9,7 +9,11 @@ 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 @@ -191,6 +195,25 @@ 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 d8d02314b..7eafcb31a 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -151,7 +151,7 @@ val screenModule = module { get(), get(), get(), - get() + get(), ) } @@ -164,7 +164,11 @@ val screenModule = module { get(), get(), get(), - windowSize + get(), + get(), + get(), + get(), + windowSize, ) } viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } @@ -213,6 +217,7 @@ val screenModule = module { get(), get(), get(), + get(), get() ) } @@ -441,7 +446,7 @@ val screenModule = module { } single { IAPRepository(get()) } - factory { IAPInteractor(get(), get()) } + factory { IAPInteractor(get(), get(), get(), get(), get()) } viewModel { (iapFlow: IAPFlow, purchaseFlowData: PurchaseFlowData) -> IAPViewModel( iapFlow = iapFlow, diff --git a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt index 2682f957c..47a040e63 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt @@ -39,6 +39,7 @@ data class CourseEnrollments( enrollments.results.forEach { courseData -> courseData.setStoreSku(appConfig.iapConfig.productPrefix) } + primaryCourse?.setStoreSku(appConfig.iapConfig.productPrefix) } return CourseEnrollments(enrollments, appConfig, primaryCourse) diff --git a/core/src/main/java/org/openedx/core/domain/interactor/IAPInteractor.kt b/core/src/main/java/org/openedx/core/domain/interactor/IAPInteractor.kt index b1f2e2762..e586d551c 100644 --- a/core/src/main/java/org/openedx/core/domain/interactor/IAPInteractor.kt +++ b/core/src/main/java/org/openedx/core/domain/interactor/IAPInteractor.kt @@ -1,23 +1,47 @@ package org.openedx.core.domain.interactor +import android.content.Context import androidx.fragment.app.FragmentActivity import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.Purchase import org.openedx.core.ApiConstants +import org.openedx.core.R +import org.openedx.core.config.Config import org.openedx.core.data.repository.iap.IAPRepository +import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.iap.ProductInfo import org.openedx.core.exception.iap.IAPException import org.openedx.core.extension.decodeToLong 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.global.AppData import org.openedx.core.presentation.iap.IAPRequestType +import org.openedx.core.utils.EmailUtil class IAPInteractor( + private val appData: AppData, private val billingProcessor: BillingProcessor, + private val config: Config, private val repository: IAPRepository, + private val preferencesManager: CorePreferences, ) { + private val iapConfig + get() = preferencesManager.appConfig.iapConfig + private val isIAPEnabled + get() = iapConfig.isEnabled && iapConfig.disableVersions.contains(appData.versionName).not() + + fun showFeedbackScreen(context: Context, message: String) { + EmailUtil.showFeedbackScreen( + context = context, + feedbackEmailAddress = config.getFeedbackEmailAddress(), + subject = context.getString(R.string.core_error_upgrading_course_in_app), + feedback = message, + appVersion = appData.versionName + ) + } + suspend fun loadPrice(productId: String): ProductDetails.OneTimePurchaseOfferDetails { val response = billingProcessor.querySyncDetails(productId) val productDetails = response.productDetailsList?.firstOrNull()?.oneTimePurchaseOfferDetails @@ -122,4 +146,25 @@ class IAPInteractor( } } } + + suspend fun detectUnfulfilledPurchase( + onSuccess: () -> Unit, + onFailure: (IAPException) -> Unit, + ) { + if (isIAPEnabled) { + preferencesManager.user?.id?.let { userId -> + runCatching { + processUnfulfilledPurchase(userId) + }.onSuccess { + if (it) { + onSuccess() + } + }.onFailure { + if (it is IAPException) { + onFailure(it) + } + } + } + } + } } 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 68bd4f7c2..63f548b95 100644 --- a/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt +++ b/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt @@ -1,7 +1,11 @@ package org.openedx.core.presentation interface IAPAnalytics { - fun logEvent(event: String, params: Map) + fun logIAPEvent( + event: IAPAnalyticsEvent, + params: MutableMap = mutableMapOf(), + screenName: String, + ) } enum class IAPAnalyticsEvent(val eventName: String, val biValue: String) { 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 2946ef5df..5ea0a4964 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 @@ -339,24 +339,27 @@ class IAPViewModel( event: IAPAnalyticsEvent, params: MutableMap = mutableMapOf() ) { - analytics.logEvent(event.eventName, params.apply { - put(IAPAnalyticsKeys.NAME.key, event.biValue) - purchaseFlowData.takeIf { it.courseId.isNullOrBlank().not() }?.let { - put(IAPAnalyticsKeys.COURSE_ID.key, purchaseFlowData.courseId) - put( - IAPAnalyticsKeys.PACING.key, - if (purchaseFlowData.isSelfPaced == true) IAPAnalyticsKeys.SELF.key else IAPAnalyticsKeys.INSTRUCTOR.key - ) - } - purchaseFlowData.formattedPrice?.takeIf { it.isNotBlank() }?.let { formattedPrice -> - put(IAPAnalyticsKeys.PRICE.key, formattedPrice) - } - purchaseFlowData.componentId?.takeIf { it.isNotBlank() }?.let { componentId -> - put(IAPAnalyticsKeys.COMPONENT_ID.key, componentId) - } - put(IAPAnalyticsKeys.SCREEN_NAME.key, purchaseFlowData.screenName) - put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key) - }) + analytics.logIAPEvent( + event = event, + params = params.apply { + put(IAPAnalyticsKeys.NAME.key, event.biValue) + purchaseFlowData.takeIf { it.courseId.isNullOrBlank().not() }?.let { + put(IAPAnalyticsKeys.COURSE_ID.key, purchaseFlowData.courseId) + put( + IAPAnalyticsKeys.PACING.key, + if (purchaseFlowData.isSelfPaced == true) IAPAnalyticsKeys.SELF.key else IAPAnalyticsKeys.INSTRUCTOR.key + ) + } + purchaseFlowData.formattedPrice?.takeIf { it.isNotBlank() }?.let { formattedPrice -> + put(IAPAnalyticsKeys.PRICE.key, formattedPrice) + } + purchaseFlowData.componentId?.takeIf { it.isNotBlank() }?.let { componentId -> + put(IAPAnalyticsKeys.COMPONENT_ID.key, componentId) + } + put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key) + }, + screenName = purchaseFlowData.screenName.orEmpty() + ) } fun clearIAPFLow() { diff --git a/core/src/main/java/org/openedx/core/ui/UpgradeToAccessView.kt b/core/src/main/java/org/openedx/core/ui/UpgradeToAccessView.kt index 8e24936b3..9fbb74fad 100644 --- a/core/src/main/java/org/openedx/core/ui/UpgradeToAccessView.kt +++ b/core/src/main/java/org/openedx/core/ui/UpgradeToAccessView.kt @@ -2,20 +2,26 @@ package org.openedx.core.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos +import androidx.compose.material.icons.filled.EmojiEvents import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Lock import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -31,54 +37,86 @@ import org.openedx.core.ui.theme.appTypography fun UpgradeToAccessView( modifier: Modifier = Modifier, type: UpgradeToAccessViewType = UpgradeToAccessViewType.DASHBOARD, + iconPadding: PaddingValues = PaddingValues(end = 16.dp), + padding: PaddingValues = PaddingValues(vertical = 8.dp, horizontal = 16.dp), onClick: () -> Unit, ) { - val shape = when (type) { - UpgradeToAccessViewType.DASHBOARD -> RoundedCornerShape( - bottomStart = 16.dp, - bottomEnd = 16.dp + val shape: Shape + var primaryIcon = Icons.Filled.Lock + var textColor = MaterialTheme.appColors.primaryButtonText + var backgroundColor = MaterialTheme.appColors.primaryButtonBackground + var secondaryIcon: @Composable () -> Unit = { + Icon( + modifier = Modifier + .padding(start = 16.dp), + imageVector = Icons.Filled.Info, + contentDescription = null, + tint = textColor ) + } + when (type) { + UpgradeToAccessViewType.DASHBOARD -> { + shape = RoundedCornerShape( + bottomStart = 16.dp, + bottomEnd = 16.dp + ) + } + + UpgradeToAccessViewType.COURSE -> { + shape = MaterialTheme.appShapes.buttonShape + } - UpgradeToAccessViewType.COURSE -> MaterialTheme.appShapes.buttonShape + UpgradeToAccessViewType.GALLERY -> { + primaryIcon = Icons.Filled.EmojiEvents + textColor = MaterialTheme.appColors.textDark + shape = RectangleShape + backgroundColor = textColor.copy(0.05f) + secondaryIcon = { + Icon( + modifier = Modifier + .padding(start = 16.dp) + .size(16.dp), + imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, + contentDescription = null, + tint = textColor + ) + } + } } Row( modifier = modifier .clip(shape = shape) .fillMaxWidth() - .background(color = MaterialTheme.appColors.primaryButtonBackground) + .background(color = backgroundColor) .clickable { onClick() } - .padding(vertical = 8.dp, horizontal = 16.dp), + .padding(padding), verticalAlignment = Alignment.CenterVertically ) { Icon( - modifier = Modifier.padding(end = 16.dp), - imageVector = Icons.Filled.Lock, + modifier = Modifier.padding(iconPadding), + imageVector = primaryIcon, contentDescription = null, - tint = MaterialTheme.appColors.primaryButtonText + tint = textColor ) Text( modifier = Modifier.weight(1f), text = stringResource(id = R.string.iap_upgrade_access_course), - color = MaterialTheme.appColors.primaryButtonText, + color = textColor, style = MaterialTheme.appTypography.labelLarge ) - Icon( - modifier = Modifier.padding(start = 16.dp), - imageVector = Icons.Filled.Info, - contentDescription = null, - tint = MaterialTheme.appColors.primaryButtonText - ) + secondaryIcon() } } enum class UpgradeToAccessViewType { + GALLERY, DASHBOARD, COURSE, } -@Preview +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) @Composable private fun UpgradeToAccessViewPreview( @PreviewParameter(UpgradeToAccessViewTypeParameterProvider::class) type: UpgradeToAccessViewType @@ -93,5 +131,6 @@ private class UpgradeToAccessViewTypeParameterProvider : override val values = sequenceOf( UpgradeToAccessViewType.DASHBOARD, UpgradeToAccessViewType.COURSE, + UpgradeToAccessViewType.GALLERY, ) } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt index 8f6cc124b..52e314516 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -42,6 +42,7 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -83,10 +84,17 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Pagination import org.openedx.core.domain.model.Progress +import org.openedx.core.exception.iap.IAPException +import org.openedx.core.presentation.iap.IAPAction +import org.openedx.core.presentation.iap.IAPUIState import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog import org.openedx.core.ui.OpenEdXButton +import org.openedx.core.ui.PurchasesFulfillmentCompletedDialog import org.openedx.core.ui.TextIcon +import org.openedx.core.ui.UpgradeErrorDialog +import org.openedx.core.ui.UpgradeToAccessView +import org.openedx.core.ui.UpgradeToAccessViewType import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors @@ -106,10 +114,12 @@ fun DashboardGalleryView( val updating by viewModel.updating.collectAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) val uiState by viewModel.uiState.collectAsState(DashboardGalleryUIState.Loading) + val iapUiState by viewModel.iapUiState.collectAsState(IAPUIState.Clear) DashboardGalleryView( uiMessage = uiMessage, uiState = uiState, + iapUiState = iapUiState, updating = updating, apiHostUrl = viewModel.apiHostUrl, hasInternetConnection = viewModel.hasInternetConnection, @@ -154,6 +164,11 @@ fun DashboardGalleryView( ) } } + }, + onIAPAction = { action, course, iapException -> + viewModel.processIAPAction( + fragmentManager, action, course, iapException + ) } ) } @@ -163,9 +178,11 @@ fun DashboardGalleryView( private fun DashboardGalleryView( uiMessage: UIMessage?, uiState: DashboardGalleryUIState, + iapUiState: IAPUIState?, updating: Boolean, apiHostUrl: String, onAction: (DashboardGalleryScreenAction) -> Unit, + onIAPAction: (IAPAction, EnrolledCourse?, IAPException?) -> Unit = { _, _, _ -> }, hasInternetConnection: Boolean ) { val scaffoldState = rememberScaffoldState() @@ -229,8 +246,14 @@ private fun DashboardGalleryView( blockId ) ) - } + }, + onIAPAction = onIAPAction, ) + LaunchedEffect(uiState.userCourses.enrollments.courses) { + if (uiState.userCourses.enrollments.courses.isNotEmpty()) { + onIAPAction(IAPAction.ACTION_UNFULFILLED, null, null) + } + } } is DashboardGalleryUIState.Empty -> { @@ -268,6 +291,40 @@ private fun DashboardGalleryView( } ) } + when (iapUiState) { + is IAPUIState.PurchasesFulfillmentCompleted -> { + PurchasesFulfillmentCompletedDialog(onConfirm = { + onIAPAction(IAPAction.ACTION_COMPLETION, null, null) + }, onDismiss = { + onIAPAction(IAPAction.ACTION_CLOSE, null, null) + }) + } + + is IAPUIState.Error -> { + UpgradeErrorDialog( + title = stringResource(id = CoreR.string.iap_error_title), + description = stringResource(id = CoreR.string.iap_course_not_fullfilled), + confirmText = stringResource(id = CoreR.string.core_cancel), + onConfirm = { + onIAPAction( + IAPAction.ACTION_ERROR_CLOSE, + null, + null + ) + }, + dismissText = stringResource(id = CoreR.string.iap_get_help), + onDismiss = { + onIAPAction( + IAPAction.ACTION_GET_HELP, + null, + iapUiState.iapException + ) + } + ) + } + + else -> {} + } } } } @@ -282,6 +339,7 @@ private fun UserCourses( navigateToDates: (EnrolledCourse) -> Unit, onViewAllClick: () -> Unit, resumeBlockId: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, + onIAPAction: (IAPAction, EnrolledCourse?, IAPException?) -> Unit = { _, _, _ -> }, ) { Column( modifier = modifier @@ -290,11 +348,13 @@ private fun UserCourses( val primaryCourse = userCourses.primary if (primaryCourse != null) { PrimaryCourseCard( + isValuePropEnabled = userCourses.configs.isValuePropEnabled, primaryCourse = primaryCourse, apiHostUrl = apiHostUrl, navigateToDates = navigateToDates, resumeBlockId = resumeBlockId, - openCourse = openCourse + openCourse = openCourse, + onIAPAction = onIAPAction, ) } if (userCourses.enrollments.courses.isNotEmpty()) { @@ -507,11 +567,13 @@ private fun AssignmentItem( @Composable private fun PrimaryCourseCard( + isValuePropEnabled: Boolean, primaryCourse: EnrolledCourse, apiHostUrl: String, navigateToDates: (EnrolledCourse) -> Unit, resumeBlockId: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, openCourse: (EnrolledCourse) -> Unit, + onIAPAction: (IAPAction, EnrolledCourse?, IAPException?) -> Unit = { _, _, _ -> }, ) { val context = LocalContext.current Card( @@ -605,6 +667,19 @@ private fun PrimaryCourseCard( ) ) } + if (primaryCourse.isUpgradeable && isValuePropEnabled) { + UpgradeToAccessView( + type = UpgradeToAccessViewType.GALLERY, + iconPadding = PaddingValues(end = 12.dp), + padding = PaddingValues(vertical = 16.dp, horizontal = 14.dp) + ) { + onIAPAction( + IAPAction.ACTION_USER_INITIATED, + primaryCourse, + null + ) + } + } ResumeButton( primaryCourse = primaryCourse, onClick = { @@ -863,6 +938,7 @@ private fun DashboardGalleryViewPreview() { OpenEdXTheme { DashboardGalleryView( uiState = DashboardGalleryUIState.Courses(mockUserCourses), + iapUiState = null, apiHostUrl = "", uiMessage = null, updating = false, 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 7f1036e1d..7aa054a05 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -1,32 +1,55 @@ package org.openedx.courses.presentation +import android.annotation.SuppressLint +import android.content.Context import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.UIMessage 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.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.IAPRequestType +import org.openedx.core.presentation.iap.IAPUIState import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate +import org.openedx.core.system.notifier.CourseDataUpdated import org.openedx.core.system.notifier.DiscoveryNotifier +import org.openedx.core.system.notifier.IAPNotifier import org.openedx.core.system.notifier.NavigationToDiscovery +import org.openedx.core.system.notifier.UpdateCourseData import org.openedx.core.ui.WindowSize import org.openedx.core.utils.FileUtil import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardRouter +@SuppressLint("StaticFieldLeak") class DashboardGalleryViewModel( + private val context: Context, private val config: Config, private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, @@ -34,7 +57,10 @@ class DashboardGalleryViewModel( private val networkConnection: NetworkConnection, private val fileUtil: FileUtil, private val dashboardRouter: DashboardRouter, - private val windowSize: WindowSize + private val iapNotifier: IAPNotifier, + private val iapInteractor: IAPInteractor, + private val iapAnalytics: IAPAnalytics, + private val windowSize: WindowSize, ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() @@ -55,14 +81,23 @@ class DashboardGalleryViewModel( val hasInternetConnection: Boolean get() = networkConnection.isOnline() + private val _iapUiState = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + val iapUiState: SharedFlow + get() = _iapUiState.asSharedFlow() + private var isLoading = false init { collectDiscoveryNotifier() + collectIapNotifier() getCourses() } - fun getCourses() { + fun getCourses(isIAPFlow: Boolean = false) { viewModelScope.launch { try { if (networkConnection.isOnline()) { @@ -78,6 +113,9 @@ class DashboardGalleryViewModel( } else { _uiState.value = DashboardGalleryUIState.Courses(response) } + if (isIAPFlow) { + iapNotifier.send(CourseDataUpdated()) + } } else { val courseEnrollments = fileUtil.getObjectFromFile() if (courseEnrollments == null) { @@ -100,12 +138,12 @@ class DashboardGalleryViewModel( } } - fun updateCourses() { + fun updateCourses(isIAPFlow: Boolean = false) { if (isLoading) { return } _updating.value = true - getCourses() + getCourses(isIAPFlow = isIAPFlow) } fun navigateToDiscovery() { @@ -132,6 +170,63 @@ class DashboardGalleryViewModel( ) } + fun processIAPAction( + fragmentManager: FragmentManager, + action: IAPAction, + course: EnrolledCourse?, + iapException: IAPException?, + ) { + when (action) { + IAPAction.ACTION_USER_INITIATED -> { + if (course != null) { + IAPDialogFragment.newInstance( + iapFlow = IAPFlow.USER_INITIATED, + screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, + courseId = course.course.id, + courseName = course.course.name, + isSelfPaced = course.course.isSelfPaced, + productInfo = course.productInfo + ).show( + fragmentManager, + IAPDialogFragment.TAG + ) + } + } + + IAPAction.ACTION_COMPLETION -> { + IAPDialogFragment.newInstance( + IAPFlow.SILENT, + IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName + ).show( + fragmentManager, + IAPDialogFragment.TAG + ) + clearIAPState() + } + + IAPAction.ACTION_UNFULFILLED -> { + detectUnfulfilledPurchase() + } + + IAPAction.ACTION_CLOSE -> { + clearIAPState() + } + + IAPAction.ACTION_ERROR_CLOSE -> { + logIAPCancelEvent() + } + + IAPAction.ACTION_GET_HELP -> { + iapException?.getFormattedErrorMessage()?.let { + showFeedbackScreen(it) + } + } + + else -> { + } + } + } + private fun collectDiscoveryNotifier() { viewModelScope.launch { discoveryNotifier.notifier.collect { @@ -142,6 +237,70 @@ class DashboardGalleryViewModel( } } + private fun collectIapNotifier() { + iapNotifier.notifier.onEach { event -> + when (event) { + is UpdateCourseData -> { + updateCourses(true) + } + } + }.distinctUntilChanged().launchIn(viewModelScope) + } + + private fun detectUnfulfilledPurchase() { + viewModelScope.launch(Dispatchers.IO) { + iapInteractor.detectUnfulfilledPurchase( + onSuccess = { + iapAnalytics.logIAPEvent( + event = IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED, + screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, + ) + _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) + }, + onFailure = { + _iapUiState.tryEmit( + IAPUIState.Error( + IAPException( + IAPRequestType.UNFULFILLED_CODE, + it.httpErrorCode, + it.errorMessage + ) + ) + ) + } + ) + } + } + + 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, + ) + } + + private fun clearIAPState() { + viewModelScope.launch { + _iapUiState.emit(null) + } + } + companion object { private const val PAGE_SIZE_TABLET = 7 private const val PAGE_SIZE_PHONE = 5 diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt index 7d5a4e360..e89427607 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -85,11 +85,8 @@ import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Progress import org.openedx.core.domain.model.iap.ProductInfo import org.openedx.core.exception.iap.IAPException -import org.openedx.core.presentation.IAPAnalyticsScreen -import org.openedx.core.presentation.dialog.IAPDialogFragment import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox import org.openedx.core.presentation.iap.IAPAction -import org.openedx.core.presentation.iap.IAPFlow import org.openedx.core.presentation.iap.IAPUIState import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.ui.HandleUIMessage @@ -172,55 +169,9 @@ class DashboardListFragment : Fragment() { }, ), onIAPAction = { action, course, iapException -> - when (action) { - IAPAction.ACTION_USER_INITIATED -> { - if (course != null) { - IAPDialogFragment.newInstance( - iapFlow = IAPFlow.USER_INITIATED, - screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, - courseId = course.course.id, - courseName = course.course.name, - isSelfPaced = course.course.isSelfPaced, - productInfo = course.productInfo!! - ).show( - requireActivity().supportFragmentManager, - IAPDialogFragment.TAG - ) - } - } - - IAPAction.ACTION_COMPLETION -> { - IAPDialogFragment.newInstance( - IAPFlow.SILENT, - IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName - ).show( - requireActivity().supportFragmentManager, - IAPDialogFragment.TAG - ) - viewModel.clearIAPState() - } - - IAPAction.ACTION_UNFULFILLED -> { - viewModel.detectUnfulfilledPurchase() - } - - IAPAction.ACTION_CLOSE -> { - viewModel.clearIAPState() - } - - IAPAction.ACTION_ERROR_CLOSE -> { - viewModel.logIAPCancelEvent() - } - - IAPAction.ACTION_GET_HELP -> { - iapException?.getFormattedErrorMessage() - ?.let { viewModel.showFeedbackScreen(requireActivity(), it) } - } - - else -> { - - } - } + viewModel.processIAPAction( + requireActivity().supportFragmentManager, action, course, iapException + ) } ) } 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 a993a7291..f3cfea743 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -1,6 +1,8 @@ package org.openedx.dashboard.presentation +import android.annotation.SuppressLint import android.content.Context +import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -28,7 +30,7 @@ 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.dialog.IAPDialogFragment import org.openedx.core.presentation.iap.IAPAction import org.openedx.core.presentation.iap.IAPFlow import org.openedx.core.presentation.iap.IAPRequestType @@ -42,11 +44,11 @@ import org.openedx.core.system.notifier.IAPNotifier import org.openedx.core.system.notifier.UpdateCourseData import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.AppUpgradeEvent -import org.openedx.core.utils.EmailUtil import org.openedx.dashboard.domain.interactor.DashboardInteractor +@SuppressLint("StaticFieldLeak") class DashboardListViewModel( - private val appData: AppData, + private val context: Context, private val config: Config, private val networkConnection: NetworkConnection, private val interactor: DashboardInteractor, @@ -57,7 +59,7 @@ class DashboardListViewModel( private val appNotifier: AppNotifier, private val preferencesManager: CorePreferences, private val iapAnalytics: IAPAnalytics, - private val iapInteractor: IAPInteractor + private val iapInteractor: IAPInteractor, ) : BaseViewModel() { private val coursesList = mutableListOf() @@ -97,12 +99,6 @@ class DashboardListViewModel( val appUpgradeEvent: LiveData get() = _appUpgradeEvent - private val iapConfig - get() = preferencesManager.appConfig.iapConfig - private val isIAPEnabled - get() = iapConfig.isEnabled && - iapConfig.disableVersions.contains(appData.versionName).not() - override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) viewModelScope.launch { @@ -177,6 +173,63 @@ class DashboardListViewModel( } } + fun processIAPAction( + fragmentManager: FragmentManager, + action: IAPAction, + course: EnrolledCourse?, + iapException: IAPException?, + ) { + when (action) { + IAPAction.ACTION_USER_INITIATED -> { + if (course != null) { + IAPDialogFragment.newInstance( + iapFlow = IAPFlow.USER_INITIATED, + screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, + courseId = course.course.id, + courseName = course.course.name, + isSelfPaced = course.course.isSelfPaced, + productInfo = course.productInfo + ).show( + fragmentManager, + IAPDialogFragment.TAG + ) + } + } + + IAPAction.ACTION_COMPLETION -> { + IAPDialogFragment.newInstance( + IAPFlow.SILENT, + IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName + ).show( + fragmentManager, + IAPDialogFragment.TAG + ) + clearIAPState() + } + + IAPAction.ACTION_UNFULFILLED -> { + detectUnfulfilledPurchase() + } + + IAPAction.ACTION_CLOSE -> { + clearIAPState() + } + + IAPAction.ACTION_ERROR_CLOSE -> { + logIAPCancelEvent() + } + + IAPAction.ACTION_GET_HELP -> { + iapException?.getFormattedErrorMessage()?.let { + showFeedbackScreen(it) + } + } + + else -> { + } + } + } + private fun internalLoadingCourses() { viewModelScope.launch { try { @@ -243,73 +296,55 @@ class DashboardListViewModel( analytics.dashboardCourseClickedEvent(courseId, courseName) } - fun detectUnfulfilledPurchase() { - if (isIAPEnabled) { - viewModelScope.launch(Dispatchers.IO) { - preferencesManager.user?.id?.let { userId -> - runCatching { - iapInteractor.processUnfulfilledPurchase(userId) - }.onSuccess { - if (it) { - unfulfilledPurchaseInitiatedEvent() - _iapUiState.emit(IAPUIState.PurchasesFulfillmentCompleted) - } - }.onFailure { - if (it is IAPException) { - _iapUiState.emit( - IAPUIState.Error( - IAPException( - IAPRequestType.UNFULFILLED_CODE, - it.httpErrorCode, - it.errorMessage - ) - ) + private fun detectUnfulfilledPurchase() { + viewModelScope.launch(Dispatchers.IO) { + iapInteractor.detectUnfulfilledPurchase( + onSuccess = { + iapAnalytics.logIAPEvent( + event = IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED, + screenName = IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName, + ) + _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) + }, + onFailure = { + _iapUiState.tryEmit( + IAPUIState.Error( + IAPException( + IAPRequestType.UNFULFILLED_CODE, + it.httpErrorCode, + it.errorMessage, ) - } - } + ) + ) } - } + ) } } - private fun unfulfilledPurchaseInitiatedEvent() { - logIAPEvent(IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED) - } - - fun showFeedbackScreen(context: Context, message: String) { - EmailUtil.showFeedbackScreen( - context = context, - feedbackEmailAddress = config.getFeedbackEmailAddress(), - subject = context.getString(R.string.core_error_upgrading_course_in_app), - feedback = message, - appVersion = appData.versionName + 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, ) - logIAPEvent(IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, buildMap { - put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) - put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_GET_HELP.action) - }.toMutableMap()) } - fun logIAPCancelEvent() { - logIAPEvent(IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, buildMap { - put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) - put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_CLOSE.action) - }.toMutableMap()) - } - - private fun logIAPEvent( - event: IAPAnalyticsEvent, - params: MutableMap = mutableMapOf() - ) { - iapAnalytics.logEvent(event.eventName, params.apply { - put(IAPAnalyticsKeys.NAME.key, event.biValue) - put(IAPAnalyticsKeys.SCREEN_NAME.key, IAPAnalyticsScreen.COURSE_ENROLLMENT.screenName) - put(IAPAnalyticsKeys.IAP_FLOW_TYPE.key, IAPFlow.SILENT.value) - put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key) - }) + 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, + ) } - fun clearIAPState() { + private fun clearIAPState() { viewModelScope.launch { _iapUiState.emit(null) } diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt index e51468605..507afcfee 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt @@ -1,5 +1,6 @@ package org.openedx.dashboard.presentation +import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -35,7 +36,6 @@ import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.IAPConfig import org.openedx.core.domain.model.Pagination import org.openedx.core.presentation.IAPAnalytics -import org.openedx.core.presentation.global.AppData import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate @@ -65,7 +65,7 @@ class DashboardViewModelTest { private val appNotifier = mockk() private val iapAnalytics = mockk() private val corePreferences = mockk() - private val appData = mockk() + private val context = mockk() private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" @@ -105,7 +105,7 @@ class DashboardViewModelTest { every { corePreferences.appConfig } returns appConfig val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, @@ -136,7 +136,7 @@ class DashboardViewModelTest { every { corePreferences.appConfig } returns appConfig val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, @@ -168,7 +168,7 @@ class DashboardViewModelTest { every { corePreferences.appConfig } returns appConfig val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, @@ -200,7 +200,7 @@ class DashboardViewModelTest { every { corePreferences.appConfig } returns appConfig val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, @@ -242,7 +242,7 @@ class DashboardViewModelTest { every { corePreferences.appConfig.iapConfig } returns appConfig.iapConfig val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, @@ -272,7 +272,7 @@ class DashboardViewModelTest { every { corePreferences.appConfig } returns appConfig coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, @@ -306,7 +306,7 @@ class DashboardViewModelTest { every { corePreferences.appConfig } returns appConfig coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, @@ -344,7 +344,7 @@ class DashboardViewModelTest { coEvery { iapNotifier.send(any()) } returns Unit val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, @@ -388,7 +388,7 @@ class DashboardViewModelTest { coEvery { iapNotifier.send(any()) } returns Unit val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, @@ -421,7 +421,7 @@ class DashboardViewModelTest { coEvery { iapNotifier.notifier } returns flow { emit(CourseDataUpdated()) } every { corePreferences.appConfig } returns appConfig val viewModel = DashboardListViewModel( - appData, + context, config, networkConnection, interactor, 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 7a3f3f7eb..54ed28e20 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 @@ -131,15 +131,7 @@ class SettingsFragment : Fragment() { } IAPAction.ACTION_RESTORE_PURCHASE_CANCEL -> { - viewModel.logIAPEvent( - IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, - buildMap { - put( - IAPAnalyticsKeys.ACTION.key, - IAPAction.ACTION_CLOSE.action - ) - }.toMutableMap() - ) + viewModel.onRestorePurchaseCancel() viewModel.clearIAPState() } 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 305797cfd..6393234e0 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 @@ -24,6 +24,7 @@ import org.openedx.core.domain.interactor.IAPInteractor 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 @@ -54,6 +55,7 @@ class SettingsViewModel( private val resourceManager: ResourceManager, private val corePreferences: CorePreferences, private val interactor: ProfileInteractor, + private val iapAnalytics: IAPAnalytics, private val iapInteractor: IAPInteractor, private val cookieManager: AppCookieManager, private val workerController: DownloadWorkerController, @@ -235,7 +237,10 @@ class SettingsViewModel( } fun restorePurchase() { - logIAPEvent(IAPAnalyticsEvent.IAP_RESTORE_PURCHASE_CLICKED) + iapAnalytics.logIAPEvent( + event = IAPAnalyticsEvent.IAP_RESTORE_PURCHASE_CLICKED, + screenName = IAPAnalyticsScreen.PROFILE.screenName, + ) viewModelScope.launch(Dispatchers.IO) { val userId = corePreferences.user?.id ?: return@launch @@ -247,13 +252,10 @@ class SettingsViewModel( iapInteractor.processUnfulfilledPurchase(userId) }.onSuccess { if (it) { - logIAPEvent(IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED, buildMap { - put( - IAPAnalyticsKeys.SCREEN_NAME.key, - IAPAnalyticsScreen.PROFILE.screenName - ) - put(IAPAnalyticsKeys.IAP_FLOW_TYPE.key, IAPFlow.RESTORE.value) - }.toMutableMap()) + iapAnalytics.logIAPEvent( + event = IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED, + screenName = IAPAnalyticsScreen.PROFILE.screenName, + ) _iapUiState.emit(IAPUIState.PurchasesFulfillmentCompleted) } else { _iapUiState.emit(IAPUIState.FakePurchasesFulfillmentCompleted) @@ -275,36 +277,39 @@ class SettingsViewModel( } fun logIAPCancelEvent() { - logIAPEvent(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()) + 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) { - EmailUtil.showFeedbackScreen( - context = context, - feedbackEmailAddress = config.getFeedbackEmailAddress(), - subject = context.getString(R.string.core_error_upgrading_course_in_app), - feedback = message, - appVersion = appData.versionName + 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, ) - logIAPEvent(IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, buildMap { - put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action) - put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_GET_HELP.action) - }.toMutableMap()) } - fun logIAPEvent( - event: IAPAnalyticsEvent, - params: MutableMap = mutableMapOf() - ) { - analytics.logEvent(event.eventName, params.apply { - put(IAPAnalyticsKeys.NAME.key, event.biValue) - put(IAPAnalyticsKeys.SCREEN_NAME.key, IAPAnalyticsScreen.PROFILE.screenName) - put(IAPAnalyticsKeys.IAP_FLOW_TYPE.key, IAPFlow.RESTORE.value) - put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key) - }) + 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, + ) } fun clearIAPState() {