From 3ca9e3d567b5b99928a95e08e2ea927db577495f Mon Sep 17 00:00:00 2001 From: Syedh30 Date: Tue, 9 Jul 2024 18:16:24 +0530 Subject: [PATCH 1/3] feat: google alternative billing --- .../java/com/dooboolab/rniap/RNIapModule.kt | 21 +++++++++++++++++-- src/eventEmitter.ts | 21 +++++++++++++++++++ src/hooks/withIAPContext.tsx | 20 ++++++++++++++++++ src/iap.ts | 3 +++ src/modules/android.ts | 1 + src/types/index.ts | 3 +++ 6 files changed, 67 insertions(+), 2 deletions(-) diff --git a/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt b/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt index e68412aec..0f3bd135d 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,7 @@ 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 +147,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 +452,7 @@ class RNIapModule( type: String, skuArr: ReadableArray, purchaseToken: String?, + externalTransactionID: String?, replacementMode: Int, obfuscatedAccountId: String?, obfuscatedProfileId: String?, @@ -508,6 +511,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 +725,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 +743,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..938355233 100644 --- a/src/eventEmitter.ts +++ b/src/eventEmitter.ts @@ -65,6 +65,27 @@ export const purchaseUpdatedListener = ( return emitterSubscription; }; +export const userChoiceBillingUpdateListener = ( + 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..fe791ba3e 100644 --- a/src/hooks/withIAPContext.tsx +++ b/src/hooks/withIAPContext.tsx @@ -4,6 +4,7 @@ import { promotedProductListener, purchaseErrorListener, purchaseUpdatedListener, + userChoiceBillingUpdateListener, 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 = userChoiceBillingUpdateListener( + 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 ca0419704..cef5b5d5a 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, + externalTransactionID, prorationModeAndroid = -1, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, @@ -807,6 +809,7 @@ export const requestSubscription = ( ANDROID_ITEM_TYPE_SUBSCRIPTION, subscriptionOffers?.map((so) => so.sku), purchaseTokenAndroid, + externalTransactionID, prorationModeAndroid, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, diff --git a/src/modules/android.ts b/src/modules/android.ts index 306786fc1..d82f180ae 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, + externalTransactionID: string | undefined, prorationMode: ProrationModesAndroid | -1, obfuscatedAccountId: string | undefined, obfuscatedProfileId: string | undefined, diff --git a/src/types/index.ts b/src/types/index.ts index b65dd9ade..5d970a975 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 + externalTransactionToken?: string; } export interface PurchaseResult { @@ -254,6 +256,7 @@ export interface SubscriptionOffer { export interface RequestSubscriptionAndroid extends RequestPurchaseBaseAndroid { purchaseTokenAndroid?: string; + externalTransactionID?: string; prorationModeAndroid?: ProrationModesAndroid; subscriptionOffers: SubscriptionOffer[]; } From b2f2b730a22a9c9c4e1f57cbe4bfd3a622368d27 Mon Sep 17 00:00:00 2001 From: Syedh30 Date: Thu, 11 Jul 2024 14:11:54 +0530 Subject: [PATCH 2/3] fix: updated suffix for android --- android/src/play/java/com/dooboolab/rniap/RNIapModule.kt | 3 ++- src/eventEmitter.ts | 2 +- src/hooks/withIAPContext.tsx | 4 ++-- src/iap.ts | 4 ++-- src/modules/android.ts | 2 +- src/types/index.ts | 4 ++-- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt b/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt index 0f3bd135d..57217860d 100644 --- a/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt +++ b/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt @@ -43,7 +43,8 @@ class RNIapModule( private val builder: BillingClient.Builder = BillingClient.newBuilder(reactContext).enablePendingPurchases(), private val googleApiAvailability: GoogleApiAvailability = GoogleApiAvailability.getInstance(), ) : ReactContextBaseJavaModule(reactContext), - PurchasesUpdatedListener, UserChoiceBillingListener { + PurchasesUpdatedListener, + UserChoiceBillingListener { private var billingClientCache: BillingClient? = null private val skus: MutableMap = mutableMapOf() diff --git a/src/eventEmitter.ts b/src/eventEmitter.ts index 938355233..2bb78737f 100644 --- a/src/eventEmitter.ts +++ b/src/eventEmitter.ts @@ -65,7 +65,7 @@ export const purchaseUpdatedListener = ( return emitterSubscription; }; -export const userChoiceBillingUpdateListener = ( +export const userChoiceBillingUpdateListenerAndroid = ( listener: (event: Purchase) => void, ) => { const eventEmitter = new NativeEventEmitter(getNativeModule()); diff --git a/src/hooks/withIAPContext.tsx b/src/hooks/withIAPContext.tsx index fe791ba3e..79a8ab86d 100644 --- a/src/hooks/withIAPContext.tsx +++ b/src/hooks/withIAPContext.tsx @@ -4,7 +4,7 @@ import { promotedProductListener, purchaseErrorListener, purchaseUpdatedListener, - userChoiceBillingUpdateListener, + userChoiceBillingUpdateListenerAndroid, transactionListener, } from '../eventEmitter'; import {IapIos, initConnection} from '../iap'; @@ -146,7 +146,7 @@ export function withIAPContext(Component: React.ComponentType) { }, ); - const userChoiceBillingUpdateSubscription = userChoiceBillingUpdateListener( + const userChoiceBillingUpdateSubscription = userChoiceBillingUpdateListenerAndroid( async (purchase: ProductPurchase | SubscriptionPurchase) => { setAlternativePurchaseError(undefined); setAlternativePurchase(purchase); diff --git a/src/iap.ts b/src/iap.ts index 12c2a2798..f51703daf 100644 --- a/src/iap.ts +++ b/src/iap.ts @@ -798,7 +798,7 @@ export const requestSubscription = ( const { subscriptionOffers, purchaseTokenAndroid, - externalTransactionID, + externalTransactionIdAndroid, replacementModeAndroid = -1, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, @@ -809,7 +809,7 @@ export const requestSubscription = ( ANDROID_ITEM_TYPE_SUBSCRIPTION, subscriptionOffers?.map((so) => so.sku), purchaseTokenAndroid, - externalTransactionID, + externalTransactionIdAndroid, replacementModeAndroid, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, diff --git a/src/modules/android.ts b/src/modules/android.ts index 8cff3b0ac..0f1629ae4 100644 --- a/src/modules/android.ts +++ b/src/modules/android.ts @@ -39,7 +39,7 @@ export type BuyItemByType = ( type: string, skus: Sku[], purchaseToken: string | undefined, - externalTransactionID: 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 60ed67aee..b7134e485 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -93,7 +93,7 @@ export interface ProductPurchase { userJsonAmazon?: string; isCanceledAmazon?: boolean; //UserChoiceBilling - externalTransactionToken?: string; + externalTransactionTokenAndroid?: string; } export interface PurchaseResult { @@ -256,7 +256,7 @@ export interface SubscriptionOffer { export interface RequestSubscriptionAndroid extends RequestPurchaseBaseAndroid { purchaseTokenAndroid?: string; - externalTransactionID?: string; + externalTransactionIdAndroid?: string; replacementModeAndroid?: ReplacementModesAndroid; subscriptionOffers: SubscriptionOffer[]; } From 2e9e6c0c325f00cdfe7d2e1aef30a108d632b5ef Mon Sep 17 00:00:00 2001 From: Syedh30 Date: Thu, 11 Jul 2024 14:26:03 +0530 Subject: [PATCH 3/3] fix: ci failing for RNIapModule --- android/src/play/java/com/dooboolab/rniap/RNIapModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt b/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt index 57217860d..81bf1394d 100644 --- a/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt +++ b/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt @@ -43,7 +43,7 @@ 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()