Skip to content
This repository has been archived by the owner on May 6, 2024. It is now read-only.

Commit

Permalink
feat: Consume Product after purchasing
Browse files Browse the repository at this point in the history
Fixes: LEARNER-9818
  • Loading branch information
HamzaIsrar12 committed Feb 14, 2024
1 parent 16ca431 commit 43f1dfb
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ 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
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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object InAppPurchasesUtils {
): MutableList<IAPFlowData> {
purchases.forEach { purchase ->
auditCourses.find { course ->
purchase.getCourseSku()?.equals(course.courseSku) == true
purchase.getCourseSku() == course.courseSku
}?.apply {
this.purchaseToken = purchase.purchaseToken
this.flowType = flowType
Expand Down
33 changes: 10 additions & 23 deletions OpenEdXMobile/src/main/java/org/edx/mobile/util/TextUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ public static CharSequence join(CharSequence delimiter, Iterable<CharSequence> 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;
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = { _, _ ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<ExecuteOrderResponse> {
override fun onSuccess(result: Result.Success<ExecuteOrderResponse>) {
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) {
Expand All @@ -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
*
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 43f1dfb

Please sign in to comment.