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 f885d07e3..0075bf492 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -4,7 +4,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module import org.openedx.app.AppViewModel -import org.openedx.app.BuildConfig import org.openedx.app.MainViewModel import org.openedx.auth.data.repository.AuthRepository import org.openedx.auth.domain.interactor.AuthInteractor @@ -140,7 +139,7 @@ val screenModule = module { factory { DashboardInteractor(get()) } viewModel { DashboardListViewModel( - versionName = BuildConfig.VERSION_NAME, + get(), get(), get(), get(), @@ -202,7 +201,6 @@ val screenModule = module { viewModel { (username: String) -> AnothersProfileViewModel(get(), get(), username) } viewModel { SettingsViewModel( - versionName = BuildConfig.VERSION_NAME, get(), get(), get(), @@ -254,7 +252,7 @@ val screenModule = module { courseTitle, resumeBlockId, enrollmentMode, - versionName = BuildConfig.VERSION_NAME, + get(), get(), get(), get(), @@ -447,7 +445,7 @@ val screenModule = module { IAPViewModel( iapFlow = iapFlow, purchaseFlowData = purchaseFlowData, - versionName = BuildConfig.VERSION_NAME, + get(), get(), get(), get(), diff --git a/core/src/main/java/org/openedx/core/data/repository/iap/IAPRepository.kt b/core/src/main/java/org/openedx/core/data/repository/iap/IAPRepository.kt index 313346a62..a4b28600a 100644 --- a/core/src/main/java/org/openedx/core/data/repository/iap/IAPRepository.kt +++ b/core/src/main/java/org/openedx/core/data/repository/iap/IAPRepository.kt @@ -18,7 +18,11 @@ class IAPRepository(private val api: InAppPurchasesApi) { return mapToDomain() } } - throw IAPException(IAPRequestType.ADD_TO_BASKET_CODE, response.code(), response.getMessage()) + throw IAPException( + requestType = IAPRequestType.ADD_TO_BASKET_CODE, + httpErrorCode = response.code(), + errorMessage = response.getMessage() + ) } suspend fun proceedCheckout(basketId: Long): CheckoutResponse { @@ -31,7 +35,11 @@ class IAPRepository(private val api: InAppPurchasesApi) { return mapToDomain() } } - throw IAPException(IAPRequestType.CHECKOUT_CODE, response.code(), response.getMessage()) + throw IAPException( + requestType = IAPRequestType.CHECKOUT_CODE, + httpErrorCode = response.code(), + errorMessage = response.getMessage() + ) } suspend fun executeOrder( @@ -53,6 +61,10 @@ class IAPRepository(private val api: InAppPurchasesApi) { return mapToDomain() } } - throw IAPException(IAPRequestType.EXECUTE_ORDER_CODE, response.code(), response.getMessage()) + throw IAPException( + requestType = IAPRequestType.EXECUTE_ORDER_CODE, + httpErrorCode = response.code(), + errorMessage = response.getMessage() + ) } } 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 208427061..c456f2d29 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 @@ -2,7 +2,7 @@ package org.openedx.core.domain.interactor import android.text.TextUtils import androidx.fragment.app.FragmentActivity -import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.Purchase import org.openedx.core.ApiConstants @@ -20,12 +20,20 @@ class IAPInteractor( private val repository: IAPRepository, ) { suspend fun loadPrice(productId: String): ProductDetails.OneTimePurchaseOfferDetails { - val response = - billingProcessor.querySyncDetails(productId) - val productDetail = response.productDetailsList?.firstOrNull() + val response = billingProcessor.querySyncDetails(productId) + val productDetails = response.productDetailsList?.firstOrNull()?.oneTimePurchaseOfferDetails val billingResult = response.billingResult - if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && productDetail?.oneTimePurchaseOfferDetails != null) { - return productDetail.oneTimePurchaseOfferDetails!! + + if (billingResult.responseCode == BillingResponseCode.OK) { + if (productDetails != null) { + return productDetails + } else { + throw IAPException( + requestType = IAPRequestType.NO_SKU_CODE, + httpErrorCode = billingResult.responseCode, + errorMessage = billingResult.debugMessage + ) + } } else { throw IAPException( requestType = IAPRequestType.PRICE_CODE, @@ -71,8 +79,12 @@ class IAPInteractor( suspend fun consumePurchase(purchaseToken: String) { val result = billingProcessor.consumePurchase(purchaseToken) - if (result.responseCode != BillingClient.BillingResponseCode.OK) { - throw IAPException(IAPRequestType.CONSUME_CODE, result.responseCode, result.debugMessage) + if (result.responseCode != BillingResponseCode.OK) { + throw IAPException( + requestType = IAPRequestType.CONSUME_CODE, + httpErrorCode = result.responseCode, + errorMessage = result.debugMessage + ) } } @@ -98,7 +110,8 @@ class IAPInteractor( productDetail?.oneTimePurchaseOfferDetails?.takeIf { TextUtils.isEmpty(purchase.getCourseSku()).not() }?.let { oneTimeProductDetails -> - val basketId = addToBasket(purchase.getCourseSku()!!) + val courseSku = purchase.getCourseSku() ?: return@let + val basketId = addToBasket(courseSku) processCheckout(basketId) executeOrder( basketId = basketId, diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseStructure.kt b/core/src/main/java/org/openedx/core/domain/model/CourseStructure.kt index b98543361..5869bf222 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseStructure.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseStructure.kt @@ -25,6 +25,7 @@ data class CourseStructure( ) { private val isStarted: Boolean get() = TimeUtils.isDatePassed(Date(), start) + val isUpgradeable: Boolean get() = enrollmentDetails.isAuditMode && isStarted && diff --git a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt index 265361b03..1a785015b 100644 --- a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt +++ b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt @@ -21,18 +21,10 @@ data class EnrolledCourse( private val isAuditMode: Boolean get() = EnrollmentMode.AUDIT.toString().equals(mode, ignoreCase = true) + val isUpgradeable: Boolean get() = isAuditMode && course.isStarted && course.isUpgradeDeadlinePassed.not() && productInfo != null } - -/** - * Method to filter the audit courses from the given enrolled course list. - * - * @return the list of all audit courses with non-null Skus. - */ -fun List.getAuditCourses(): List { - return this.filter { it.isUpgradeable }.toList() -} diff --git a/core/src/main/java/org/openedx/core/domain/model/EnrollmentMode.kt b/core/src/main/java/org/openedx/core/domain/model/EnrollmentMode.kt index 4803e453b..08df4208b 100644 --- a/core/src/main/java/org/openedx/core/domain/model/EnrollmentMode.kt +++ b/core/src/main/java/org/openedx/core/domain/model/EnrollmentMode.kt @@ -4,9 +4,9 @@ package org.openedx.core.domain.model * Course Enrollment modes */ enum class EnrollmentMode(private val mode: String) { - AUDIT("audit"), VERIFIED("verified"), HONOR("honor"), - NO_ID_PROFESSIONAL("no-id-professional"), PROFESSIONAL("professional"), - CREDIT("credit"), MASTERS("masters"), NONE("none"); + AUDIT("audit"), + VERIFIED("verified"), + NONE("none"); override fun toString(): String { return mode 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 b452e4445..554fbc5b8 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 @@ -1,15 +1,19 @@ package org.openedx.core.domain.model.iap +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import org.openedx.core.domain.ProductInfo +@Parcelize data class PurchaseFlowData( - val screenName: String? = null, - val courseId: String? = null, - val courseName: String? = null, - val isSelfPaced: Boolean? = null, - val componentId: String? = null, - val productInfo: ProductInfo? = null, -) { + var screenName: String? = null, + var courseId: String? = null, + var courseName: String? = null, + var isSelfPaced: Boolean? = null, + var componentId: String? = null, + var productInfo: ProductInfo? = null, +) : Parcelable { + var currencyCode: String = "" var price: Double = 0.0 var formattedPrice: String? = null @@ -17,4 +21,19 @@ data class PurchaseFlowData( var basketId: Long = -1 var flowStartTime: Long = 0 + + fun reset() { + screenName = null + courseId = null + courseName = null + isSelfPaced = null + componentId = null + productInfo = null + currencyCode = "" + price = 0.0 + formattedPrice = null + purchaseToken = null + basketId = -1 + flowStartTime = 0 + } } diff --git a/core/src/main/java/org/openedx/core/exception/iap/IAPException.kt b/core/src/main/java/org/openedx/core/exception/iap/IAPException.kt index 835f1c896..1491b1503 100644 --- a/core/src/main/java/org/openedx/core/exception/iap/IAPException.kt +++ b/core/src/main/java/org/openedx/core/exception/iap/IAPException.kt @@ -19,7 +19,7 @@ import java.util.Locale * */ class IAPException( val requestType: IAPRequestType = IAPRequestType.UNKNOWN, - val httpErrorCode: Int = -1, + val httpErrorCode: Int = DEFAULT_HTTP_ERROR_CODE, val errorMessage: String ) : Exception(errorMessage) { @@ -36,13 +36,17 @@ class IAPException( } body.append(String.format("%s", requestType.request)) // change the default value to -1 cuz in case of BillingClient return errorCode 0 for price load. - if (httpErrorCode == -1) { + if (httpErrorCode == DEFAULT_HTTP_ERROR_CODE) { return body.toString() } body.append(String.format(Locale.ENGLISH, "-%d", httpErrorCode)) if (!TextUtils.isEmpty(errorMessage)) body.append(String.format("-%s", errorMessage)) return body.toString() } + + companion object { + private const val DEFAULT_HTTP_ERROR_CODE = -1 + } } /** 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 1ead717a4..f0cde1550 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 @@ -171,12 +171,6 @@ class BillingProcessor( return result.billingResult } - fun release() { - if (billingClient.isReady) { - billingClient.endConnection() - } - } - /** * Method to query the Purchases async and returns purchases for currently owned items * bought within the app. 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 e60880941..68bd4f7c2 100644 --- a/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt +++ b/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt @@ -14,13 +14,22 @@ enum class IAPAnalyticsEvent(val eventName: String, val biValue: String) { "Payments: Course Upgrade Success", "edx.bi.app.payments.course_upgrade_success" ), - IAP_PAYMENT_ERROR("Payments: Payment Error", "edx.bi.app.payments.payment_error"), - IAP_PAYMENT_CANCELED("Payments: Canceled by User", "edx.bi.app.payments.canceled_by_user"), + IAP_PAYMENT_ERROR( + "Payments: Payment Error", + "edx.bi.app.payments.payment_error" + ), + IAP_PAYMENT_CANCELED( + "Payments: Canceled by User", + "edx.bi.app.payments.canceled_by_user" + ), IAP_COURSE_UPGRADE_ERROR( "Payments: Course Upgrade Error", "edx.bi.app.payments.course_upgrade_error" ), - IAP_PRICE_LOAD_ERROR("Payments: Price Load Error", "edx.bi.app.payments.price_load_error"), + IAP_PRICE_LOAD_ERROR( + "Payments: Price Load Error", + "edx.bi.app.payments.price_load_error" + ), IAP_ERROR_ALERT_ACTION( "Payments: Error Alert Action", "edx.bi.app.payments.error_alert_action" 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 387fc176a..e227be4e9 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 @@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment +import com.android.billingclient.api.BillingClient import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.R @@ -46,8 +47,10 @@ import org.openedx.core.presentation.iap.IAPRequestType import org.openedx.core.presentation.iap.IAPUIState import org.openedx.core.presentation.iap.IAPViewModel import org.openedx.core.ui.CourseAlreadyPurchasedErrorDialog +import org.openedx.core.ui.CourseAlreadyPurchasedExecuteErrorDialog import org.openedx.core.ui.GeneralUpgradeErrorDialog import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.NoSkuErrorDialog import org.openedx.core.ui.OpenEdXButton import org.openedx.core.ui.PriceLoadErrorDialog import org.openedx.core.ui.UnlockingAccessView @@ -60,14 +63,8 @@ class IAPDialogFragment : DialogFragment() { private val iapViewModel by viewModel { parametersOf( - requireArguments().serializable(ARG_IAP_FLOW), PurchaseFlowData( - screenName = requireArguments().getString(ARG_SCREEN_NAME, ""), - courseId = requireArguments().getString(ARG_COURSE_ID, ""), - courseName = requireArguments().getString(ARG_COURSE_NAME, ""), - isSelfPaced = requireArguments().getBoolean(ARG_SELF_PACES, false), - componentId = requireArguments().getString(ARG_COMPONENT_ID, ""), - productInfo = requireArguments().parcelable(ARG_PRODUCT_INFO) - ) + requireArguments().serializable(ARG_IAP_FLOW), + requireArguments().parcelable(ARG_PURCHASE_FLOW_DATA) ) } @@ -76,7 +73,7 @@ class IAPDialogFragment : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ) = ComposeView(requireContext()).apply { - dialog!!.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { @@ -85,9 +82,10 @@ class IAPDialogFragment : DialogFragment() { val scaffoldState = rememberScaffoldState() val isFullScreenLoader = - iapState is IAPUIState.Loading && (iapState as IAPUIState.Loading).loaderType == IAPLoaderType.FULL_SCREEN + (iapState as? IAPUIState.Loading)?.loaderType == IAPLoaderType.FULL_SCREEN - Scaffold(modifier = Modifier.fillMaxSize(), + Scaffold( + modifier = Modifier.fillMaxSize(), backgroundColor = MaterialTheme.appColors.background, topBar = { if (isFullScreenLoader.not()) { @@ -120,12 +118,12 @@ class IAPDialogFragment : DialogFragment() { } iapState is IAPUIState.ProductData && TextUtils.isEmpty( - iapViewModel.purchaseFlowData.formattedPrice + iapViewModel.purchaseData.formattedPrice ).not() -> { OpenEdXButton(modifier = Modifier.fillMaxWidth(), text = stringResource( id = R.string.iap_upgrade_price, - iapViewModel.purchaseFlowData.formattedPrice!!, + iapViewModel.purchaseData.formattedPrice!!, ), onClick = { iapViewModel.startPurchaseFlow() @@ -134,7 +132,8 @@ class IAPDialogFragment : DialogFragment() { } } } - }) { contentPadding -> + } + ) { contentPadding -> HandleUIMessage( uiMessage = uiMessage, @@ -143,7 +142,8 @@ class IAPDialogFragment : DialogFragment() { if (iapState is IAPUIState.CourseDataUpdated) { onDismiss() } - }) + } + ) when (iapState) { is IAPUIState.PurchaseProduct -> { @@ -169,7 +169,19 @@ class IAPDialogFragment : DialogFragment() { }) } - (iapException.httpErrorCode == 406) -> { + (iapException.requestType == IAPRequestType.NO_SKU_CODE) -> { + NoSkuErrorDialog(onConfirm = { + iapViewModel.logIAPErrorActionEvent( + iapException.requestType.request, + IAPAction.ACTION_OK.action + ) + onDismiss() + }) + } + + (iapException.requestType == IAPRequestType.ADD_TO_BASKET_CODE || + iapException.requestType == IAPRequestType.CHECKOUT_CODE) && + (iapException.httpErrorCode == 406) -> { CourseAlreadyPurchasedErrorDialog( onRefresh = { iapViewModel.logIAPErrorActionEvent( @@ -178,14 +190,6 @@ class IAPDialogFragment : DialogFragment() { ) iapViewModel.refreshCourse() }, - onGetHelp = { - iapViewModel.showFeedbackScreen( - requireActivity(), - iapException.requestType.request, - iapException.getFormattedErrorMessage() - ) - onDismiss() - }, onDismiss = { iapViewModel.logIAPErrorActionEvent( iapException.requestType.request, @@ -196,18 +200,106 @@ class IAPDialogFragment : DialogFragment() { } (iapException.requestType == IAPRequestType.EXECUTE_ORDER_CODE) -> { - UpgradeErrorDialog( - title = stringResource(id = R.string.iap_error_title), - description = stringResource(id = R.string.iap_course_not_fullfilled), - confirmText = stringResource(id = R.string.core_error_try_again), - onConfirm = { + if (iapException.httpErrorCode == 409) { + UpgradeErrorDialog( + title = stringResource(id = R.string.iap_error_title), + description = stringResource(id = R.string.iap_course_already_paid_for_message), + confirmText = stringResource(id = R.string.core_cancel), + onConfirm = { + iapViewModel.logIAPErrorActionEvent( + iapException.requestType.request, + IAPAction.ACTION_CLOSE.action + ) + dismiss() + }, + dismissText = stringResource(id = R.string.iap_get_help), + onDismiss = { + iapViewModel.showFeedbackScreen( + requireActivity(), + iapException.requestType.request, + iapException.getFormattedErrorMessage() + ) + onDismiss() + } + ) + } else { + val confirmText = when (iapException.httpErrorCode) { + 406 -> { + stringResource(id = R.string.iap_label_refresh_now) + } + + else -> { + stringResource(id = R.string.iap_refresh_to_retry) + } + } + + val description = when (iapException.httpErrorCode) { + 400, 403 -> { + stringResource(id = R.string.iap_course_not_fullfilled) + } + + 406 -> { + stringResource(id = R.string.iap_course_already_paid_for_message) + } + + else -> { + stringResource(id = R.string.iap_general_upgrade_error_message) + } + } + CourseAlreadyPurchasedExecuteErrorDialog( + confirmText = confirmText, + description = description, + onRefresh = { + if (iapException.httpErrorCode == 406) { + iapViewModel.logIAPErrorActionEvent( + iapException.requestType.request, + IAPAction.ACTION_REFRESH.action + ) + iapViewModel.refreshCourse() + } else { + iapViewModel.logIAPErrorActionEvent( + iapException.requestType.request, + IAPAction.ACTION_RETRY.action + ) + iapViewModel.retryExecuteOrder() + } + }, + onGetHelp = { + iapViewModel.showFeedbackScreen( + requireActivity(), + iapException.requestType.request, + iapException.getFormattedErrorMessage() + ) + onDismiss() + }, + onDismiss = { + iapViewModel.logIAPErrorActionEvent( + iapException.requestType.request, + IAPAction.ACTION_CLOSE.action + ) + onDismiss() + } + ) + } + } + + iapException.requestType == IAPRequestType.CONSUME_CODE -> { + CourseAlreadyPurchasedExecuteErrorDialog( + onRefresh = { iapViewModel.logIAPErrorActionEvent( iapException.requestType.request, IAPAction.ACTION_RETRY.action ) - iapViewModel.retryExecuteOrder() + iapViewModel.retryToConsumeOrder() + }, + onGetHelp = { + iapViewModel.showFeedbackScreen( + requireActivity(), + iapException.requestType.request, + iapException.getFormattedErrorMessage() + ) + onDismiss() }, - dismissText = stringResource(id = R.string.core_cancel), onDismiss = { iapViewModel.logIAPErrorActionEvent( iapException.requestType.request, @@ -225,7 +317,16 @@ class IAPDialogFragment : DialogFragment() { } (iapException.httpErrorCode == 400) -> { - stringResource(id = R.string.iap_course_not_available_message) + if (iapException.requestType == IAPRequestType.CHECKOUT_CODE) { + stringResource(id = R.string.iap_payment_could_not_be_processed) + } else { + stringResource(id = R.string.iap_course_not_available_message) + } + } + + (iapException.requestType == IAPRequestType.PAYMENT_SDK_CODE && + iapException.httpErrorCode == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) -> { + stringResource(id = R.string.iap_payment_could_not_be_processed) } else -> { @@ -262,10 +363,10 @@ class IAPDialogFragment : DialogFragment() { if (isFullScreenLoader) { UnlockingAccessView() - } else if (TextUtils.isEmpty(iapViewModel.purchaseFlowData.courseName).not()) { + } else if (TextUtils.isEmpty(iapViewModel.purchaseData.courseName).not()) { ValuePropUpgradeFeatures( Modifier.padding(contentPadding), - iapViewModel.purchaseFlowData.courseName!! + iapViewModel.purchaseData.courseName!! ) } } @@ -287,12 +388,7 @@ class IAPDialogFragment : DialogFragment() { const val TAG = "IAPDialogFragment" private const val ARG_IAP_FLOW = "iap_flow" - private const val ARG_SCREEN_NAME = "SCREEN_NAME" - private const val ARG_COURSE_ID = "course_id" - private const val ARG_COURSE_NAME = "course_name" - private const val ARG_SELF_PACES = "self_paces" - private const val ARG_COMPONENT_ID = "component_id" - private const val ARG_PRODUCT_INFO = "product_info" + private const val ARG_PURCHASE_FLOW_DATA = "purchase_flow_data" fun newInstance( iapFlow: IAPFlow, @@ -304,14 +400,18 @@ class IAPDialogFragment : DialogFragment() { productInfo: ProductInfo? = null ): IAPDialogFragment { val fragment = IAPDialogFragment() + val purchaseFlowData = PurchaseFlowData().apply { + this.screenName = screenName + this.courseId = courseId + this.courseName = courseName + this.isSelfPaced = isSelfPaced + this.componentId = componentId + this.productInfo = productInfo + } + fragment.arguments = bundleOf( ARG_IAP_FLOW to iapFlow, - ARG_SCREEN_NAME to screenName, - ARG_COURSE_ID to courseId, - ARG_COURSE_NAME to courseName, - ARG_SELF_PACES to isSelfPaced, - ARG_COMPONENT_ID to componentId, - ARG_PRODUCT_INFO to productInfo + ARG_PURCHASE_FLOW_DATA to purchaseFlowData ) return fragment } 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 c92645119..a19b62e34 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 @@ -10,7 +10,6 @@ sealed class IAPUIState { data object CourseDataUpdated : IAPUIState() data class Loading(val loaderType: IAPLoaderType) : IAPUIState() data class Error(val iapException: IAPException) : IAPUIState() - data object Clear : IAPUIState() } @@ -35,10 +34,12 @@ enum class IAPAction(val action: String) { ACTION_RELOAD_PRICE("reload_price"), ACTION_REFRESH("refresh"), ACTION_RETRY("retry"), - ACTION_UNFULFILLED("Unfulfilled"), + ACTION_UNFULFILLED("unfulfilled"), ACTION_RESTORE("restore"), ACTION_ERROR_CLOSE("error_close"), - ACTION_COMPLETION("completion") + ACTION_COMPLETION("completion"), + ACTION_OK("ok"), + ACTION_RESTORE_PURCHASE_CANCEL("restore_purchase_cancel") } enum class IAPRequestType(val request: 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 636028421..dcdde987c 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 @@ -30,17 +30,18 @@ import org.openedx.core.module.billing.getPriceAmount import org.openedx.core.presentation.IAPAnalytics import org.openedx.core.presentation.IAPAnalyticsEvent import org.openedx.core.presentation.IAPAnalyticsKeys +import org.openedx.core.presentation.global.AppData import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.CourseDataUpdated import org.openedx.core.system.notifier.IAPNotifier import org.openedx.core.system.notifier.UpdateCourseData import org.openedx.core.utils.EmailUtil -import java.util.Calendar +import org.openedx.core.utils.TimeUtils class IAPViewModel( iapFlow: IAPFlow, - var purchaseFlowData: PurchaseFlowData, - private val versionName: String, + private val purchaseFlowData: PurchaseFlowData, + private val appData: AppData, private val iapInteractor: IAPInteractor, private val corePreferences: CorePreferences, private val analytics: IAPAnalytics, @@ -49,8 +50,7 @@ class IAPViewModel( private val iapNotifier: IAPNotifier ) : BaseViewModel() { - private val _uiState = - MutableStateFlow(IAPUIState.Loading(IAPLoaderType.PRICE)) + private val _uiState = MutableStateFlow(IAPUIState.Loading(IAPLoaderType.PRICE)) val uiState: StateFlow get() = _uiState.asStateFlow() @@ -58,6 +58,9 @@ class IAPViewModel( val uiMessage: SharedFlow get() = _uiMessage.asSharedFlow() + val purchaseData: PurchaseFlowData + get() = purchaseFlowData + private val purchaseListeners = object : BillingProcessor.PurchaseListeners { override fun onPurchaseComplete(purchase: Purchase) { if (purchase.getCourseSku() == purchaseFlowData.productInfo?.courseSku) { @@ -99,39 +102,12 @@ class IAPViewModel( IAPFlow.SILENT, IAPFlow.RESTORE -> { _uiState.value = IAPUIState.Loading(IAPLoaderType.FULL_SCREEN) - purchaseFlowData.flowStartTime = getCurrentTime() + purchaseFlowData.flowStartTime = TimeUtils.getCurrentTime() updateCourseData() } } } - private fun updateErrorState(iapException: IAPException) { - val feedbackErrorMessage: String = iapException.getFormattedErrorMessage() - when (iapException.requestType) { - IAPRequestType.PAYMENT_SDK_CODE -> { - if (BillingClient.BillingResponseCode.USER_CANCELED == iapException.httpErrorCode) { - canceledByUserEvent() - } else { - purchaseErrorEvent(feedbackErrorMessage) - } - } - - IAPRequestType.PRICE_CODE, - IAPRequestType.NO_SKU_CODE -> { - priceLoadErrorEvent(feedbackErrorMessage) - } - - else -> { - courseUpgradeErrorEvent(feedbackErrorMessage) - } - } - if (BillingClient.BillingResponseCode.USER_CANCELED != iapException.httpErrorCode) { - _uiState.value = IAPUIState.Error(iapException) - } else { - _uiState.value = IAPUIState.Clear - } - } - fun loadPrice() { viewModelScope.launch(Dispatchers.IO) { purchaseFlowData.takeIf { it.courseId != null && it.productInfo != null } @@ -165,7 +141,7 @@ class IAPViewModel( fun startPurchaseFlow() { upgradeNowClickedEvent() _uiState.value = IAPUIState.Loading(loaderType = IAPLoaderType.PURCHASE_FLOW) - purchaseFlowData.flowStartTime = getCurrentTime() + purchaseFlowData.flowStartTime = TimeUtils.getCurrentTime() purchaseFlowData.takeIf { purchaseFlowData.courseName != null && it.productInfo != null } ?.apply { addToBasket(productInfo?.courseSku!!) @@ -261,7 +237,7 @@ class IAPViewModel( fun refreshCourse() { _uiState.value = IAPUIState.Loading(IAPLoaderType.FULL_SCREEN) - purchaseFlowData.flowStartTime = getCurrentTime() + purchaseFlowData.flowStartTime = TimeUtils.getCurrentTime() updateCourseData() } @@ -269,6 +245,10 @@ class IAPViewModel( executeOrder(purchaseFlowData) } + fun retryToConsumeOrder() { + consumeOrderForFurtherPurchases(purchaseFlowData) + } + private fun updateCourseData() { viewModelScope.launch(Dispatchers.IO) { purchaseFlowData.courseId?.let { courseId -> @@ -282,17 +262,44 @@ class IAPViewModel( context = context, feedbackEmailAddress = config.getFeedbackEmailAddress(), feedback = message, - appVersion = versionName + appVersion = appData.versionName ) logIAPErrorActionEvent(flowType, IAPAction.ACTION_GET_HELP.action) } + private fun updateErrorState(iapException: IAPException) { + val feedbackErrorMessage: String = iapException.getFormattedErrorMessage() + when (iapException.requestType) { + IAPRequestType.PAYMENT_SDK_CODE -> { + if (BillingClient.BillingResponseCode.USER_CANCELED == iapException.httpErrorCode) { + canceledByUserEvent() + } else { + purchaseErrorEvent(feedbackErrorMessage) + } + } + + IAPRequestType.PRICE_CODE, + IAPRequestType.NO_SKU_CODE -> { + priceLoadErrorEvent(feedbackErrorMessage) + } + + else -> { + courseUpgradeErrorEvent(feedbackErrorMessage) + } + } + if (BillingClient.BillingResponseCode.USER_CANCELED != iapException.httpErrorCode) { + _uiState.value = IAPUIState.Error(iapException) + } else { + _uiState.value = IAPUIState.Clear + } + } + private fun upgradeNowClickedEvent() { logIAPEvent(IAPAnalyticsEvent.IAP_UPGRADE_NOW_CLICKED) } private fun upgradeSuccessEvent() { - val elapsedTime = getCurrentTime() - purchaseFlowData.flowStartTime + val elapsedTime = TimeUtils.getCurrentTime() - purchaseFlowData.flowStartTime logIAPEvent(IAPAnalyticsEvent.IAP_COURSE_UPGRADE_SUCCESS, buildMap { put(IAPAnalyticsKeys.ELAPSED_TIME.key, elapsedTime) }.toMutableMap()) @@ -353,10 +360,6 @@ class IAPViewModel( fun clearIAPFLow() { _uiState.value = IAPUIState.Clear - purchaseFlowData = PurchaseFlowData() - } - - companion object { - fun getCurrentTime() = Calendar.getInstance().timeInMillis + purchaseFlowData.reset() } } diff --git a/core/src/main/java/org/openedx/core/ui/IAPUI.kt b/core/src/main/java/org/openedx/core/ui/IAPUI.kt index 9f5ae7ca2..5f908fcd4 100644 --- a/core/src/main/java/org/openedx/core/ui/IAPUI.kt +++ b/core/src/main/java/org/openedx/core/ui/IAPUI.kt @@ -1,5 +1,6 @@ package org.openedx.core.ui +import android.text.TextUtils import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -79,7 +80,7 @@ fun PriceLoadErrorDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { UpgradeErrorDialog( title = stringResource(id = R.string.iap_error_title), description = stringResource(id = R.string.iap_error_price_not_fetched), - confirmText = stringResource(id = R.string.iap_label_refresh_now), + confirmText = stringResource(id = R.string.core_error_try_again), onConfirm = onConfirm, dismissText = stringResource(id = R.string.core_cancel), onDismiss = onDismiss @@ -87,36 +88,115 @@ fun PriceLoadErrorDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { } @Composable -fun CourseAlreadyPurchasedErrorDialog( +fun NoSkuErrorDialog( + onConfirm: () -> Unit, +) { + AlertDialog( + modifier = Modifier + .background( + color = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.cardShape + ) + .padding(bottom = 8.dp), + shape = MaterialTheme.appShapes.cardShape, + backgroundColor = MaterialTheme.appColors.background, + title = { + Text( + text = stringResource(id = R.string.iap_error_title), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium, + ) + }, + text = { + Text( + text = stringResource(id = R.string.iap_error_price_not_fetched), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.bodyMedium + ) + }, + confirmButton = { + OpenEdXButton( + modifier = Modifier.wrapContentSize(), + text = stringResource(id = R.string.core_ok), + onClick = onConfirm + ) + }, + onDismissRequest = onConfirm + ) +} + +@Composable +fun CourseAlreadyPurchasedErrorDialog(onRefresh: () -> Unit, onDismiss: () -> Unit) { + UpgradeErrorDialog( + title = stringResource(id = R.string.iap_error_title), + description = stringResource(id = R.string.iap_course_already_paid_for_message), + confirmText = stringResource(id = R.string.iap_label_refresh_now), + onConfirm = onRefresh, + dismissText = stringResource(id = R.string.core_cancel), + onDismiss = onDismiss + ) +} + +@Composable +fun CourseAlreadyPurchasedExecuteErrorDialog( + confirmText: String = "", + description: String = "", onRefresh: () -> Unit, onGetHelp: () -> Unit, onDismiss: () -> Unit ) { + var genConfirmText = confirmText + if (TextUtils.isEmpty(confirmText)) { + genConfirmText = stringResource(id = R.string.iap_refresh_to_retry) + } + + var genDescription = description + if (TextUtils.isEmpty(genDescription)) { + genDescription = stringResource(id = R.string.iap_course_not_fullfilled) + } + AlertDialog( + modifier = Modifier + .background( + color = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.cardShape + ) + .padding(bottom = 8.dp), + shape = MaterialTheme.appShapes.cardShape, + backgroundColor = MaterialTheme.appColors.background, title = { Text( text = stringResource(id = R.string.iap_error_title), - style = MaterialTheme.appTypography.titleMedium + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium, + ) + }, + text = { + Text( + text = genDescription, + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.bodyMedium ) }, - text = { Text(text = stringResource(id = R.string.iap_course_already_paid_for_message)) }, buttons = { Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.SpaceBetween + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + verticalArrangement = Arrangement.SpaceBetween, ) { OpenEdXButton( modifier = Modifier .fillMaxWidth() - .padding(8.dp), - text = stringResource(id = R.string.iap_label_refresh_now), + .padding(2.dp), + text = genConfirmText, onClick = onRefresh ) OpenEdXButton( modifier = Modifier .fillMaxWidth() - .padding(8.dp), + .padding(2.dp), text = stringResource(id = R.string.core_contact_support), onClick = onGetHelp ) @@ -124,7 +204,7 @@ fun CourseAlreadyPurchasedErrorDialog( OpenEdXButton( modifier = Modifier .fillMaxWidth() - .padding(8.dp), + .padding(2.dp), text = stringResource(id = R.string.core_cancel), onClick = onDismiss ) @@ -140,12 +220,13 @@ fun GeneralUpgradeErrorDialog( onConfirm: () -> Unit, onDismiss: () -> Unit ) { + var genDescription = description if (description.isBlank()) { - stringResource(id = R.string.iap_general_upgrade_error_message) + genDescription = stringResource(id = R.string.iap_general_upgrade_error_message) } UpgradeErrorDialog( title = stringResource(id = R.string.iap_error_title), - description = description, + description = genDescription, confirmText = stringResource(id = R.string.core_cancel), onConfirm = onConfirm, dismissText = stringResource(id = R.string.iap_get_help), @@ -162,24 +243,39 @@ fun UpgradeErrorDialog( dismissText: String, onDismiss: () -> Unit ) { - AlertDialog( - title = { Text(text = title, style = MaterialTheme.appTypography.titleMedium) }, - text = { Text(text = description) }, + modifier = Modifier + .background( + color = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.cardShape + ) + .padding(bottom = 8.dp), + shape = MaterialTheme.appShapes.cardShape, + backgroundColor = MaterialTheme.appColors.background, + title = { + Text( + text = title, + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium, + ) + }, + text = { + Text( + text = description, + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.bodyMedium + ) + }, confirmButton = { OpenEdXButton( - modifier = Modifier - .wrapContentSize() - .padding(8.dp), + modifier = Modifier.wrapContentSize(), text = confirmText, onClick = onConfirm ) }, dismissButton = { OpenEdXButton( - modifier = Modifier - .wrapContentSize() - .padding(8.dp), + modifier = Modifier.wrapContentSize(), text = dismissText, onClick = onDismiss ) @@ -218,29 +314,38 @@ fun CheckingPurchasesDialog() { @Composable fun FakePurchasesFulfillmentCompleted(onCancel: () -> Unit, onGetHelp: () -> Unit) { AlertDialog( + modifier = Modifier + .background( + color = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.cardShape + ) + .padding(end = 8.dp, bottom = 8.dp), + shape = MaterialTheme.appShapes.cardShape, + backgroundColor = MaterialTheme.appColors.background, title = { Text( text = stringResource(id = R.string.iap_title_purchases_restored), - style = MaterialTheme.appTypography.titleMedium + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium, ) }, text = { - Text(text = stringResource(id = R.string.iap_message_purchases_restored)) + Text( + text = stringResource(id = R.string.iap_message_purchases_restored), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.bodyMedium, + ) }, confirmButton = { OpenEdXButton( - modifier = Modifier - .wrapContentSize() - .padding(4.dp), + modifier = Modifier.wrapContentSize(), text = stringResource(id = R.string.core_cancel), onClick = onCancel ) }, dismissButton = { OpenEdXButton( - modifier = Modifier - .wrapContentSize() - .padding(4.dp), + modifier = Modifier.wrapContentSize(), text = stringResource(id = R.string.iap_get_help), onClick = onGetHelp ) @@ -252,29 +357,38 @@ fun FakePurchasesFulfillmentCompleted(onCancel: () -> Unit, onGetHelp: () -> Uni @Composable fun PurchasesFulfillmentCompletedDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { AlertDialog( + modifier = Modifier + .background( + color = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.cardShape + ) + .padding(end = 8.dp, bottom = 8.dp), + shape = MaterialTheme.appShapes.cardShape, + backgroundColor = MaterialTheme.appColors.background, title = { Text( text = stringResource(id = R.string.iap_silent_course_upgrade_success_title), - style = MaterialTheme.appTypography.titleMedium + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium, ) }, text = { - Text(text = stringResource(id = R.string.iap_silent_course_upgrade_success_message)) + Text( + text = stringResource(id = R.string.iap_silent_course_upgrade_success_message), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.bodyMedium, + ) }, confirmButton = { OpenEdXButton( - modifier = Modifier - .wrapContentSize() - .padding(4.dp), + modifier = Modifier.wrapContentSize(), text = stringResource(id = R.string.iap_label_refresh_now), onClick = onConfirm ) }, dismissButton = { OpenEdXButton( - modifier = Modifier - .wrapContentSize() - .padding(4.dp), + modifier = Modifier.wrapContentSize(), text = stringResource(id = R.string.iap_label_continue_without_update), onClick = onDismiss ) @@ -322,5 +436,17 @@ private fun PreviewFakePurchasesFulfillmentCompleted() { @Preview @Composable private fun PreviewCourseAlreadyPurchasedErrorDialog() { - CourseAlreadyPurchasedErrorDialog(onRefresh = {}, onGetHelp = {}, onDismiss = {}) + CourseAlreadyPurchasedErrorDialog(onRefresh = {}, onDismiss = {}) +} + +@Preview +@Composable +private fun PreviewCourseAlreadyPurchasedExecuteErrorDialog() { + CourseAlreadyPurchasedExecuteErrorDialog(onRefresh = {}, onGetHelp = {}, onDismiss = {}) +} + +@Preview +@Composable +private fun PreviewNoSkuErrorDialog() { + NoSkuErrorDialog(onConfirm = {}) } diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index d1f6455cb..a0cc25c6f 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -204,6 +204,7 @@ Unlock access to all course activities, including graded assignments Full access to course content and course material even after the course ends An Error occurred + Refresh to retry It looks like something went wrong when upgrading your course. If this error continues, please contact Support. Error upgrading course in app Thank you for your purchase. Enjoy full access to your course! @@ -219,5 +220,6 @@ The course you are looking to upgrade could not be found. Please try your upgrade again. If this error continues, contact Support. The course you are looking to upgrade has already been paid for. For additional help, reach out to Support. Something happened when we tried to update your course experience. If this error continues, reach out to Support for help. + Your payment could not be processed at this time. Please try again. For additional help, reach out to Support. 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 68c2c33ca..29b1f86ce 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 @@ -112,9 +112,6 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initCourseView() - if (viewModel.calendarSyncUIState.value.isCalendarSyncEnabled) { - setUpCourseCalendar() - } observe() } @@ -135,6 +132,8 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { requireActivity().supportFragmentManager, viewModel.courseName ) + } else if (viewModel.calendarSyncUIState.value.isCalendarSyncEnabled) { + setUpCourseCalendar() } } viewModel.errorMessage.observe(viewLifecycleOwner) { 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 e1c18d5d4..55ccfd2b5 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 @@ -29,6 +29,7 @@ import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CourseStructure import org.openedx.core.exception.NoCachedDataException import org.openedx.core.extension.isInternetError +import org.openedx.core.presentation.global.AppData import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState import org.openedx.core.system.CalendarManager @@ -66,7 +67,7 @@ class CourseContainerViewModel( var courseName: String, private var resumeBlockId: String, private val enrollmentMode: String, - private val versionName: String, + private val appData: AppData, private val config: Config, private val interactor: CourseInteractor, private val calendarManager: CalendarManager, @@ -113,7 +114,7 @@ class CourseContainerViewModel( private val isIAPEnabled get() = iapConfig.isEnabled && - iapConfig.disableVersions.contains(versionName).not() + iapConfig.disableVersions.contains(appData.versionName).not() private var _canShowUpgradeButton = MutableStateFlow(false) val canShowUpgradeButton: StateFlow @@ -128,7 +129,7 @@ class CourseContainerViewModel( private val _calendarSyncUIState = MutableStateFlow( CalendarSyncUIState( - isCalendarSyncEnabled = isCalendarSyncEnabled(), + isCalendarSyncEnabled = false, calendarTitle = calendarManager.getCourseCalendarTitle(courseName), courseDates = emptyList(), dialogType = CalendarSyncDialogType.NONE, @@ -203,6 +204,9 @@ class CourseContainerViewModel( _courseStructure?.let { courseName = it.name loadCourseImage(courseStructure?.media?.image?.large) + _calendarSyncUIState.update { state -> + state.copy(isCalendarSyncEnabled = isCalendarSyncEnabled()) + } _dataReady.value = courseStructure?.start?.let { start -> val isReady = start < Date() if (isReady) { @@ -295,7 +299,7 @@ class CourseContainerViewModel( } _refreshing.value = false courseNotifier.send(CourseStructureUpdated(courseId)) - if(isIAPFlow) { + if (isIAPFlow) { iapNotifier.send(CourseDataUpdated()) } } 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 3c3e92953..471aa59fc 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -209,7 +209,7 @@ class DashboardListFragment : Fragment() { } IAPAction.ACTION_ERROR_CLOSE -> { - viewModel.loadIAPCancelEvent() + viewModel.logIAPCancelEvent() } IAPAction.ACTION_GET_HELP -> { 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 0455347d2..82affb640 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -28,6 +28,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.iap.IAPAction import org.openedx.core.presentation.iap.IAPFlow import org.openedx.core.presentation.iap.IAPRequestType @@ -45,7 +46,7 @@ import org.openedx.core.utils.EmailUtil import org.openedx.dashboard.domain.interactor.DashboardInteractor class DashboardListViewModel( - private val versionName: String, + private val appData: AppData, private val config: Config, private val networkConnection: NetworkConnection, private val interactor: DashboardInteractor, @@ -100,7 +101,7 @@ class DashboardListViewModel( get() = preferencesManager.appConfig.iapConfig private val isIAPEnabled get() = iapConfig.isEnabled && - iapConfig.disableVersions.contains(versionName).not() + iapConfig.disableVersions.contains(appData.versionName).not() override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) @@ -275,7 +276,7 @@ class DashboardListViewModel( context = context, feedbackEmailAddress = config.getFeedbackEmailAddress(), feedback = message, - appVersion = versionName + appVersion = appData.versionName ) logIAPEvent(IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, buildMap { put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED) @@ -283,7 +284,7 @@ class DashboardListViewModel( }.toMutableMap()) } - fun loadIAPCancelEvent() { + fun logIAPCancelEvent() { logIAPEvent(IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, buildMap { put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED) put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_CLOSE) diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index eee22e36d..2f91a3a31 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -84,3 +84,5 @@ SOCIAL_AUTH_ENABLED: false UI_COMPONENTS: COURSE_DROPDOWN_NAVIGATION_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false + +ECOMMERCE_URL: 'http://localhost:8000' diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index eee22e36d..2f91a3a31 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -84,3 +84,5 @@ SOCIAL_AUTH_ENABLED: false UI_COMPONENTS: COURSE_DROPDOWN_NAVIGATION_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false + +ECOMMERCE_URL: 'http://localhost:8000' diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index eee22e36d..2f91a3a31 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -84,3 +84,5 @@ SOCIAL_AUTH_ENABLED: false UI_COMPONENTS: COURSE_DROPDOWN_NAVIGATION_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false + +ECOMMERCE_URL: 'http://localhost:8000' 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 a374c2ff6..95de33a51 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,6 +10,8 @@ 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.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 @@ -104,29 +106,18 @@ class SettingsFragment : Fragment() { SettingsScreenAction.RestorePurchaseClick -> { viewModel.restorePurchase() } - - SettingsScreenAction.RestorePurchaseCancel -> { - viewModel.clearIAPState() - } - - SettingsScreenAction.GetHelpClick -> { - viewModel.clearIAPState() - viewModel.showFeedbackScreen( - requireActivity(), - "test message" - ) - } } }, onIAPAction = { action, iapException -> when (action) { IAPAction.ACTION_ERROR_CLOSE -> { - viewModel.loadIAPCancelEvent() + viewModel.logIAPCancelEvent() } IAPAction.ACTION_GET_HELP -> { - iapException?.getFormattedErrorMessage() - ?.let { viewModel.showFeedbackScreen(requireActivity(), it) } + viewModel.clearIAPState() + val errorMessage = iapException?.getFormattedErrorMessage() ?: "" + viewModel.showFeedbackScreen(requireActivity(), errorMessage) } IAPAction.ACTION_RESTORE -> { @@ -139,6 +130,19 @@ class SettingsFragment : Fragment() { ) } + IAPAction.ACTION_RESTORE_PURCHASE_CANCEL -> { + viewModel.logIAPEvent( + IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, + buildMap { + put( + IAPAnalyticsKeys.ACTION.key, + IAPAction.ACTION_CLOSE + ) + }.toMutableMap() + ) + viewModel.clearIAPState() + } + else -> {} } } @@ -167,7 +171,5 @@ internal interface SettingsScreenAction { object ManageAccountClick : SettingsScreenAction object CalendarSettingsClick : SettingsScreenAction object RestorePurchaseClick : SettingsScreenAction - object RestorePurchaseCancel : SettingsScreenAction - object GetHelpClick : SettingsScreenAction } diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt index c05899151..176f1abe3 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt @@ -222,24 +222,26 @@ internal fun SettingsScreen( } } - when { - iapUiState is IAPUIState.FakePurchasesFulfillmentCompleted -> { + when (iapUiState) { + is IAPUIState.FakePurchasesFulfillmentCompleted -> { FakePurchasesFulfillmentCompleted(onCancel = { - onAction(SettingsScreenAction.RestorePurchaseCancel) + onIAPAction(IAPAction.ACTION_RESTORE_PURCHASE_CANCEL, null) }, onGetHelp = { - onAction(SettingsScreenAction.GetHelpClick) + onIAPAction(IAPAction.ACTION_GET_HELP, null) }) } - (iapUiState is IAPUIState.Loading && iapUiState.loaderType == IAPLoaderType.RESTORE_PURCHASES) -> { - CheckingPurchasesDialog() + is IAPUIState.Loading -> { + if (iapUiState.loaderType == IAPLoaderType.RESTORE_PURCHASES) { + CheckingPurchasesDialog() + } } - iapUiState is IAPUIState.PurchasesFulfillmentCompleted -> { + is IAPUIState.PurchasesFulfillmentCompleted -> { onIAPAction(IAPAction.ACTION_RESTORE, null) } - iapUiState is IAPUIState.Error -> { + is IAPUIState.Error -> { UpgradeErrorDialog( title = stringResource(id = R.string.iap_error_title), description = stringResource(id = R.string.iap_course_not_fullfilled), 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 9139a929c..bdc70bd13 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 @@ -48,7 +48,6 @@ import org.openedx.profile.system.notifier.AccountDeactivated import org.openedx.profile.system.notifier.ProfileNotifier class SettingsViewModel( - private val versionName: String, private val appData: AppData, private val config: Config, private val resourceManager: ResourceManager, @@ -233,43 +232,44 @@ class SettingsViewModel( fun restorePurchase() { logIAPEvent(IAPAnalyticsEvent.IAP_RESTORE_PURCHASE_CLICKED) viewModelScope.launch(Dispatchers.IO) { + val userId = corePreferences.user?.id ?: return@launch + _iapUiState.emit(IAPUIState.Loading(IAPLoaderType.RESTORE_PURCHASES)) // delay to show loading state delay(2000) - corePreferences.user?.id?.let { userId -> - runCatching { - 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()) - _iapUiState.emit(IAPUIState.PurchasesFulfillmentCompleted) - } else { - _iapUiState.emit(IAPUIState.FakePurchasesFulfillmentCompleted) - } - }.onFailure { - if (it is IAPException) { - _iapUiState.emit( - IAPUIState.Error( - IAPException( - IAPRequestType.RESTORE_CODE, - it.httpErrorCode, - it.errorMessage - ) + + runCatching { + 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()) + _iapUiState.emit(IAPUIState.PurchasesFulfillmentCompleted) + } else { + _iapUiState.emit(IAPUIState.FakePurchasesFulfillmentCompleted) + } + }.onFailure { + if (it is IAPException) { + _iapUiState.emit( + IAPUIState.Error( + IAPException( + IAPRequestType.RESTORE_CODE, + it.httpErrorCode, + it.errorMessage ) ) - } + ) } } } } - fun loadIAPCancelEvent() { + fun logIAPCancelEvent() { logIAPEvent(IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, buildMap { put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_RESTORE) put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_CLOSE) @@ -281,7 +281,7 @@ class SettingsViewModel( context = context, feedbackEmailAddress = config.getFeedbackEmailAddress(), feedback = message, - appVersion = versionName + appVersion = appData.versionName ) logIAPEvent(IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, buildMap { put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED) @@ -289,7 +289,7 @@ class SettingsViewModel( }.toMutableMap()) } - private fun logIAPEvent( + fun logIAPEvent( event: IAPAnalyticsEvent, params: MutableMap = mutableMapOf() ) {