react-native-iap 16.4.1 → 16.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/android/build.gradle +19 -21
- package/android/fix-prefab.gradle +11 -24
- package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +200 -142
- package/android/src/main/java/com/margelo/nitro/iap/TokenizedListenerRegistry.kt +0 -1
- package/ios/HybridRnIap.swift +832 -165
- package/ios/RnIapHelper.swift +1 -1
- package/lib/module/hooks/useIAP.js +48 -32
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +207 -153
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.kepler.js +1 -0
- package/lib/module/index.kepler.js.map +1 -1
- package/lib/module/kit-api.js +9 -5
- package/lib/module/kit-api.js.map +1 -1
- package/lib/module/types.js +1 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/error.js +5 -4
- package/lib/module/utils/error.js.map +1 -1
- package/lib/module/vega-adapter.js +4 -1
- package/lib/module/vega-adapter.js.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +31 -15
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +28 -14
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.kepler.d.ts.map +1 -1
- package/lib/typescript/src/kit-api.d.ts +9 -5
- package/lib/typescript/src/kit-api.d.ts.map +1 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +10 -0
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +12 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/error.d.ts.map +1 -1
- package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -0
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JNitroPurchaseRequest.hpp +7 -1
- package/nitrogen/generated/android/c++/JNitroPurchaseRequestType.hpp +58 -0
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitHorizonProps.hpp +66 -0
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitProps.hpp +8 -1
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithProviderProps.hpp +3 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.hpp +74 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitProps.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequest.kt +8 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequestType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitHorizonProps.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitProps.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.kt +62 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +65 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +6 -0
- package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +6 -0
- package/nitrogen/generated/ios/swift/NitroPurchaseRequest.swift +13 -2
- package/nitrogen/generated/ios/swift/NitroPurchaseRequestType.swift +40 -0
- package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitHorizonProps.swift +66 -0
- package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitProps.swift +38 -1
- package/nitrogen/generated/ios/swift/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.swift +30 -0
- package/nitrogen/generated/shared/c++/NitroPurchaseRequest.hpp +9 -2
- package/nitrogen/generated/shared/c++/NitroPurchaseRequestType.hpp +76 -0
- package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitHorizonProps.hpp +90 -0
- package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitProps.hpp +8 -1
- package/openiap-versions.json +3 -3
- package/package.json +1 -1
- package/src/hooks/useIAP.ts +96 -58
- package/src/index.kepler.ts +1 -0
- package/src/index.ts +249 -196
- package/src/kit-api.ts +10 -6
- package/src/specs/RnIap.nitro.ts +12 -0
- package/src/types.ts +13 -2
- package/src/utils/error.ts +7 -3
- package/src/vega-adapter.ts +9 -5
package/src/hooks/useIAP.ts
CHANGED
|
@@ -83,10 +83,12 @@ type UseIap = {
|
|
|
83
83
|
*
|
|
84
84
|
* @example
|
|
85
85
|
* ```ts
|
|
86
|
-
* purchaseUpdatedListener(
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
86
|
+
* purchaseUpdatedListener((purchase) => {
|
|
87
|
+
* void verifyOnServer(purchase)
|
|
88
|
+
* .then((verified) => verified
|
|
89
|
+
* ? finishTransaction({ purchase, isConsumable: false })
|
|
90
|
+
* : undefined)
|
|
91
|
+
* .catch((error) => console.warn('Transaction finalization failed:', error));
|
|
90
92
|
* });
|
|
91
93
|
* ```
|
|
92
94
|
*
|
|
@@ -109,18 +111,25 @@ type UseIap = {
|
|
|
109
111
|
*
|
|
110
112
|
* @example
|
|
111
113
|
* ```ts
|
|
112
|
-
* const { availablePurchases, getAvailablePurchases, finishTransaction } = useIAP();
|
|
114
|
+
* const { connected, availablePurchases, getAvailablePurchases, finishTransaction } = useIAP();
|
|
113
115
|
*
|
|
114
116
|
* useEffect(() => {
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
+
* if (!connected) return;
|
|
118
|
+
* void getAvailablePurchases().catch((error) => {
|
|
119
|
+
* console.warn('Purchase restore failed', error);
|
|
120
|
+
* });
|
|
121
|
+
* }, [connected, getAvailablePurchases]);
|
|
117
122
|
*
|
|
118
123
|
* useEffect(() => {
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* if (
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
+
* void (async () => {
|
|
125
|
+
* for (const p of availablePurchases) {
|
|
126
|
+
* if (await verifyOnServer(p)) {
|
|
127
|
+
* await finishTransaction({ purchase: p, isConsumable: false });
|
|
128
|
+
* }
|
|
129
|
+
* }
|
|
130
|
+
* })().catch((error) => {
|
|
131
|
+
* console.warn('Restored purchase processing failed', error);
|
|
132
|
+
* });
|
|
124
133
|
* }, [availablePurchases, finishTransaction]);
|
|
125
134
|
* ```
|
|
126
135
|
*
|
|
@@ -139,14 +148,17 @@ type UseIap = {
|
|
|
139
148
|
*
|
|
140
149
|
* @example
|
|
141
150
|
* ```ts
|
|
142
|
-
* const { products, fetchProducts } = useIAP();
|
|
151
|
+
* const { connected, products, fetchProducts } = useIAP();
|
|
143
152
|
*
|
|
144
153
|
* useEffect(() => {
|
|
154
|
+
* if (!connected) return;
|
|
145
155
|
* void fetchProducts({
|
|
146
156
|
* skus: ['com.app.coins_100', 'com.app.premium'],
|
|
147
157
|
* type: 'in-app',
|
|
158
|
+
* }).catch((error) => {
|
|
159
|
+
* console.warn('Product fetch failed', error);
|
|
148
160
|
* });
|
|
149
|
-
* }, [fetchProducts]);
|
|
161
|
+
* }, [connected, fetchProducts]);
|
|
150
162
|
*
|
|
151
163
|
* // Render `products` directly from hook state.
|
|
152
164
|
* ```
|
|
@@ -169,10 +181,14 @@ type UseIap = {
|
|
|
169
181
|
* - `type: 'subs'` — same shape, plus `request.google.subscriptionOffers: [{ sku, offerToken }]`.
|
|
170
182
|
* @returns Promise that resolves when the request is dispatched; results land in the
|
|
171
183
|
* hook's `onPurchaseSuccess` / `onPurchaseError` callbacks.
|
|
172
|
-
* @throws
|
|
184
|
+
* @throws Invalid arguments or failures that prevent dispatch. Store outcomes are event-based;
|
|
185
|
+
* on Android, `not-prepared` is delivered through `onPurchaseError`.
|
|
173
186
|
*
|
|
174
187
|
* @example
|
|
175
188
|
* ```ts
|
|
189
|
+
* const { connected, requestPurchase } = useIAP();
|
|
190
|
+
* if (!connected) return;
|
|
191
|
+
*
|
|
176
192
|
* await requestPurchase({
|
|
177
193
|
* request: {
|
|
178
194
|
* apple: { sku: 'com.app.premium' },
|
|
@@ -309,6 +325,18 @@ export interface UseIapOptions {
|
|
|
309
325
|
billingChoiceScreenTypeAndroid?: BillingChoiceScreenTypeAndroid;
|
|
310
326
|
}
|
|
311
327
|
|
|
328
|
+
function mergeByKey<T>(
|
|
329
|
+
existingItems: T[],
|
|
330
|
+
newItems: T[],
|
|
331
|
+
getKey: (item: T) => string,
|
|
332
|
+
): T[] {
|
|
333
|
+
const merged = new Map(
|
|
334
|
+
existingItems.map((item) => [getKey(item), item] as const),
|
|
335
|
+
);
|
|
336
|
+
newItems.forEach((item) => merged.set(getKey(item), item));
|
|
337
|
+
return Array.from(merged.values());
|
|
338
|
+
}
|
|
339
|
+
|
|
312
340
|
/**
|
|
313
341
|
* React Hook for managing In-App Purchases.
|
|
314
342
|
* See documentation at https://react-native-iap.hyo.dev/docs/hooks/useIAP
|
|
@@ -326,27 +354,6 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
326
354
|
const optionsRef = useRef<UseIapOptions | undefined>(options);
|
|
327
355
|
const connectedRef = useRef<boolean>(false);
|
|
328
356
|
|
|
329
|
-
// Helper function to merge arrays with duplicate checking
|
|
330
|
-
const mergeWithDuplicateCheck = useCallback(
|
|
331
|
-
<T>(
|
|
332
|
-
existingItems: T[],
|
|
333
|
-
newItems: T[],
|
|
334
|
-
getKey: (item: T) => string,
|
|
335
|
-
): T[] => {
|
|
336
|
-
const merged = [...existingItems];
|
|
337
|
-
newItems.forEach((newItem) => {
|
|
338
|
-
const isDuplicate = merged.some(
|
|
339
|
-
(existingItem) => getKey(existingItem) === getKey(newItem),
|
|
340
|
-
);
|
|
341
|
-
if (!isDuplicate) {
|
|
342
|
-
merged.push(newItem);
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
return merged;
|
|
346
|
-
},
|
|
347
|
-
[],
|
|
348
|
-
);
|
|
349
|
-
|
|
350
357
|
useEffect(() => {
|
|
351
358
|
optionsRef.current = options;
|
|
352
359
|
}, [options]);
|
|
@@ -367,6 +374,8 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
367
374
|
// Track if component is mounted to prevent listener leaks on early unmount
|
|
368
375
|
const isMountedRef = useRef<boolean>(false);
|
|
369
376
|
const initializationGenerationRef = useRef(0);
|
|
377
|
+
const productRequestGenerationRef = useRef(0);
|
|
378
|
+
const productRequestOwnersRef = useRef(new Map<string, number>());
|
|
370
379
|
|
|
371
380
|
const subscriptionsRefState = useRef<ProductSubscription[]>([]);
|
|
372
381
|
|
|
@@ -388,12 +397,11 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
388
397
|
skus: string[];
|
|
389
398
|
type?: ProductQueryType | null;
|
|
390
399
|
}): Promise<void> => {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
);
|
|
395
|
-
return;
|
|
400
|
+
const generation = ++productRequestGenerationRef.current;
|
|
401
|
+
for (const sku of params.skus) {
|
|
402
|
+
productRequestOwnersRef.current.set(sku, generation);
|
|
396
403
|
}
|
|
404
|
+
|
|
397
405
|
try {
|
|
398
406
|
const requestType = params.type ?? 'in-app';
|
|
399
407
|
RnIapConsole.debug('[useIAP] Calling fetchProducts with:', {
|
|
@@ -408,13 +416,18 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
408
416
|
'[useIAP] fetchProducts count:',
|
|
409
417
|
result?.length ?? 0,
|
|
410
418
|
);
|
|
411
|
-
const items = (
|
|
419
|
+
const items = (
|
|
420
|
+
(result ?? []) as (Product | ProductSubscription)[]
|
|
421
|
+
).filter(
|
|
422
|
+
(item) => productRequestOwnersRef.current.get(item.id) === generation,
|
|
423
|
+
);
|
|
424
|
+
if (items.length === 0) return;
|
|
412
425
|
|
|
413
426
|
// fetchProducts already returns properly filtered results based on type
|
|
414
427
|
if (requestType === 'subs') {
|
|
415
428
|
// All items are already subscriptions
|
|
416
429
|
setSubscriptions((prevSubscriptions: ProductSubscription[]) =>
|
|
417
|
-
|
|
430
|
+
mergeByKey(
|
|
418
431
|
prevSubscriptions,
|
|
419
432
|
items as ProductSubscription[],
|
|
420
433
|
(subscription: ProductSubscription) => subscription.id,
|
|
@@ -433,14 +446,14 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
433
446
|
);
|
|
434
447
|
|
|
435
448
|
setProducts((prevProducts: Product[]) =>
|
|
436
|
-
|
|
449
|
+
mergeByKey(
|
|
437
450
|
prevProducts,
|
|
438
451
|
newProducts,
|
|
439
452
|
(product: Product) => product.id,
|
|
440
453
|
),
|
|
441
454
|
);
|
|
442
455
|
setSubscriptions((prevSubscriptions: ProductSubscription[]) =>
|
|
443
|
-
|
|
456
|
+
mergeByKey(
|
|
444
457
|
prevSubscriptions,
|
|
445
458
|
newSubscriptions,
|
|
446
459
|
(subscription: ProductSubscription) => subscription.id,
|
|
@@ -451,7 +464,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
451
464
|
|
|
452
465
|
// For 'in-app' type, all items are already products
|
|
453
466
|
setProducts((prevProducts: Product[]) =>
|
|
454
|
-
|
|
467
|
+
mergeByKey(
|
|
455
468
|
prevProducts,
|
|
456
469
|
items as Product[],
|
|
457
470
|
(product: Product) => product.id,
|
|
@@ -460,9 +473,16 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
460
473
|
} catch (error) {
|
|
461
474
|
RnIapConsole.error('Error fetching products:', error);
|
|
462
475
|
invokeOnError(error);
|
|
476
|
+
throw error;
|
|
477
|
+
} finally {
|
|
478
|
+
for (const sku of params.skus) {
|
|
479
|
+
if (productRequestOwnersRef.current.get(sku) === generation) {
|
|
480
|
+
productRequestOwnersRef.current.delete(sku);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
463
483
|
}
|
|
464
484
|
},
|
|
465
|
-
[
|
|
485
|
+
[invokeOnError],
|
|
466
486
|
);
|
|
467
487
|
|
|
468
488
|
const getAvailablePurchasesInternal = useCallback(
|
|
@@ -609,19 +629,27 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
609
629
|
if (!subscriptionsRef.current.purchaseUpdate) {
|
|
610
630
|
subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
|
|
611
631
|
async (purchase: Purchase) => {
|
|
632
|
+
// Deliver first so store refresh latency cannot delay verification
|
|
633
|
+
// and transaction finalization in the host app.
|
|
634
|
+
try {
|
|
635
|
+
const callbackResult =
|
|
636
|
+
optionsRef.current?.onPurchaseSuccess?.(purchase);
|
|
637
|
+
void Promise.resolve(callbackResult).catch((e) => {
|
|
638
|
+
RnIapConsole.error('[useIAP] onPurchaseSuccess failed:', e);
|
|
639
|
+
});
|
|
640
|
+
} catch (e) {
|
|
641
|
+
RnIapConsole.error('[useIAP] onPurchaseSuccess failed:', e);
|
|
642
|
+
}
|
|
643
|
+
|
|
612
644
|
try {
|
|
613
645
|
await getActiveSubscriptionsInternal();
|
|
614
|
-
await getAvailablePurchasesInternal();
|
|
615
646
|
} catch (e) {
|
|
616
647
|
RnIapConsole.warn('[useIAP] post-purchase refresh failed:', e);
|
|
617
648
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
// mount-guarded inside the refresh helpers.
|
|
623
|
-
if (optionsRef.current?.onPurchaseSuccess) {
|
|
624
|
-
optionsRef.current.onPurchaseSuccess(purchase);
|
|
649
|
+
try {
|
|
650
|
+
await getAvailablePurchasesInternal();
|
|
651
|
+
} catch (e) {
|
|
652
|
+
RnIapConsole.warn('[useIAP] post-purchase refresh failed:', e);
|
|
625
653
|
}
|
|
626
654
|
},
|
|
627
655
|
optionsRef.current?.purchaseUpdatedListenerOptions,
|
|
@@ -657,7 +685,6 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
657
685
|
|
|
658
686
|
if (
|
|
659
687
|
Platform.OS === 'android' &&
|
|
660
|
-
optionsRef.current?.onUserChoiceBillingAndroid &&
|
|
661
688
|
!subscriptionsRef.current.userChoiceBillingAndroid
|
|
662
689
|
) {
|
|
663
690
|
subscriptionsRef.current.userChoiceBillingAndroid =
|
|
@@ -670,7 +697,6 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
670
697
|
|
|
671
698
|
if (
|
|
672
699
|
Platform.OS === 'android' &&
|
|
673
|
-
optionsRef.current?.onDeveloperProvidedBillingAndroid &&
|
|
674
700
|
!subscriptionsRef.current.developerProvidedBillingAndroid
|
|
675
701
|
) {
|
|
676
702
|
subscriptionsRef.current.developerProvidedBillingAndroid =
|
|
@@ -757,16 +783,22 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
757
783
|
);
|
|
758
784
|
|
|
759
785
|
const reconnect = useCallback(async (): Promise<boolean> => {
|
|
786
|
+
const generation = ++initializationGenerationRef.current;
|
|
760
787
|
const config = buildAndroidConfig();
|
|
761
788
|
|
|
762
789
|
try {
|
|
763
790
|
const result = await initConnection(config);
|
|
764
791
|
|
|
765
|
-
if (
|
|
792
|
+
if (
|
|
793
|
+
!isMountedRef.current ||
|
|
794
|
+
initializationGenerationRef.current !== generation
|
|
795
|
+
) {
|
|
766
796
|
return false;
|
|
767
797
|
}
|
|
768
798
|
|
|
769
799
|
if (result) {
|
|
800
|
+
// endConnection invalidates these hook-owned subscription handles.
|
|
801
|
+
cleanupListeners();
|
|
770
802
|
registerListeners();
|
|
771
803
|
setConnected(true);
|
|
772
804
|
return true;
|
|
@@ -775,6 +807,12 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
775
807
|
setConnected(false);
|
|
776
808
|
return false;
|
|
777
809
|
} catch (error) {
|
|
810
|
+
if (
|
|
811
|
+
!isMountedRef.current ||
|
|
812
|
+
initializationGenerationRef.current !== generation
|
|
813
|
+
) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
778
816
|
RnIapConsole.error('[useIAP] reconnect failed:', error);
|
|
779
817
|
cleanupListeners();
|
|
780
818
|
if (isMountedRef.current) {
|
package/src/index.kepler.ts
CHANGED