diff --git a/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt b/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt index e68412aec..81bf1394d 100644 --- a/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt +++ b/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt @@ -16,6 +16,8 @@ import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.QueryProductDetailsParams import com.android.billingclient.api.QueryPurchaseHistoryParams import com.android.billingclient.api.QueryPurchasesParams +import com.android.billingclient.api.UserChoiceBillingListener +import com.android.billingclient.api.UserChoiceDetails import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise @@ -41,7 +43,8 @@ class RNIapModule( private val builder: BillingClient.Builder = BillingClient.newBuilder(reactContext).enablePendingPurchases(), private val googleApiAvailability: GoogleApiAvailability = GoogleApiAvailability.getInstance(), ) : ReactContextBaseJavaModule(reactContext), - PurchasesUpdatedListener { + PurchasesUpdatedListener, + UserChoiceBillingListener { private var billingClientCache: BillingClient? = null private val skus: MutableMap = mutableMapOf() @@ -145,7 +148,7 @@ class RNIapModule( promise.safeResolve(true) return } - builder.setListener(this).build().also { + builder.setListener(this).enableUserChoiceBilling(this).build().also { billingClientCache = it it.startConnection( object : BillingClientStateListener { @@ -450,6 +453,7 @@ class RNIapModule( type: String, skuArr: ReadableArray, purchaseToken: String?, + externalTransactionID: String?, replacementMode: Int, obfuscatedAccountId: String?, obfuscatedProfileId: String?, @@ -508,6 +512,9 @@ class RNIapModule( builder.setProductDetailsParamsList(productParamsList).setIsOfferPersonalized(isOfferPersonalized) val subscriptionUpdateParamsBuilder = SubscriptionUpdateParams.newBuilder() + if (externalTransactionID != null) { + subscriptionUpdateParamsBuilder.setOriginalExternalTransactionId(externalTransactionID) + } if (purchaseToken != null) { subscriptionUpdateParamsBuilder.setOldPurchaseToken(purchaseToken) @@ -719,6 +726,7 @@ class RNIapModule( companion object { private const val PROMISE_BUY_ITEM = "PROMISE_BUY_ITEM" + private const val USER_ALTER_ITEM = "USER_ALTER_ITEM" const val TAG = "RNIapModule" } @@ -736,4 +744,14 @@ class RNIapModule( } reactContext.addLifecycleEventListener(lifecycleEventListener) } + + override fun userSelectedAlternativeBilling(userChoiceDetails: UserChoiceDetails) { + val products = userChoiceDetails.products + val externalToken = userChoiceDetails.externalTransactionToken + val result = Arguments.createMap() + result.putString("products", userChoiceDetails.products.toString()) + result.putString("externalTransactionToken", userChoiceDetails.externalTransactionToken) + sendEvent(reactContext, "user-alternative-billing", result) + PromiseUtils.resolvePromisesForKey(USER_ALTER_ITEM, null) + } } diff --git a/src/eventEmitter.ts b/src/eventEmitter.ts index 40077d5d8..2bb78737f 100644 --- a/src/eventEmitter.ts +++ b/src/eventEmitter.ts @@ -65,6 +65,27 @@ export const purchaseUpdatedListener = ( return emitterSubscription; }; +export const userChoiceBillingUpdateListenerAndroid = ( + listener: (event: Purchase) => void, +) => { + const eventEmitter = new NativeEventEmitter(getNativeModule()); + const proxyListener = isIosStorekit2() + ? (event: Purchase) => { + listener(transactionSk2ToPurchaseMap(event as any)); + } + : listener; + const emitterUserChoiceBilling = eventEmitter.addListener( + 'user-alternative-billing', + proxyListener, + ); + + if (isAndroid) { + getAndroidModule().startListening(); + } + + return emitterUserChoiceBilling; +}; + /** * Add IAP purchase error event * Register a callback that gets called when there has been an error with a purchase. Returns a React Native `EmitterSubscription` on which you can call `.remove()` to stop receiving updates. diff --git a/src/hooks/withIAPContext.tsx b/src/hooks/withIAPContext.tsx index 3b978f91d..79a8ab86d 100644 --- a/src/hooks/withIAPContext.tsx +++ b/src/hooks/withIAPContext.tsx @@ -4,6 +4,7 @@ import { promotedProductListener, purchaseErrorListener, purchaseUpdatedListener, + userChoiceBillingUpdateListenerAndroid, transactionListener, } from '../eventEmitter'; import {IapIos, initConnection} from '../iap'; @@ -71,6 +72,9 @@ export function withIAPContext(Component: React.ComponentType) { const [currentPurchaseError, setCurrentPurchaseError] = useState(); + const [alternativePurchase, setAlternativePurchase] = useState(); + const [alternativePurchaseError, setAlternativePurchaseError] = + useState(); const [initConnectionError, setInitConnectionError] = useState(); @@ -85,6 +89,8 @@ export function withIAPContext(Component: React.ComponentType) { currentPurchase, currentTransaction, currentPurchaseError, + alternativePurchase, + alternativePurchaseError, initConnectionError, setProducts, setSubscriptions, @@ -92,6 +98,8 @@ export function withIAPContext(Component: React.ComponentType) { setAvailablePurchases, setCurrentPurchase, setCurrentPurchaseError, + setAlternativePurchase, + setAlternativePurchaseError, }), [ connected, @@ -103,6 +111,8 @@ export function withIAPContext(Component: React.ComponentType) { currentPurchase, currentTransaction, currentPurchaseError, + alternativePurchase, + alternativePurchaseError, initConnectionError, setProducts, setSubscriptions, @@ -110,6 +120,8 @@ export function withIAPContext(Component: React.ComponentType) { setAvailablePurchases, setCurrentPurchase, setCurrentPurchaseError, + setAlternativePurchase, + setAlternativePurchaseError, ], ); @@ -134,6 +146,13 @@ export function withIAPContext(Component: React.ComponentType) { }, ); + const userChoiceBillingUpdateSubscription = userChoiceBillingUpdateListenerAndroid( + async (purchase: ProductPurchase | SubscriptionPurchase) => { + setAlternativePurchaseError(undefined); + setAlternativePurchase(purchase); + }, + ); + const transactionUpdateSubscription = transactionListener( async (transactionOrError: TransactionEvent) => { setCurrentPurchaseError(transactionOrError?.error); @@ -162,6 +181,7 @@ export function withIAPContext(Component: React.ComponentType) { purchaseErrorSubscription.remove(); promotedProductSubscription?.remove(); transactionUpdateSubscription?.remove(); + userChoiceBillingUpdateSubscription?.remove(); }; }, [connected]); diff --git a/src/iap.ts b/src/iap.ts index 5add8df07..f51703daf 100644 --- a/src/iap.ts +++ b/src/iap.ts @@ -642,6 +642,7 @@ export const requestPurchase = ( ANDROID_ITEM_TYPE_IAP, skus, undefined, + undefined, -1, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, @@ -797,6 +798,7 @@ export const requestSubscription = ( const { subscriptionOffers, purchaseTokenAndroid, + externalTransactionIdAndroid, replacementModeAndroid = -1, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, @@ -807,6 +809,7 @@ export const requestSubscription = ( ANDROID_ITEM_TYPE_SUBSCRIPTION, subscriptionOffers?.map((so) => so.sku), purchaseTokenAndroid, + externalTransactionIdAndroid, replacementModeAndroid, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, diff --git a/src/modules/android.ts b/src/modules/android.ts index c6c2b4723..0f1629ae4 100644 --- a/src/modules/android.ts +++ b/src/modules/android.ts @@ -39,6 +39,7 @@ export type BuyItemByType = ( type: string, skus: Sku[], purchaseToken: string | undefined, + externalTransactionIdAndroid: string | undefined, replacementModeAndroid: ReplacementModesAndroid | -1, obfuscatedAccountId: string | undefined, obfuscatedProfileId: string | undefined, diff --git a/src/types/index.ts b/src/types/index.ts index fd9dd9b37..b7134e485 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -92,6 +92,8 @@ export interface ProductPurchase { userMarketplaceAmazon?: string; userJsonAmazon?: string; isCanceledAmazon?: boolean; + //UserChoiceBilling + externalTransactionTokenAndroid?: string; } export interface PurchaseResult { @@ -254,6 +256,7 @@ export interface SubscriptionOffer { export interface RequestSubscriptionAndroid extends RequestPurchaseBaseAndroid { purchaseTokenAndroid?: string; + externalTransactionIdAndroid?: string; replacementModeAndroid?: ReplacementModesAndroid; subscriptionOffers: SubscriptionOffer[]; }