From c9188d487bc90ec87959d1e07fbfea5da018a81f Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Mon, 13 Jan 2025 14:54:25 +0500 Subject: [PATCH 1/5] chore: add missing course properties in IAP unfulfilled event fixes: LEARNER-10309 --- .../core/domain/interactor/IAPInteractor.kt | 77 +++++++++++++------ .../core/presentation/iap/IAPEventLogger.kt | 6 +- .../presentation/DashboardGalleryViewModel.kt | 13 +++- .../presentation/DashboardListViewModel.kt | 11 ++- 4 files changed, 79 insertions(+), 28 deletions(-) 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 970d4ef9e..97ce0ca90 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 @@ -10,9 +10,12 @@ 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.EnrolledCourse import org.openedx.core.domain.model.iap.ProductInfo +import org.openedx.core.domain.model.iap.PurchaseFlowData import org.openedx.core.exception.iap.IAPException import org.openedx.core.extension.decodeToLong +import org.openedx.core.extension.decodeToString import org.openedx.core.module.billing.BillingProcessor import org.openedx.core.module.billing.getCourseSku import org.openedx.core.module.billing.getPriceAmount @@ -109,12 +112,42 @@ class IAPInteractor( } } - suspend fun processUnfulfilledPurchase(userId: Long): Boolean { + suspend fun processUnfulfilledPurchase( + userId: Long, + enrolledCourses: List = arrayListOf(), + purchaseVerified: (PurchaseFlowData) -> Unit = {}, + ): Boolean { val purchases = billingProcessor.queryPurchases() - val userPurchases = - purchases.filter { it.accountIdentifiers?.obfuscatedAccountId?.decodeToLong() == userId } + val userPurchases = purchases.filter { purchase -> + val userAccountId = purchase.accountIdentifiers?.obfuscatedAccountId?.decodeToLong() + val storeSku = purchase.accountIdentifiers?.obfuscatedProfileId?.decodeToString() + + userAccountId == userId && enrolledCourses.any { enrolledCourse -> + storeSku == enrolledCourse.productInfo?.courseSku + } + } if (userPurchases.isNotEmpty()) { - startUnfulfilledVerification(userPurchases) + userPurchases.forEach { purchase -> + val courseVerified = enrolledCourses.find { enrolledCourse -> + enrolledCourse.productInfo?.courseSku == purchase.getCourseSku() + } + courseVerified?.let { + val productDetails = + billingProcessor.querySyncDetails(purchase.products[0]).productDetailsList?.firstOrNull() + val purchaseProductFlow = PurchaseFlowData( + courseId = courseVerified.course.id, + isSelfPaced = courseVerified.course.isSelfPaced, + productInfo = courseVerified.productInfo + ).apply { + productDetails?.oneTimePurchaseOfferDetails?.let { + this.price = it.getPriceAmount() + this.currencyCode = it.priceCurrencyCode + } + } + startUnfulfilledVerification(purchase) + purchaseVerified(purchaseProductFlow) + } + } return true } else { purchases.forEach { @@ -124,34 +157,34 @@ class IAPInteractor( return false } - private suspend fun startUnfulfilledVerification(userPurchases: List) { - userPurchases.forEach { purchase -> - val productDetail = - billingProcessor.querySyncDetails(purchase.products.first()).productDetailsList?.firstOrNull() - productDetail?.oneTimePurchaseOfferDetails?.takeIf { - purchase.getCourseSku().isNullOrEmpty().not() - }?.let { oneTimeProductDetails -> - val courseSku = purchase.getCourseSku() ?: return@let - val basketId = addToBasket(courseSku) - executeOrder( - basketId = basketId, - purchaseToken = purchase.purchaseToken, - price = oneTimeProductDetails.getPriceAmount(), - currencyCode = oneTimeProductDetails.priceCurrencyCode, - ) - consumePurchase(purchase.purchaseToken) - } + private suspend fun startUnfulfilledVerification(userPurchase: Purchase) { + val productDetail = + billingProcessor.querySyncDetails(userPurchase.products.first()).productDetailsList?.firstOrNull() + productDetail?.oneTimePurchaseOfferDetails?.takeIf { + userPurchase.getCourseSku().isNullOrEmpty().not() + }?.let { oneTimeProductDetails -> + val courseSku = userPurchase.getCourseSku() ?: return@let + val basketId = addToBasket(courseSku) + executeOrder( + basketId = basketId, + purchaseToken = userPurchase.purchaseToken, + price = oneTimeProductDetails.getPriceAmount(), + currencyCode = oneTimeProductDetails.priceCurrencyCode, + ) + consumePurchase(userPurchase.purchaseToken) } } suspend fun detectUnfulfilledPurchase( + enrolledCourses: List, + purchaseVerified: (PurchaseFlowData) -> Unit, onSuccess: () -> Unit, onFailure: (IAPException) -> Unit, ) { if (isIAPEnabled) { preferencesManager.user?.id?.let { userId -> runCatching { - processUnfulfilledPurchase(userId) + processUnfulfilledPurchase(userId, enrolledCourses, purchaseVerified) }.onSuccess { if (it) { onSuccess() 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 0d5e0c16f..a296f3bdc 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 @@ -15,8 +15,8 @@ import org.openedx.core.utils.TimeUtils class IAPEventLogger( private val analytics: IAPAnalytics, - private val purchaseFlowData: PurchaseFlowData? = null, private val isSilentIAPFlow: Boolean? = null, + var purchaseFlowData: PurchaseFlowData? = null, ) { fun upgradeNowClickedEvent() { logIAPEvent(IAPAnalyticsEvent.IAP_UPGRADE_NOW_CLICKED) @@ -64,7 +64,7 @@ class IAPEventLogger( IAPRequestType.PRICE_CODE, IAPRequestType.NO_SKU_CODE, - -> { + -> { priceLoadErrorEvent(feedbackErrorMessage) } @@ -186,7 +186,7 @@ class IAPEventLogger( putAll(params) putAll(getIAPEventParams()) putAll(getUnfulfilledIAPEventParams()) - }, + } ) } } 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 78631b9b7..f97c725fc 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -45,6 +45,7 @@ import org.openedx.core.system.notifier.PushNotifier import org.openedx.core.system.notifier.UpdateCourseData import org.openedx.core.ui.WindowSize import org.openedx.core.utils.FileUtil +import org.openedx.dashboard.domain.CourseStatusFilter import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardRouter @@ -76,7 +77,7 @@ class DashboardGalleryViewModel( val uiMessage: SharedFlow get() = _uiMessage.asSharedFlow() - private val _updating = MutableStateFlow(false) + private val _updating = MutableStateFlow(false) val updating: StateFlow get() = _updating.asStateFlow() @@ -258,9 +259,17 @@ class DashboardGalleryViewModel( private fun detectUnfulfilledPurchase() { viewModelScope.launch(Dispatchers.IO) { + val enrolledCourses = + interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses iapInteractor.detectUnfulfilledPurchase( + enrolledCourses = enrolledCourses, + purchaseVerified = { purchaseFlowData -> + eventLogger.apply { + this.purchaseFlowData = purchaseFlowData + this.logUnfulfilledPurchaseInitiatedEvent() + } + }, onSuccess = { - eventLogger.logUnfulfilledPurchaseInitiatedEvent() _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) }, onFailure = { 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 bb54964ed..9084f50d7 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -45,6 +45,7 @@ import org.openedx.core.system.notifier.PushNotifier 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.dashboard.domain.CourseStatusFilter import org.openedx.dashboard.domain.interactor.DashboardInteractor @SuppressLint("StaticFieldLeak") @@ -308,9 +309,17 @@ class DashboardListViewModel( private fun detectUnfulfilledPurchase() { viewModelScope.launch(Dispatchers.IO) { + val enrolledCourses = + interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses iapInteractor.detectUnfulfilledPurchase( + enrolledCourses = enrolledCourses, + purchaseVerified = { purchaseFlowData -> + eventLogger.apply { + this.purchaseFlowData = purchaseFlowData + this.logUnfulfilledPurchaseInitiatedEvent() + } + }, onSuccess = { - eventLogger.logUnfulfilledPurchaseInitiatedEvent() _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) }, onFailure = { From 1bc2d8268413a5a5bdfbce7099fb282b522ce746 Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Mon, 13 Jan 2025 16:50:59 +0500 Subject: [PATCH 2/5] chore: add missing course properties in IAP restore purchases event fixes: LEARNER-10309 --- .../java/org/openedx/app/di/ScreenModule.kt | 1 + .../core/domain/interactor/IAPInteractor.kt | 2 +- .../core/system/notifier/app/AppNotifier.kt | 10 ++++++- .../notifier/app/EnrolledCourseEvent.kt | 5 ++++ .../app/RequestEnrolledCourseEvent.kt | 3 ++ .../presentation/DashboardGalleryViewModel.kt | 18 ++++++++++++ .../presentation/DashboardListViewModel.kt | 22 +++++++++----- .../presentation/settings/SettingsFragment.kt | 2 +- .../settings/SettingsViewModel.kt | 29 +++++++++++++++---- 9 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/system/notifier/app/EnrolledCourseEvent.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/app/RequestEnrolledCourseEvent.kt 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 57da53c2c..aae54b301 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -172,6 +172,7 @@ val screenModule = module { get(), get(), get(), + get(), windowSize, get(), ) 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 97ce0ca90..f23c46cb2 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 @@ -114,7 +114,7 @@ class IAPInteractor( suspend fun processUnfulfilledPurchase( userId: Long, - enrolledCourses: List = arrayListOf(), + enrolledCourses: List, purchaseVerified: (PurchaseFlowData) -> Unit = {}, ): Boolean { val purchases = billingProcessor.queryPurchases() diff --git a/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt index 804d84a65..80464be11 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt @@ -1,12 +1,17 @@ package org.openedx.core.system.notifier.app +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow class AppNotifier { - private val channel = MutableSharedFlow(replay = 0, extraBufferCapacity = 0) + private val channel = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) val notifier: Flow = channel.asSharedFlow() @@ -16,4 +21,7 @@ class AppNotifier { suspend fun send(event: AppUpgradeEvent) = channel.emit(event) + suspend fun send(event: EnrolledCourseEvent) = channel.emit(event) + + suspend fun send(event: RequestEnrolledCourseEvent) = channel.emit(event) } diff --git a/core/src/main/java/org/openedx/core/system/notifier/app/EnrolledCourseEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/app/EnrolledCourseEvent.kt new file mode 100644 index 000000000..bcd0d0de9 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/app/EnrolledCourseEvent.kt @@ -0,0 +1,5 @@ +package org.openedx.core.system.notifier.app + +import org.openedx.core.domain.model.EnrolledCourse + +class EnrolledCourseEvent(val enrolledCourses: List,) : AppEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/app/RequestEnrolledCourseEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/app/RequestEnrolledCourseEvent.kt new file mode 100644 index 000000000..09e2490e3 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/app/RequestEnrolledCourseEvent.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.app + +object RequestEnrolledCourseEvent : AppEvent 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 f97c725fc..5bb2e551e 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -43,6 +43,9 @@ import org.openedx.core.system.notifier.NavigationToDiscovery import org.openedx.core.system.notifier.PushEvent import org.openedx.core.system.notifier.PushNotifier import org.openedx.core.system.notifier.UpdateCourseData +import org.openedx.core.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.EnrolledCourseEvent +import org.openedx.core.system.notifier.app.RequestEnrolledCourseEvent import org.openedx.core.ui.WindowSize import org.openedx.core.utils.FileUtil import org.openedx.dashboard.domain.CourseStatusFilter @@ -61,6 +64,7 @@ class DashboardGalleryViewModel( private val dashboardRouter: DashboardRouter, private val iapNotifier: IAPNotifier, private val pushNotifier: PushNotifier, + private val appNotifier: AppNotifier, private val iapInteractor: IAPInteractor, private val windowSize: WindowSize, iapAnalytics: IAPAnalytics, @@ -97,11 +101,25 @@ class DashboardGalleryViewModel( private var isLoading = false init { + collectAppEvent() collectDiscoveryNotifier() collectIapNotifier() getCourses() } + private fun collectAppEvent() { + appNotifier.notifier + .onEach { + if (it is RequestEnrolledCourseEvent) { + val enrolledCourses = + interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses + appNotifier.send(EnrolledCourseEvent(enrolledCourses)) + } + } + .distinctUntilChanged() + .launchIn(viewModelScope) + } + fun getCourses(isIAPFlow: Boolean = false) { viewModelScope.launch { try { 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 9084f50d7..6dfa68613 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -45,6 +45,8 @@ import org.openedx.core.system.notifier.PushNotifier 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.system.notifier.app.EnrolledCourseEvent +import org.openedx.core.system.notifier.app.RequestEnrolledCourseEvent import org.openedx.dashboard.domain.CourseStatusFilter import org.openedx.dashboard.domain.interactor.DashboardInteractor @@ -125,7 +127,7 @@ class DashboardListViewModel( init { getCourses() - collectAppUpgradeEvent() + collectAppEvent() } fun getCourses() { @@ -293,14 +295,20 @@ class DashboardListViewModel( } } - private fun collectAppUpgradeEvent() { - viewModelScope.launch { - appNotifier.notifier.collect { event -> - if (event is AppUpgradeEvent) { - _appUpgradeEvent.value = event + private fun collectAppEvent() { + appNotifier.notifier + .onEach { + if (it is AppUpgradeEvent) { + _appUpgradeEvent.value = it + } + if (it is RequestEnrolledCourseEvent) { + val enrolledCourses = + interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses + appNotifier.send(EnrolledCourseEvent(enrolledCourses)) } } - } + .distinctUntilChanged() + .launchIn(viewModelScope) } fun dashboardCourseClickedEvent(courseId: String, courseName: String) { 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 f4bc7b2c6..66cc55244 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 @@ -102,7 +102,7 @@ class SettingsFragment : Fragment() { } SettingsScreenAction.RestorePurchaseClick -> { - viewModel.restorePurchase() + viewModel.restorePurchasesClicked() } SettingsScreenAction.FeedbackFormClick -> { 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 78b7d7495..ad1ac0a34 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 @@ -21,6 +21,7 @@ import org.openedx.core.UIMessage 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.exception.iap.IAPException import org.openedx.core.extension.isInternetError import org.openedx.core.module.DownloadWorkerController @@ -34,7 +35,9 @@ import org.openedx.core.system.AppCookieManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.system.notifier.app.AppUpgradeEvent +import org.openedx.core.system.notifier.app.EnrolledCourseEvent import org.openedx.core.system.notifier.app.LogoutEvent +import org.openedx.core.system.notifier.app.RequestEnrolledCourseEvent import org.openedx.core.utils.EmailUtil import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Configuration @@ -96,7 +99,7 @@ class SettingsViewModel( ) init { - collectAppUpgradeEvent() + collectAppEvent() collectProfileEvent() } @@ -128,12 +131,15 @@ class SettingsViewModel( } } - private fun collectAppUpgradeEvent() { + private fun collectAppEvent() { viewModelScope.launch { appNotifier.notifier.collect { event -> if (event is AppUpgradeEvent) { _appUpgradeEvent.value = event } + if (event is EnrolledCourseEvent) { + restorePurchase(event.enrolledCourses) + } } } } @@ -239,8 +245,14 @@ class SettingsViewModel( ) } - fun restorePurchase() { + fun restorePurchasesClicked() { eventLogger.logRestorePurchasesClickedEvent() + viewModelScope.launch { + appNotifier.send(RequestEnrolledCourseEvent) + } + } + + private fun restorePurchase(enrolledCourses: List) { viewModelScope.launch(Dispatchers.IO) { val userId = corePreferences.user?.id ?: return@launch @@ -249,10 +261,17 @@ class SettingsViewModel( delay(2000) runCatching { - iapInteractor.processUnfulfilledPurchase(userId) + iapInteractor.processUnfulfilledPurchase( + userId, + enrolledCourses, + purchaseVerified = { purchaseFlowData -> + eventLogger.apply { + this.purchaseFlowData = purchaseFlowData + this.logUnfulfilledPurchaseInitiatedEvent() + } + }) }.onSuccess { if (it) { - eventLogger.logUnfulfilledPurchaseInitiatedEvent() _iapUiState.emit(IAPUIState.PurchasesFulfillmentCompleted) } else { _iapUiState.emit(IAPUIState.FakePurchasesFulfillmentCompleted) From 3e7c5952d2953ba1e567d03c5ec741e3c1794f93 Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Mon, 20 Jan 2025 19:18:12 +0500 Subject: [PATCH 3/5] fix: address PR comments --- .../core/domain/interactor/IAPInteractor.kt | 37 ++++++++++--------- .../core/domain/model/CourseEnrollments.kt | 6 ++- .../core/module/billing/BillingProcessor.kt | 5 +++ .../core/presentation/iap/IAPEventLogger.kt | 2 +- .../core/presentation/iap/IAPViewModel.kt | 4 +- .../container/CourseContainerViewModel.kt | 1 - .../presentation/DashboardGalleryView.kt | 4 +- .../presentation/DashboardGalleryViewModel.kt | 8 +++- .../presentation/DashboardListViewModel.kt | 8 +++- .../settings/SettingsViewModel.kt | 8 ++-- 10 files changed, 51 insertions(+), 32 deletions(-) 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 f23c46cb2..e2eef51f0 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 @@ -14,14 +14,14 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.iap.ProductInfo import org.openedx.core.domain.model.iap.PurchaseFlowData import org.openedx.core.exception.iap.IAPException -import org.openedx.core.extension.decodeToLong -import org.openedx.core.extension.decodeToString 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.module.billing.getUserId import org.openedx.core.presentation.global.AppData import org.openedx.core.presentation.iap.IAPRequestType import org.openedx.core.utils.EmailUtil +import org.openedx.core.utils.TimeUtils class IAPInteractor( private val appData: AppData, @@ -115,19 +115,19 @@ class IAPInteractor( suspend fun processUnfulfilledPurchase( userId: Long, enrolledCourses: List, - purchaseVerified: (PurchaseFlowData) -> Unit = {}, - ): Boolean { + verificationInitiated: (PurchaseFlowData) -> Unit = {}, + ): PurchaseFlowData? { val purchases = billingProcessor.queryPurchases() val userPurchases = purchases.filter { purchase -> - val userAccountId = purchase.accountIdentifiers?.obfuscatedAccountId?.decodeToLong() - val storeSku = purchase.accountIdentifiers?.obfuscatedProfileId?.decodeToString() + val userAccountId = purchase.getUserId() + val courseSku = purchase.getCourseSku() userAccountId == userId && enrolledCourses.any { enrolledCourse -> - storeSku == enrolledCourse.productInfo?.courseSku + courseSku == enrolledCourse.productInfo?.courseSku } } if (userPurchases.isNotEmpty()) { - userPurchases.forEach { purchase -> + userPurchases[0].let { purchase -> val courseVerified = enrolledCourses.find { enrolledCourse -> enrolledCourse.productInfo?.courseSku == purchase.getCourseSku() } @@ -143,18 +143,19 @@ class IAPInteractor( this.price = it.getPriceAmount() this.currencyCode = it.priceCurrencyCode } + this.flowStartTime = TimeUtils.getCurrentTime() } + verificationInitiated(purchaseProductFlow) startUnfulfilledVerification(purchase) - purchaseVerified(purchaseProductFlow) + return purchaseProductFlow } } - return true } else { - purchases.forEach { + purchases.subtract(userPurchases.toSet()).forEach { billingProcessor.consumePurchase(it.purchaseToken) } } - return false + return null } private suspend fun startUnfulfilledVerification(userPurchase: Purchase) { @@ -177,17 +178,17 @@ class IAPInteractor( suspend fun detectUnfulfilledPurchase( enrolledCourses: List, - purchaseVerified: (PurchaseFlowData) -> Unit, - onSuccess: () -> Unit, + verificationInitiated: (PurchaseFlowData) -> Unit, + onSuccess: (PurchaseFlowData) -> Unit, onFailure: (IAPException) -> Unit, ) { if (isIAPEnabled) { preferencesManager.user?.id?.let { userId -> runCatching { - processUnfulfilledPurchase(userId, enrolledCourses, purchaseVerified) - }.onSuccess { - if (it) { - onSuccess() + processUnfulfilledPurchase(userId, enrolledCourses, verificationInitiated) + }.onSuccess { purchaseFlowData -> + purchaseFlowData?.let { + onSuccess(purchaseFlowData) } }.onFailure { if (it is IAPException) { diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseEnrollments.kt b/core/src/main/java/org/openedx/core/domain/model/CourseEnrollments.kt index 6606902c2..6ab336c3c 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseEnrollments.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseEnrollments.kt @@ -4,4 +4,8 @@ data class CourseEnrollments( val enrollments: DashboardCourseList, val configs: AppConfig, val primary: EnrolledCourse?, -) +) { + fun hasEnrolledCourses(): Boolean { + return primary != null || enrollments.courses.isNotEmpty() + } +} diff --git a/core/src/main/java/org/openedx/core/module/billing/BillingProcessor.kt b/core/src/main/java/org/openedx/core/module/billing/BillingProcessor.kt index dff7a717c..2c3d38ee1 100644 --- a/core/src/main/java/org/openedx/core/module/billing/BillingProcessor.kt +++ b/core/src/main/java/org/openedx/core/module/billing/BillingProcessor.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import org.openedx.core.domain.model.iap.ProductInfo +import org.openedx.core.extension.decodeToLong import org.openedx.core.extension.decodeToString import org.openedx.core.extension.encodeToString import org.openedx.core.extension.safeResume @@ -203,3 +204,7 @@ fun ProductDetails.OneTimePurchaseOfferDetails.getPriceAmount(): Double = fun Purchase.getCourseSku(): String? { return this.accountIdentifiers?.obfuscatedProfileId?.decodeToString() } + +fun Purchase.getUserId(): Long? { + return this.accountIdentifiers?.obfuscatedAccountId?.decodeToLong() +} 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 a296f3bdc..6947ccf85 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 @@ -15,7 +15,7 @@ import org.openedx.core.utils.TimeUtils class IAPEventLogger( private val analytics: IAPAnalytics, - private val isSilentIAPFlow: Boolean? = null, + val isSilentIAPFlow: Boolean? = null, var purchaseFlowData: PurchaseFlowData? = null, ) { fun upgradeNowClickedEvent() { 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 981e6b107..7561475b2 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 @@ -84,7 +84,9 @@ class IAPViewModel( iapNotifier.notifier.onEach { event -> when (event) { is CourseDataUpdated -> { - eventLogger.upgradeSuccessEvent() + if (eventLogger.isSilentIAPFlow == null) { + eventLogger.upgradeSuccessEvent() + } _uiMessage.emit(UIMessage.ToastMessage(resourceManager.getString(R.string.iap_success_message))) _uiState.value = IAPUIState.CourseDataUpdated } 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 1f212af6c..f90966ca2 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 @@ -286,7 +286,6 @@ class CourseContainerViewModel( } if (isIAPFlow) { if (isExpiredCoursePurchase) { - eventLogger.upgradeSuccessEvent() _uiMessage.emit( UIMessage.ToastMessage( resourceManager.getString(CoreR.string.iap_success_message) 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 7958694e9..7106721e4 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -263,8 +263,8 @@ private fun DashboardGalleryView( }, onIAPAction = onIAPAction, ) - LaunchedEffect(uiState.userCourses.enrollments.courses) { - if (uiState.userCourses.enrollments.courses.isNotEmpty()) { + LaunchedEffect(uiState.userCourses.hasEnrolledCourses()) { + if (uiState.userCourses.hasEnrolledCourses()) { onIAPAction(IAPAction.ACTION_UNFULFILLED, null, null) } } 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 5bb2e551e..10c259d38 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -281,13 +281,17 @@ class DashboardGalleryViewModel( interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses iapInteractor.detectUnfulfilledPurchase( enrolledCourses = enrolledCourses, - purchaseVerified = { purchaseFlowData -> + verificationInitiated = { purchaseFlowData -> eventLogger.apply { this.purchaseFlowData = purchaseFlowData this.logUnfulfilledPurchaseInitiatedEvent() } }, - onSuccess = { + onSuccess = { purchaseFlowData -> + eventLogger.apply { + this.purchaseFlowData = purchaseFlowData + eventLogger.upgradeSuccessEvent() + } _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) }, onFailure = { 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 6dfa68613..8966913cc 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -321,13 +321,17 @@ class DashboardListViewModel( interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses iapInteractor.detectUnfulfilledPurchase( enrolledCourses = enrolledCourses, - purchaseVerified = { purchaseFlowData -> + verificationInitiated = { purchaseFlowData -> eventLogger.apply { this.purchaseFlowData = purchaseFlowData this.logUnfulfilledPurchaseInitiatedEvent() } }, - onSuccess = { + onSuccess = { purchaseFlowData -> + eventLogger.apply { + this.purchaseFlowData = purchaseFlowData + eventLogger.upgradeSuccessEvent() + } _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) }, onFailure = { 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 ad1ac0a34..d22664403 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 @@ -264,16 +264,16 @@ class SettingsViewModel( iapInteractor.processUnfulfilledPurchase( userId, enrolledCourses, - purchaseVerified = { purchaseFlowData -> + verificationInitiated = { purchaseFlowData -> eventLogger.apply { this.purchaseFlowData = purchaseFlowData this.logUnfulfilledPurchaseInitiatedEvent() } }) - }.onSuccess { - if (it) { + }.onSuccess { purchaseFlowData -> + purchaseFlowData?.let { _iapUiState.emit(IAPUIState.PurchasesFulfillmentCompleted) - } else { + } ?: run { _iapUiState.emit(IAPUIState.FakePurchasesFulfillmentCompleted) } }.onFailure { From d9e15e1b4092aaf052dcdbf1d7b011f5c0970ed9 Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Mon, 27 Jan 2025 13:29:36 +0500 Subject: [PATCH 4/5] fix: address PR comments-2 --- .../core/domain/interactor/IAPInteractor.kt | 4 ++-- .../core/domain/model/iap/PurchaseFlowData.kt | 14 ++++++++++++++ .../core/presentation/iap/IAPEventLogger.kt | 2 +- .../openedx/core/presentation/iap/IAPViewModel.kt | 6 ++++-- .../container/CourseContainerViewModel.kt | 14 ++++++++++---- .../presentation/DashboardGalleryViewModel.kt | 2 +- .../presentation/DashboardListViewModel.kt | 2 +- .../presentation/settings/SettingsViewModel.kt | 4 ++++ 8 files changed, 37 insertions(+), 11 deletions(-) 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 e2eef51f0..d9af0ffa4 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 @@ -127,7 +127,7 @@ class IAPInteractor( } } if (userPurchases.isNotEmpty()) { - userPurchases[0].let { purchase -> + userPurchases.first().let { purchase -> val courseVerified = enrolledCourses.find { enrolledCourse -> enrolledCourse.productInfo?.courseSku == purchase.getCourseSku() } @@ -151,7 +151,7 @@ class IAPInteractor( } } } else { - purchases.subtract(userPurchases.toSet()).forEach { + purchases.forEach { billingProcessor.consumePurchase(it.purchaseToken) } } 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 48233d299..c6fc65885 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 @@ -37,6 +37,20 @@ data class PurchaseFlowData( basketId = -1 flowStartTime = 0 } + + fun isSilentIAPFlow(): Boolean? { + return when (iapFlow) { + IAPFlow.SILENT -> { + true + } + IAPFlow.RESTORE -> { + false + } + else -> { + null + } + } + } } enum class IAPFlow(val value: String) { 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 6947ccf85..301a43cb6 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 @@ -15,7 +15,7 @@ import org.openedx.core.utils.TimeUtils class IAPEventLogger( private val analytics: IAPAnalytics, - val isSilentIAPFlow: Boolean? = null, + var isSilentIAPFlow: Boolean? = null, var purchaseFlowData: PurchaseFlowData? = null, ) { fun upgradeNowClickedEvent() { 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 7561475b2..0233d59fd 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 @@ -24,6 +24,7 @@ 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.isNull import org.openedx.core.module.billing.BillingProcessor import org.openedx.core.module.billing.getCourseSku import org.openedx.core.module.billing.getPriceAmount @@ -55,6 +56,7 @@ class IAPViewModel( val eventLogger = IAPEventLogger( analytics = analytics, + isSilentIAPFlow = purchaseData.isSilentIAPFlow(), purchaseFlowData = purchaseData ) @@ -84,10 +86,10 @@ class IAPViewModel( iapNotifier.notifier.onEach { event -> when (event) { is CourseDataUpdated -> { - if (eventLogger.isSilentIAPFlow == null) { + if (eventLogger.isSilentIAPFlow.isNull()) { eventLogger.upgradeSuccessEvent() + _uiMessage.emit(UIMessage.ToastMessage(resourceManager.getString(R.string.iap_success_message))) } - _uiMessage.emit(UIMessage.ToastMessage(resourceManager.getString(R.string.iap_success_message))) _uiState.value = IAPUIState.CourseDataUpdated } } 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 f90966ca2..ddfe05c94 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 @@ -39,6 +39,7 @@ import org.openedx.core.domain.model.iap.PurchaseFlowData import org.openedx.core.exception.iap.IAPException import org.openedx.core.extension.isFalse import org.openedx.core.extension.isNotNull +import org.openedx.core.extension.isNull import org.openedx.core.extension.isTrue import org.openedx.core.module.billing.BillingProcessor import org.openedx.core.module.billing.getCourseSku @@ -286,11 +287,16 @@ class CourseContainerViewModel( } if (isIAPFlow) { if (isExpiredCoursePurchase) { - _uiMessage.emit( - UIMessage.ToastMessage( - resourceManager.getString(CoreR.string.iap_success_message) + if (eventLogger.isSilentIAPFlow.isNull()) { + eventLogger.upgradeSuccessEvent() + _uiMessage.emit( + UIMessage.ToastMessage( + resourceManager.getString( + CoreR.string.iap_success_message + ) + ) ) - ) + } } else { iapNotifier.send(CourseDataUpdated()) } 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 10c259d38..d42372325 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -290,7 +290,7 @@ class DashboardGalleryViewModel( onSuccess = { purchaseFlowData -> eventLogger.apply { this.purchaseFlowData = purchaseFlowData - eventLogger.upgradeSuccessEvent() + this.upgradeSuccessEvent() } _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) }, 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 8966913cc..fd8028428 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -330,7 +330,7 @@ class DashboardListViewModel( onSuccess = { purchaseFlowData -> eventLogger.apply { this.purchaseFlowData = purchaseFlowData - eventLogger.upgradeSuccessEvent() + this.upgradeSuccessEvent() } _iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted) }, 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 d22664403..95b3770e5 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 @@ -272,6 +272,10 @@ class SettingsViewModel( }) }.onSuccess { purchaseFlowData -> purchaseFlowData?.let { + eventLogger.apply { + this.purchaseFlowData = purchaseFlowData + this.upgradeSuccessEvent() + } _iapUiState.emit(IAPUIState.PurchasesFulfillmentCompleted) } ?: run { _iapUiState.emit(IAPUIState.FakePurchasesFulfillmentCompleted) From 2acc478446cb56758eb0181e4c85f089eb6108d4 Mon Sep 17 00:00:00 2001 From: Farhan Arshad Date: Mon, 27 Jan 2025 15:21:38 +0500 Subject: [PATCH 5/5] fix: black screen issue while refreshing the course data --- .../openedx/core/presentation/iap/IAPViewModel.kt | 2 +- .../container/CourseContainerViewModel.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) 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 0233d59fd..2e8504b77 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 @@ -88,8 +88,8 @@ class IAPViewModel( is CourseDataUpdated -> { if (eventLogger.isSilentIAPFlow.isNull()) { eventLogger.upgradeSuccessEvent() - _uiMessage.emit(UIMessage.ToastMessage(resourceManager.getString(R.string.iap_success_message))) } + _uiMessage.emit(UIMessage.ToastMessage(resourceManager.getString(R.string.iap_success_message))) _uiState.value = IAPUIState.CourseDataUpdated } } 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 ddfe05c94..f72291493 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 @@ -289,14 +289,14 @@ class CourseContainerViewModel( if (isExpiredCoursePurchase) { if (eventLogger.isSilentIAPFlow.isNull()) { eventLogger.upgradeSuccessEvent() - _uiMessage.emit( - UIMessage.ToastMessage( - resourceManager.getString( - CoreR.string.iap_success_message - ) + } + _uiMessage.emit( + UIMessage.ToastMessage( + resourceManager.getString( + CoreR.string.iap_success_message ) ) - } + ) } else { iapNotifier.send(CourseDataUpdated()) }