From 43f1dfbb1b751c11288069cd0c65c145ec68ddbc Mon Sep 17 00:00:00 2001 From: Hamza Israr Date: Thu, 15 Feb 2024 04:10:40 +0500 Subject: [PATCH] feat: Consume Product after purchasing Fixes: LEARNER-9818 --- .../org/edx/mobile/exception/ErrorMessage.kt | 2 + .../mobile/inapppurchases/BillingProcessor.kt | 15 ++++++- .../edx/mobile/util/InAppPurchasesUtils.kt | 2 +- .../java/org/edx/mobile/util/TextUtils.java | 33 +++++----------- .../org/edx/mobile/view/AccountFragment.kt | 2 + .../edx/mobile/view/MyCoursesListFragment.kt | 2 + .../dialog/FullscreenLoaderDialogFragment.kt | 16 ++++++-- .../viewModel/InAppPurchasesViewModel.kt | 39 +++++++++++++++---- 8 files changed, 74 insertions(+), 37 deletions(-) diff --git a/OpenEdXMobile/src/main/java/org/edx/mobile/exception/ErrorMessage.kt b/OpenEdXMobile/src/main/java/org/edx/mobile/exception/ErrorMessage.kt index f397f70dee..72bda8cc92 100644 --- a/OpenEdXMobile/src/main/java/org/edx/mobile/exception/ErrorMessage.kt +++ b/OpenEdXMobile/src/main/java/org/edx/mobile/exception/ErrorMessage.kt @@ -23,6 +23,7 @@ data class ErrorMessage( const val PAYMENT_SDK_CODE = 0x204 const val COURSE_REFRESH_CODE = 0x205 const val PRICE_CODE = 0x206 + const val CONSUME_CODE = 0x207 } private fun isPreUpgradeErrorType(): Boolean = requestType == PRICE_CODE || @@ -50,6 +51,7 @@ data class ErrorMessage( fun canRetry(): Boolean { return requestType == PRICE_CODE || requestType == EXECUTE_ORDER_CODE || + requestType == CONSUME_CODE || requestType == COURSE_REFRESH_CODE } } diff --git a/OpenEdXMobile/src/main/java/org/edx/mobile/inapppurchases/BillingProcessor.kt b/OpenEdXMobile/src/main/java/org/edx/mobile/inapppurchases/BillingProcessor.kt index 7b3c417c5b..0cc2283840 100644 --- a/OpenEdXMobile/src/main/java/org/edx/mobile/inapppurchases/BillingProcessor.kt +++ b/OpenEdXMobile/src/main/java/org/edx/mobile/inapppurchases/BillingProcessor.kt @@ -11,6 +11,7 @@ import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ConsumeParams import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.ProductDetailsResult import com.android.billingclient.api.Purchase @@ -18,6 +19,7 @@ import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.QueryProductDetailsParams import com.android.billingclient.api.QueryPurchasesParams import com.android.billingclient.api.acknowledgePurchase +import com.android.billingclient.api.consumePurchase import com.android.billingclient.api.queryProductDetails import com.android.billingclient.api.queryPurchasesAsync import dagger.hilt.android.qualifiers.ApplicationContext @@ -127,8 +129,8 @@ class BillingProcessor @Inject constructor( * Called to purchase the new product. Query the product details and launch the purchase flow. * * @param activity active activity to launch our billing flow from - * @param productId Product Id to be purchased * @param userId User Id of the purchaser + * @param productInfo Course and Product info to purchase */ suspend fun purchaseItem( activity: Activity, @@ -243,6 +245,17 @@ class BillingProcessor @Inject constructor( ).purchasesList } + suspend fun consumePurchase(purchaseToken: String): BillingResult { + isReadyOrConnect() + val result = billingClient.consumePurchase( + ConsumeParams + .newBuilder() + .setPurchaseToken(purchaseToken) + .build() + ) + return result.billingResult + } + companion object { private val TAG = BillingProcessor::class.java.simpleName private const val RECONNECT_TIMER_START_MILLISECONDS = 1L * 1000L diff --git a/OpenEdXMobile/src/main/java/org/edx/mobile/util/InAppPurchasesUtils.kt b/OpenEdXMobile/src/main/java/org/edx/mobile/util/InAppPurchasesUtils.kt index c93f0bc82b..7be543a45a 100644 --- a/OpenEdXMobile/src/main/java/org/edx/mobile/util/InAppPurchasesUtils.kt +++ b/OpenEdXMobile/src/main/java/org/edx/mobile/util/InAppPurchasesUtils.kt @@ -21,7 +21,7 @@ object InAppPurchasesUtils { ): MutableList { purchases.forEach { purchase -> auditCourses.find { course -> - purchase.getCourseSku()?.equals(course.courseSku) == true + purchase.getCourseSku() == course.courseSku }?.apply { this.purchaseToken = purchase.purchaseToken this.flowType = flowType diff --git a/OpenEdXMobile/src/main/java/org/edx/mobile/util/TextUtils.java b/OpenEdXMobile/src/main/java/org/edx/mobile/util/TextUtils.java index afd6e5b0b4..d890cc7398 100644 --- a/OpenEdXMobile/src/main/java/org/edx/mobile/util/TextUtils.java +++ b/OpenEdXMobile/src/main/java/org/edx/mobile/util/TextUtils.java @@ -64,9 +64,7 @@ public static CharSequence join(CharSequence delimiter, Iterable t * @return App specific URI string. */ public static String createAppUri(@NonNull String title, @NonNull String uri) { - final StringBuilder uriString = new StringBuilder(AppConstants.APP_URI_SCHEME); - uriString.append(title).append("?").append(PARAM_INTENT_FILE_LINK).append("=").append(uri); - return uriString.toString(); + return AppConstants.APP_URI_SCHEME + title + "?" + PARAM_INTENT_FILE_LINK + "=" + uri; } /** @@ -207,26 +205,15 @@ public static StringBuilder getFormattedErrorMessage(int requestType, int errorC if (requestType == 0) { return body; } - String endpoint; - switch (requestType) { - case ErrorMessage.ADD_TO_BASKET_CODE: - endpoint = "basket"; - break; - case ErrorMessage.CHECKOUT_CODE: - endpoint = "checkout"; - break; - case ErrorMessage.EXECUTE_ORDER_CODE: - endpoint = "execute"; - break; - case ErrorMessage.PAYMENT_SDK_CODE: - endpoint = "payment"; - break; - case ErrorMessage.PRICE_CODE: - endpoint = "price"; - break; - default: - endpoint = "unhandledError"; - } + String endpoint = switch (requestType) { + case ErrorMessage.ADD_TO_BASKET_CODE -> "basket"; + case ErrorMessage.CHECKOUT_CODE -> "checkout"; + case ErrorMessage.EXECUTE_ORDER_CODE -> "execute"; + case ErrorMessage.PAYMENT_SDK_CODE -> "payment"; + case ErrorMessage.PRICE_CODE -> "price"; + case ErrorMessage.CONSUME_CODE -> "consume"; + default -> "unhandledError"; + }; body.append(String.format("%s", endpoint)); // change the default value to -1 cuz in case of BillingClient return errorCode 0 for price load. if (errorCode == -1) { diff --git a/OpenEdXMobile/src/main/java/org/edx/mobile/view/AccountFragment.kt b/OpenEdXMobile/src/main/java/org/edx/mobile/view/AccountFragment.kt index c2cec37553..6e436b9aad 100644 --- a/OpenEdXMobile/src/main/java/org/edx/mobile/view/AccountFragment.kt +++ b/OpenEdXMobile/src/main/java/org/edx/mobile/view/AccountFragment.kt @@ -229,6 +229,8 @@ class AccountFragment : BaseFragment() { retryListener = DialogInterface.OnClickListener { _, _ -> if (errorMessage.requestType == ErrorMessage.EXECUTE_ORDER_CODE) { iapViewModel.executeOrder() + } else if (errorMessage.requestType == ErrorMessage.CONSUME_CODE) { + iapViewModel.consumeOrderForFurtherPurchases(iapViewModel.iapFlowData) } else if (HttpStatus.NOT_ACCEPTABLE == (errorMessage.throwable as InAppPurchasesException).httpErrorCode) { showFullScreenLoader() } diff --git a/OpenEdXMobile/src/main/java/org/edx/mobile/view/MyCoursesListFragment.kt b/OpenEdXMobile/src/main/java/org/edx/mobile/view/MyCoursesListFragment.kt index 006155a84b..2d8a5f4ea5 100644 --- a/OpenEdXMobile/src/main/java/org/edx/mobile/view/MyCoursesListFragment.kt +++ b/OpenEdXMobile/src/main/java/org/edx/mobile/view/MyCoursesListFragment.kt @@ -246,6 +246,8 @@ class MyCoursesListFragment : OfflineSupportBaseFragment(), RefreshListener { retryListener = DialogInterface.OnClickListener { _, _ -> if (errorMessage.requestType == ErrorMessage.EXECUTE_ORDER_CODE) { iapViewModel.executeOrder() + } else if (errorMessage.requestType == ErrorMessage.CONSUME_CODE) { + iapViewModel.consumeOrderForFurtherPurchases(iapViewModel.iapFlowData) } else if (errorMessage.requestType == ErrorMessage.COURSE_REFRESH_CODE) { courseViewModel.fetchEnrolledCourses( type = CoursesRequestType.LIVE, diff --git a/OpenEdXMobile/src/main/java/org/edx/mobile/view/dialog/FullscreenLoaderDialogFragment.kt b/OpenEdXMobile/src/main/java/org/edx/mobile/view/dialog/FullscreenLoaderDialogFragment.kt index 60e93f0028..87f57f241d 100644 --- a/OpenEdXMobile/src/main/java/org/edx/mobile/view/dialog/FullscreenLoaderDialogFragment.kt +++ b/OpenEdXMobile/src/main/java/org/edx/mobile/view/dialog/FullscreenLoaderDialogFragment.kt @@ -107,10 +107,18 @@ class FullscreenLoaderDialogFragment : DialogFragment() { fragment = this@FullscreenLoaderDialogFragment, errorMessage = errorMessage, retryListener = { _, _ -> - if (errorMessage.requestType == ErrorMessage.EXECUTE_ORDER_CODE) { - iapViewModel.executeOrder(iapFlowData) - } else { - purchaseFlowComplete() + when (errorMessage.requestType) { + ErrorMessage.EXECUTE_ORDER_CODE -> { + iapViewModel.executeOrder(iapFlowData) + } + + ErrorMessage.CONSUME_CODE -> { + iapViewModel.consumeOrderForFurtherPurchases(iapViewModel.iapFlowData) + } + + else -> { + purchaseFlowComplete() + } } }, cancelListener = { _, _ -> diff --git a/OpenEdXMobile/src/main/java/org/edx/mobile/viewModel/InAppPurchasesViewModel.kt b/OpenEdXMobile/src/main/java/org/edx/mobile/viewModel/InAppPurchasesViewModel.kt index f3d546e127..af37cba173 100644 --- a/OpenEdXMobile/src/main/java/org/edx/mobile/viewModel/InAppPurchasesViewModel.kt +++ b/OpenEdXMobile/src/main/java/org/edx/mobile/viewModel/InAppPurchasesViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.ProductDetails.OneTimePurchaseOfferDetails import com.android.billingclient.api.Purchase import dagger.hilt.android.lifecycle.HiltViewModel @@ -15,6 +16,7 @@ import org.edx.mobile.extenstion.decodeToLong import org.edx.mobile.http.model.NetworkResponseCallback import org.edx.mobile.http.model.Result import org.edx.mobile.inapppurchases.BillingProcessor +import org.edx.mobile.inapppurchases.getCourseSku import org.edx.mobile.inapppurchases.getPriceAmount import org.edx.mobile.model.api.EnrolledCoursesResponse import org.edx.mobile.model.api.EnrolledCoursesResponse.ProductInfo @@ -187,21 +189,15 @@ class InAppPurchasesViewModel @Inject constructor( this.iapFlowData = iapData repository.executeOrder( basketId = iapData.basketId, - productId = iapData.productId, + productId = iapData.courseSku, purchaseToken = iapData.purchaseToken, price = iapData.price, currencyCode = iapData.currencyCode, callback = object : NetworkResponseCallback { override fun onSuccess(result: Result.Success) { result.data?.let { - iapData.isVerificationPending = false - if (iapFlowData.flowType.isSilentMode()) { - markPurchaseComplete(iapData) - } else { - _refreshCourseData.postEvent(iapData) - } + consumeOrderForFurtherPurchases(iapData) } - endLoading() } override fun onError(error: Result.Error) { @@ -215,6 +211,29 @@ class InAppPurchasesViewModel @Inject constructor( } } + fun consumeOrderForFurtherPurchases(iapFlowData: IAPFlowData) { + viewModelScope.launch { + val result = billingProcessor.consumePurchase(iapFlowData.purchaseToken) + if (result.responseCode == BillingResponseCode.OK) { + iapFlowData.isVerificationPending = false + if (iapFlowData.flowType.isSilentMode()) { + markPurchaseComplete(iapFlowData) + } else { + _refreshCourseData.postEvent(iapFlowData) + } + } else { + dispatchError( + requestType = ErrorMessage.CONSUME_CODE, + throwable = InAppPurchasesException( + httpErrorCode = result.responseCode, + errorMessage = result.debugMessage, + ) + ) + } + endLoading() + } + } + /** * To detect and handle courses which are purchased but still not Verified * @@ -254,6 +273,10 @@ class InAppPurchasesViewModel @Inject constructor( screenName ) if (incompletePurchases.isEmpty()) { + // Consume purchases for new orders if all previous purchases are executed + purchases.forEach { + billingProcessor.consumePurchase(it.purchaseToken) + } _fakeUnfulfilledCompletion.postEvent(true) } else { startUnfulfilledVerification()