react-native-iap 16.0.1 → 16.1.0
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 +4 -32
- package/android/gradle.properties +3 -3
- package/android/src/main/cpp/cpp-adapter.cpp +4 -1
- package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +21 -13
- package/android/src/main/java/com/margelo/nitro/iap/NitroIapPackage.java +2 -6
- package/ios/HybridRnIap.swift +11 -6
- package/ios/RnIapHelper.swift +33 -1
- package/lib/module/hooks/useIAP.js +34 -13
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +17 -25
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.kepler.js +3 -2
- package/lib/module/index.kepler.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/available-purchases.js +59 -0
- package/lib/module/utils/available-purchases.js.map +1 -0
- package/lib/module/utils/type-bridge.js +56 -6
- package/lib/module/utils/type-bridge.js.map +1 -1
- package/lib/module/vega-adapter.js +13 -7
- package/lib/module/vega-adapter.js.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +1 -3
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +9 -11
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.kepler.d.ts +7 -7
- package/lib/typescript/src/index.kepler.d.ts.map +1 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +3 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +11 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/available-purchases.d.ts +8 -0
- package/lib/typescript/src/utils/available-purchases.d.ts.map +1 -0
- package/lib/typescript/src/utils/error.d.ts +1 -1
- package/lib/typescript/src/utils/error.d.ts.map +1 -1
- package/lib/typescript/src/utils/errorMapping.d.ts +1 -1
- package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -1
- package/lib/typescript/src/utils/purchase.d.ts +1 -1
- package/lib/typescript/src/utils/purchase.d.ts.map +1 -1
- package/lib/typescript/src/utils/type-bridge.d.ts +2 -2
- package/lib/typescript/src/utils/type-bridge.d.ts.map +1 -1
- package/lib/typescript/src/utils.d.ts +1 -1
- package/lib/typescript/src/utils.d.ts.map +1 -1
- package/lib/typescript/src/vega-adapter.d.ts +1 -1
- package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
- package/lib/typescript/src/vega.d.ts +1 -1
- package/lib/typescript/src/vega.d.ts.map +1 -1
- package/lib/typescript/src/vega.kepler.d.ts +1 -1
- package/lib/typescript/src/vega.kepler.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JNitroPurchase.hpp +11 -3
- package/nitrogen/generated/android/c++/JPurchaseAndroid.hpp +11 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Func_void_DeveloperProvidedBillingDetailsAndroid.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Func_void_NitroProduct.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Func_void_NitroPurchase.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Func_void_NitroPurchaseResult.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Func_void_UserChoiceBillingDetails.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchase.kt +14 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseAndroid.kt +14 -4
- package/nitrogen/generated/ios/swift/NitroPurchase.swift +75 -1
- package/nitrogen/generated/ios/swift/PurchaseAndroid.swift +75 -1
- package/nitrogen/generated/shared/c++/NitroPurchase.hpp +10 -2
- package/nitrogen/generated/shared/c++/PurchaseAndroid.hpp +10 -2
- package/openiap-versions.json +3 -3
- package/package.json +34 -37
- package/src/hooks/useIAP.ts +72 -41
- package/src/index.kepler.ts +5 -9
- package/src/index.ts +33 -61
- package/src/specs/RnIap.nitro.ts +2 -0
- package/src/types.ts +11 -0
- package/src/utils/available-purchases.ts +73 -0
- package/src/utils/type-bridge.ts +84 -11
- package/src/vega-adapter.ts +24 -3
- package/lib/module/types/react-test-renderer.d.js +0 -2
- package/lib/module/types/react-test-renderer.d.js.map +0 -1
- package/src/types/react-test-renderer.d.ts +0 -7
package/src/hooks/useIAP.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {useCallback, useEffect, useState, useRef} from 'react';
|
|
3
3
|
import {Platform} from 'react-native';
|
|
4
4
|
import {RnIapConsole} from '../utils/debug';
|
|
5
|
+
import {createPurchaseError} from '../utils/errorMapping';
|
|
5
6
|
|
|
6
7
|
// Internal modules
|
|
7
8
|
import {
|
|
@@ -46,8 +47,6 @@ import type {
|
|
|
46
47
|
VerifyPurchaseWithProviderProps,
|
|
47
48
|
VerifyPurchaseWithProviderResult,
|
|
48
49
|
PurchaseOptions,
|
|
49
|
-
} from '../types';
|
|
50
|
-
import type {
|
|
51
50
|
ActiveSubscription,
|
|
52
51
|
Product,
|
|
53
52
|
Purchase,
|
|
@@ -56,8 +55,8 @@ import type {
|
|
|
56
55
|
ProductSubscription,
|
|
57
56
|
MutationField,
|
|
58
57
|
QueryField,
|
|
58
|
+
MutationFinishTransactionArgs,
|
|
59
59
|
} from '../types';
|
|
60
|
-
import type {MutationFinishTransactionArgs} from '../types';
|
|
61
60
|
|
|
62
61
|
// Types for event subscriptions
|
|
63
62
|
interface EventSubscription {
|
|
@@ -353,7 +352,8 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
353
352
|
}>({});
|
|
354
353
|
|
|
355
354
|
// Track if component is mounted to prevent listener leaks on early unmount
|
|
356
|
-
const isMountedRef = useRef<boolean>(
|
|
355
|
+
const isMountedRef = useRef<boolean>(false);
|
|
356
|
+
const initializationGenerationRef = useRef(0);
|
|
357
357
|
|
|
358
358
|
const subscriptionsRefState = useRef<ProductSubscription[]>([]);
|
|
359
359
|
|
|
@@ -467,6 +467,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
467
467
|
} catch (error) {
|
|
468
468
|
RnIapConsole.error('Error fetching available purchases:', error);
|
|
469
469
|
invokeOnError(error);
|
|
470
|
+
throw error;
|
|
470
471
|
}
|
|
471
472
|
},
|
|
472
473
|
[invokeOnError],
|
|
@@ -483,7 +484,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
483
484
|
} catch (error) {
|
|
484
485
|
RnIapConsole.error('Error getting active subscriptions:', error);
|
|
485
486
|
invokeOnError(error);
|
|
486
|
-
|
|
487
|
+
throw error;
|
|
487
488
|
}
|
|
488
489
|
},
|
|
489
490
|
[invokeOnError],
|
|
@@ -496,7 +497,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
496
497
|
} catch (error) {
|
|
497
498
|
RnIapConsole.error('Error checking active subscriptions:', error);
|
|
498
499
|
invokeOnError(error);
|
|
499
|
-
|
|
500
|
+
throw error;
|
|
500
501
|
}
|
|
501
502
|
},
|
|
502
503
|
[invokeOnError],
|
|
@@ -523,16 +524,26 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
523
524
|
|
|
524
525
|
const restorePurchases = useCallback(
|
|
525
526
|
async (options?: PurchaseOptions): Promise<void> => {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
await syncIOS();
|
|
527
|
+
if (Platform.OS === 'ios') {
|
|
528
|
+
try {
|
|
529
|
+
const synced = await syncIOS();
|
|
530
|
+
if (!synced) {
|
|
531
|
+
throw createPurchaseError({
|
|
532
|
+
code: ErrorCode.SyncError,
|
|
533
|
+
message: 'App Store purchase sync did not complete',
|
|
534
|
+
platform: 'ios',
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
} catch (error) {
|
|
538
|
+
RnIapConsole.warn('Failed to restore purchases:', error);
|
|
539
|
+
invokeOnError(error);
|
|
540
|
+
throw error;
|
|
529
541
|
}
|
|
530
|
-
|
|
531
|
-
await getAvailablePurchasesInternal(options);
|
|
532
|
-
} catch (error) {
|
|
533
|
-
RnIapConsole.warn('Failed to restore purchases:', error);
|
|
534
|
-
invokeOnError(error);
|
|
535
542
|
}
|
|
543
|
+
|
|
544
|
+
// The query helper reports and rethrows its own error, avoiding a second
|
|
545
|
+
// onError call while keeping restore failure observable to the caller.
|
|
546
|
+
await getAvailablePurchasesInternal(options);
|
|
536
547
|
},
|
|
537
548
|
[getAvailablePurchasesInternal, invokeOnError],
|
|
538
549
|
);
|
|
@@ -681,37 +692,56 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
681
692
|
subscriptionsRef.current.subscriptionBillingIssue = undefined;
|
|
682
693
|
}, []);
|
|
683
694
|
|
|
684
|
-
const initIapWithSubscriptions = useCallback(
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if (!isMountedRef.current) {
|
|
695
|
+
const initIapWithSubscriptions = useCallback(
|
|
696
|
+
async (generation: number): Promise<void> => {
|
|
697
|
+
if (
|
|
698
|
+
!isMountedRef.current ||
|
|
699
|
+
initializationGenerationRef.current !== generation
|
|
700
|
+
) {
|
|
691
701
|
return;
|
|
692
702
|
}
|
|
693
703
|
|
|
694
|
-
|
|
695
|
-
cleanupListeners();
|
|
696
|
-
setConnected(false);
|
|
697
|
-
RnIapConsole.warn('[useIAP] initConnection returned false');
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
704
|
+
const config = buildAndroidConfig();
|
|
700
705
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
706
|
+
try {
|
|
707
|
+
const result = await initConnection(config);
|
|
708
|
+
|
|
709
|
+
if (
|
|
710
|
+
!isMountedRef.current ||
|
|
711
|
+
initializationGenerationRef.current !== generation
|
|
712
|
+
) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (!result) {
|
|
717
|
+
cleanupListeners();
|
|
718
|
+
setConnected(false);
|
|
719
|
+
RnIapConsole.warn('[useIAP] initConnection returned false');
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Android retains events emitted during connection setup in bounded
|
|
724
|
+
// native queues; registration flushes that backlog after Nitro is ready.
|
|
725
|
+
// Other platforms preserve the existing post-init listener ordering.
|
|
726
|
+
registerListeners();
|
|
727
|
+
setConnected(true);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (
|
|
730
|
+
!isMountedRef.current ||
|
|
731
|
+
initializationGenerationRef.current !== generation
|
|
732
|
+
) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
RnIapConsole.error('initConnection failed:', error);
|
|
736
|
+
cleanupListeners();
|
|
737
|
+
if (isMountedRef.current) {
|
|
738
|
+
setConnected(false);
|
|
739
|
+
}
|
|
740
|
+
invokeOnError(error);
|
|
711
741
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
742
|
+
},
|
|
743
|
+
[buildAndroidConfig, registerListeners, cleanupListeners, invokeOnError],
|
|
744
|
+
);
|
|
715
745
|
|
|
716
746
|
const reconnect = useCallback(async (): Promise<boolean> => {
|
|
717
747
|
const config = buildAndroidConfig();
|
|
@@ -743,8 +773,9 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
743
773
|
}, [buildAndroidConfig, registerListeners, cleanupListeners, invokeOnError]);
|
|
744
774
|
|
|
745
775
|
useEffect(() => {
|
|
776
|
+
const generation = ++initializationGenerationRef.current;
|
|
746
777
|
isMountedRef.current = true;
|
|
747
|
-
initIapWithSubscriptions();
|
|
778
|
+
void Promise.resolve().then(() => initIapWithSubscriptions(generation));
|
|
748
779
|
|
|
749
780
|
return () => {
|
|
750
781
|
isMountedRef.current = false;
|
package/src/index.kepler.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
validateNitroProduct,
|
|
27
27
|
validateNitroPurchase,
|
|
28
28
|
} from './utils/type-bridge';
|
|
29
|
+
import {convertAndroidPurchasesOrThrow} from './utils/available-purchases';
|
|
29
30
|
|
|
30
31
|
export * from './types';
|
|
31
32
|
export * from './utils/error';
|
|
@@ -128,7 +129,8 @@ export const fetchProducts = async (
|
|
|
128
129
|
const normalizedType = normalizeProductQueryType(type);
|
|
129
130
|
const nitroProducts = await getModule().fetchProducts(skus, normalizedType);
|
|
130
131
|
return mapProducts(nitroProducts, normalizedType) as
|
|
131
|
-
|
|
132
|
+
| Product[]
|
|
133
|
+
| ProductSubscription[];
|
|
132
134
|
};
|
|
133
135
|
|
|
134
136
|
export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
@@ -152,9 +154,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
152
154
|
const result = await getModule().requestPurchase(nitroRequest);
|
|
153
155
|
if (!Array.isArray(result)) return null;
|
|
154
156
|
const purchases = result as unknown as NitroPurchase[];
|
|
155
|
-
return purchases
|
|
156
|
-
.filter(validateNitroPurchase)
|
|
157
|
-
.map((purchase) => convertNitroPurchaseToPurchase(purchase));
|
|
157
|
+
return convertAndroidPurchasesOrThrow(purchases);
|
|
158
158
|
};
|
|
159
159
|
|
|
160
160
|
export const getAvailablePurchases: QueryField<
|
|
@@ -167,11 +167,7 @@ export const getAvailablePurchases: QueryField<
|
|
|
167
167
|
};
|
|
168
168
|
return getModule()
|
|
169
169
|
.getAvailablePurchases(nitroOptions)
|
|
170
|
-
.then(
|
|
171
|
-
purchases
|
|
172
|
-
.filter(validateNitroPurchase)
|
|
173
|
-
.map(convertNitroPurchaseToPurchase),
|
|
174
|
-
);
|
|
170
|
+
.then(convertAndroidPurchasesOrThrow);
|
|
175
171
|
};
|
|
176
172
|
|
|
177
173
|
export const finishTransaction: MutationField<'finishTransaction'> = async (
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// External dependencies
|
|
2
2
|
import {Platform} from 'react-native';
|
|
3
|
-
//
|
|
4
|
-
import 'react-native-nitro-modules';
|
|
3
|
+
// Importing NitroModules installs its dispatcher before IAP is used (no-op in tests).
|
|
5
4
|
import {NitroModules} from 'react-native-nitro-modules';
|
|
6
5
|
|
|
7
6
|
// Internal modules
|
|
@@ -16,9 +15,21 @@ import type {
|
|
|
16
15
|
} from './specs/RnIap.nitro';
|
|
17
16
|
import {ErrorCode} from './types';
|
|
18
17
|
import type {
|
|
18
|
+
AppTransaction,
|
|
19
19
|
AndroidSubscriptionOfferInput,
|
|
20
|
+
ActiveSubscription,
|
|
21
|
+
BillingChoiceInfoAndroid,
|
|
22
|
+
BillingProgramAndroid,
|
|
23
|
+
BillingProgramInformationDialogParamsAndroid,
|
|
24
|
+
BillingProgramReportingDetailsAndroid,
|
|
25
|
+
BillingResultAndroid,
|
|
26
|
+
DeveloperBillingTypeAndroid,
|
|
27
|
+
DeveloperProvidedBillingDetailsAndroid,
|
|
20
28
|
DiscountOfferInputIOS,
|
|
21
29
|
FetchProductsResult,
|
|
30
|
+
GetBillingChoiceInfoParamsAndroid,
|
|
31
|
+
InAppMessageParamsAndroid,
|
|
32
|
+
InAppMessageResultAndroid,
|
|
22
33
|
MutationField,
|
|
23
34
|
Product,
|
|
24
35
|
ProductIOS,
|
|
@@ -29,7 +40,6 @@ import type {
|
|
|
29
40
|
PurchaseUpdatedListenerOptions,
|
|
30
41
|
PurchaseIOS,
|
|
31
42
|
QueryField,
|
|
32
|
-
AppTransaction,
|
|
33
43
|
VerifyPurchaseResultAndroid,
|
|
34
44
|
VerifyPurchaseResultIOS,
|
|
35
45
|
RequestPurchaseAndroidProps,
|
|
@@ -38,8 +48,6 @@ import type {
|
|
|
38
48
|
RequestSubscriptionAndroidProps,
|
|
39
49
|
RequestSubscriptionIosProps,
|
|
40
50
|
RequestSubscriptionPropsByPlatforms,
|
|
41
|
-
ActiveSubscription,
|
|
42
|
-
DeveloperProvidedBillingDetailsAndroid,
|
|
43
51
|
UserChoiceBillingDetails,
|
|
44
52
|
} from './types';
|
|
45
53
|
import {
|
|
@@ -59,26 +67,18 @@ import {
|
|
|
59
67
|
import {RnIapConsole} from './utils/debug';
|
|
60
68
|
import {getSuccessFromPurchaseVariant} from './utils/purchase';
|
|
61
69
|
import {parseAppTransactionPayload} from './utils';
|
|
70
|
+
import {
|
|
71
|
+
convertAndroidPurchasesOrThrow,
|
|
72
|
+
convertApplePurchasesOrThrow,
|
|
73
|
+
} from './utils/available-purchases';
|
|
62
74
|
import {getVegaIapModule, isVegaOS} from './vega';
|
|
63
75
|
|
|
64
76
|
// ------------------------------
|
|
65
77
|
// Billing Programs API (Android 8.2.0+)
|
|
66
78
|
// ------------------------------
|
|
67
79
|
|
|
68
|
-
//
|
|
80
|
+
// BillingProgramAndroid, ExternalLinkLaunchModeAndroid, and ExternalLinkTypeAndroid
|
|
69
81
|
// are exported from './types' (auto-generated from openiap-gql).
|
|
70
|
-
// Import them here for use in this file's interfaces and functions.
|
|
71
|
-
import type {
|
|
72
|
-
BillingChoiceInfoAndroid,
|
|
73
|
-
BillingProgramAndroid,
|
|
74
|
-
BillingProgramInformationDialogParamsAndroid,
|
|
75
|
-
BillingProgramReportingDetailsAndroid,
|
|
76
|
-
BillingResultAndroid,
|
|
77
|
-
DeveloperBillingTypeAndroid,
|
|
78
|
-
GetBillingChoiceInfoParamsAndroid,
|
|
79
|
-
InAppMessageParamsAndroid,
|
|
80
|
-
InAppMessageResultAndroid,
|
|
81
|
-
} from './types';
|
|
82
82
|
|
|
83
83
|
// Export all types
|
|
84
84
|
export type {
|
|
@@ -991,14 +991,7 @@ export const getAvailablePurchases: QueryField<
|
|
|
991
991
|
const nitroPurchases =
|
|
992
992
|
await IAP.instance.getAvailablePurchases(nitroOptions);
|
|
993
993
|
|
|
994
|
-
|
|
995
|
-
if (validPurchases.length !== nitroPurchases.length) {
|
|
996
|
-
RnIapConsole.warn(
|
|
997
|
-
`[getAvailablePurchases] Some purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`,
|
|
998
|
-
);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
return validPurchases.map(convertNitroPurchaseToPurchase);
|
|
994
|
+
return convertApplePurchasesOrThrow(nitroPurchases);
|
|
1002
995
|
} else if (isAndroidStoreRuntime()) {
|
|
1003
996
|
const includeSuspended = Boolean(
|
|
1004
997
|
options?.includeSuspendedAndroid ?? false,
|
|
@@ -1008,14 +1001,7 @@ export const getAvailablePurchases: QueryField<
|
|
|
1008
1001
|
const nitroPurchases = await IAP.instance.getAvailablePurchases({
|
|
1009
1002
|
android: {includeSuspended},
|
|
1010
1003
|
});
|
|
1011
|
-
|
|
1012
|
-
if (validPurchases.length !== nitroPurchases.length) {
|
|
1013
|
-
RnIapConsole.warn(
|
|
1014
|
-
`[getAvailablePurchases] Some Vega purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`,
|
|
1015
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
return validPurchases.map(convertNitroPurchaseToPurchase);
|
|
1004
|
+
return convertAndroidPurchasesOrThrow(nitroPurchases);
|
|
1019
1005
|
}
|
|
1020
1006
|
|
|
1021
1007
|
// For Android Play/Horizon/Fire OS, query in-app items and subscriptions separately.
|
|
@@ -1026,16 +1012,8 @@ export const getAvailablePurchases: QueryField<
|
|
|
1026
1012
|
android: {type: 'subs', includeSuspended},
|
|
1027
1013
|
});
|
|
1028
1014
|
|
|
1029
|
-
// Validate and convert both sets of purchases
|
|
1030
1015
|
const allNitroPurchases = [...inappNitroPurchases, ...subsNitroPurchases];
|
|
1031
|
-
|
|
1032
|
-
if (validPurchases.length !== allNitroPurchases.length) {
|
|
1033
|
-
RnIapConsole.warn(
|
|
1034
|
-
`[getAvailablePurchases] Some Android purchases failed validation: ${allNitroPurchases.length - validPurchases.length} invalid`,
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
return validPurchases.map(convertNitroPurchaseToPurchase);
|
|
1016
|
+
return convertAndroidPurchasesOrThrow(allNitroPurchases);
|
|
1039
1017
|
} else {
|
|
1040
1018
|
throw unsupportedPlatformError();
|
|
1041
1019
|
}
|
|
@@ -1298,11 +1276,7 @@ export const getPendingTransactionsIOS: QueryField<
|
|
|
1298
1276
|
|
|
1299
1277
|
try {
|
|
1300
1278
|
const nitroPurchases = await IAP.instance.getPendingTransactionsIOS();
|
|
1301
|
-
return nitroPurchases
|
|
1302
|
-
.map(convertNitroPurchaseToPurchase)
|
|
1303
|
-
.filter(
|
|
1304
|
-
(purchase): purchase is PurchaseIOS => purchase.store === 'apple',
|
|
1305
|
-
);
|
|
1279
|
+
return convertApplePurchasesOrThrow(nitroPurchases);
|
|
1306
1280
|
} catch (error) {
|
|
1307
1281
|
const parsedError = parseErrorAndLogIfNeeded(
|
|
1308
1282
|
'[getPendingTransactionsIOS] Failed:',
|
|
@@ -1331,11 +1305,7 @@ export const getAllTransactionsIOS: QueryField<
|
|
|
1331
1305
|
|
|
1332
1306
|
try {
|
|
1333
1307
|
const nitroPurchases = await IAP.instance.getAllTransactionsIOS();
|
|
1334
|
-
return nitroPurchases
|
|
1335
|
-
.map(convertNitroPurchaseToPurchase)
|
|
1336
|
-
.filter(
|
|
1337
|
-
(purchase): purchase is PurchaseIOS => purchase.store === 'apple',
|
|
1338
|
-
);
|
|
1308
|
+
return convertApplePurchasesOrThrow(nitroPurchases);
|
|
1339
1309
|
} catch (error) {
|
|
1340
1310
|
const parsedError = parseErrorAndLogIfNeeded(
|
|
1341
1311
|
'[getAllTransactionsIOS] Failed:',
|
|
@@ -1366,11 +1336,7 @@ export const showManageSubscriptionsIOS: MutationField<
|
|
|
1366
1336
|
|
|
1367
1337
|
try {
|
|
1368
1338
|
const nitroPurchases = await IAP.instance.showManageSubscriptionsIOS();
|
|
1369
|
-
return nitroPurchases
|
|
1370
|
-
.map(convertNitroPurchaseToPurchase)
|
|
1371
|
-
.filter(
|
|
1372
|
-
(purchase): purchase is PurchaseIOS => purchase.store === 'apple',
|
|
1373
|
-
);
|
|
1339
|
+
return convertApplePurchasesOrThrow(nitroPurchases);
|
|
1374
1340
|
} catch (error) {
|
|
1375
1341
|
const parsedError = parseErrorAndLogIfNeeded(
|
|
1376
1342
|
'[showManageSubscriptionsIOS] Failed:',
|
|
@@ -1627,7 +1593,14 @@ export const endConnection: MutationField<'endConnection'> = async () => {
|
|
|
1627
1593
|
export const restorePurchases: MutationField<'restorePurchases'> = async () => {
|
|
1628
1594
|
try {
|
|
1629
1595
|
if (Platform.OS === 'ios') {
|
|
1630
|
-
await syncIOS();
|
|
1596
|
+
const synced = await syncIOS();
|
|
1597
|
+
if (!synced) {
|
|
1598
|
+
throw createPurchaseError({
|
|
1599
|
+
code: ErrorCode.SyncError,
|
|
1600
|
+
message: 'App Store purchase sync did not complete',
|
|
1601
|
+
platform: 'ios',
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1631
1604
|
}
|
|
1632
1605
|
|
|
1633
1606
|
await getAvailablePurchases({
|
|
@@ -2485,8 +2458,7 @@ export const getActiveSubscriptions: QueryField<
|
|
|
2485
2458
|
sub.renewalInfoIOS.autoRenewPreference ?? null,
|
|
2486
2459
|
bundleOriginalTransactionId:
|
|
2487
2460
|
sub.renewalInfoIOS.bundleOriginalTransactionId ?? null,
|
|
2488
|
-
bundleProductId:
|
|
2489
|
-
sub.renewalInfoIOS.bundleProductId ?? null,
|
|
2461
|
+
bundleProductId: sub.renewalInfoIOS.bundleProductId ?? null,
|
|
2490
2462
|
bundleSubscriptionGroupId:
|
|
2491
2463
|
sub.renewalInfoIOS.bundleSubscriptionGroupId ?? null,
|
|
2492
2464
|
commitmentInfo: sub.renewalInfoIOS.commitmentInfo ?? null,
|
package/src/specs/RnIap.nitro.ts
CHANGED
|
@@ -590,6 +590,8 @@ export interface NitroPurchase {
|
|
|
590
590
|
developerPayloadAndroid?: string | null;
|
|
591
591
|
isSuspendedAndroid?: null | boolean;
|
|
592
592
|
pendingPurchaseUpdateAndroid?: PendingPurchaseUpdateAndroid | null;
|
|
593
|
+
userIdAmazon?: string | null;
|
|
594
|
+
userMarketplaceAmazon?: string | null;
|
|
593
595
|
}
|
|
594
596
|
|
|
595
597
|
/**
|
package/src/types.ts
CHANGED
|
@@ -1214,6 +1214,17 @@ export interface PurchaseAndroid extends PurchaseCommon {
|
|
|
1214
1214
|
/** Unix timestamp in milliseconds since January 1, 1970 UTC. */
|
|
1215
1215
|
transactionDate: number;
|
|
1216
1216
|
transactionId?: (string | null);
|
|
1217
|
+
/**
|
|
1218
|
+
* Amazon Appstore user id (PurchaseResponse.getUserData().getUserId()).
|
|
1219
|
+
* Only populated on the Amazon flavor; required for server-side Amazon RVS
|
|
1220
|
+
* receipt verification (userId + receiptId). Null on Google Play and Horizon.
|
|
1221
|
+
*/
|
|
1222
|
+
userIdAmazon?: (string | null);
|
|
1223
|
+
/**
|
|
1224
|
+
* Amazon Appstore marketplace (PurchaseResponse.getUserData().getMarketplace()),
|
|
1225
|
+
* for example "US" or "FR". Only populated on the Amazon flavor.
|
|
1226
|
+
*/
|
|
1227
|
+
userMarketplaceAmazon?: (string | null);
|
|
1217
1228
|
}
|
|
1218
1229
|
|
|
1219
1230
|
export interface PurchaseCommon {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {ErrorCode} from '../types';
|
|
2
|
+
import type {Purchase, PurchaseIOS} from '../types';
|
|
3
|
+
import {createPurchaseError} from './errorMapping';
|
|
4
|
+
import {
|
|
5
|
+
convertNitroPurchaseToPurchase,
|
|
6
|
+
validateNitroPurchase,
|
|
7
|
+
} from './type-bridge';
|
|
8
|
+
|
|
9
|
+
const ANDROID_STORES = new Set(['google', 'amazon', 'horizon']);
|
|
10
|
+
|
|
11
|
+
/** Decode an authoritative native purchase list atomically. */
|
|
12
|
+
export const convertAvailablePurchasesOrThrow = (
|
|
13
|
+
purchases: unknown,
|
|
14
|
+
): Purchase[] => {
|
|
15
|
+
if (!Array.isArray(purchases)) {
|
|
16
|
+
throw createPurchaseError({
|
|
17
|
+
code: ErrorCode.BillingResponseJsonParseError,
|
|
18
|
+
message: 'Malformed purchase list returned by the native bridge',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return purchases.map((purchase, index) => {
|
|
23
|
+
if (!validateNitroPurchase(purchase)) {
|
|
24
|
+
throw createPurchaseError({
|
|
25
|
+
code: ErrorCode.BillingResponseJsonParseError,
|
|
26
|
+
message: `Malformed purchase payload returned by the native bridge at index ${index}`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
return convertNitroPurchaseToPurchase(purchase);
|
|
32
|
+
} catch {
|
|
33
|
+
throw createPurchaseError({
|
|
34
|
+
code: ErrorCode.BillingResponseJsonParseError,
|
|
35
|
+
message: `Failed to decode native purchase payload at index ${index}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Decode an authoritative StoreKit purchase list without filtering entries. */
|
|
42
|
+
export const convertApplePurchasesOrThrow = (
|
|
43
|
+
purchases: unknown,
|
|
44
|
+
): PurchaseIOS[] => {
|
|
45
|
+
const decoded = convertAvailablePurchasesOrThrow(purchases);
|
|
46
|
+
const invalidIndex = decoded.findIndex(
|
|
47
|
+
(purchase) => purchase.store !== 'apple',
|
|
48
|
+
);
|
|
49
|
+
if (invalidIndex !== -1) {
|
|
50
|
+
throw createPurchaseError({
|
|
51
|
+
code: ErrorCode.BillingResponseJsonParseError,
|
|
52
|
+
message: `Native StoreKit bridge returned a non-Apple purchase at index ${invalidIndex}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return decoded as PurchaseIOS[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Decode an authoritative Android-family purchase list without foreign stores. */
|
|
59
|
+
export const convertAndroidPurchasesOrThrow = (
|
|
60
|
+
purchases: unknown,
|
|
61
|
+
): Purchase[] => {
|
|
62
|
+
const decoded = convertAvailablePurchasesOrThrow(purchases);
|
|
63
|
+
const invalidIndex = decoded.findIndex(
|
|
64
|
+
(purchase) => !ANDROID_STORES.has(purchase.store),
|
|
65
|
+
);
|
|
66
|
+
if (invalidIndex !== -1) {
|
|
67
|
+
throw createPurchaseError({
|
|
68
|
+
code: ErrorCode.BillingResponseJsonParseError,
|
|
69
|
+
message: `Native Android bridge returned a foreign purchase at index ${invalidIndex}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return decoded;
|
|
73
|
+
};
|
package/src/utils/type-bridge.ts
CHANGED
|
@@ -19,8 +19,6 @@ import type {
|
|
|
19
19
|
ProductTypeIOS,
|
|
20
20
|
PurchaseState,
|
|
21
21
|
SubscriptionPeriodIOS,
|
|
22
|
-
} from '../types';
|
|
23
|
-
import type {
|
|
24
22
|
Product,
|
|
25
23
|
ProductSubscription,
|
|
26
24
|
Purchase,
|
|
@@ -504,6 +502,16 @@ export function convertNitroPurchaseToPurchase(
|
|
|
504
502
|
? legacyAndroidId
|
|
505
503
|
: null);
|
|
506
504
|
|
|
505
|
+
// Amazon identity metadata must never leak onto other stores' purchases.
|
|
506
|
+
const amazonUserId =
|
|
507
|
+
nitroPurchase.userIdAmazon != null && store === STORE_AMAZON
|
|
508
|
+
? toNullableString(nitroPurchase.userIdAmazon)
|
|
509
|
+
: null;
|
|
510
|
+
const amazonUserMarketplace =
|
|
511
|
+
nitroPurchase.userMarketplaceAmazon != null && store === STORE_AMAZON
|
|
512
|
+
? toNullableString(nitroPurchase.userMarketplaceAmazon)
|
|
513
|
+
: null;
|
|
514
|
+
|
|
507
515
|
const androidPurchase: PurchaseAndroid = {
|
|
508
516
|
id: nitroPurchase.id,
|
|
509
517
|
productId: nitroPurchase.productId,
|
|
@@ -540,6 +548,8 @@ export function convertNitroPurchaseToPurchase(
|
|
|
540
548
|
isSuspendedAndroid: toNullableBoolean(nitroPurchase.isSuspendedAndroid),
|
|
541
549
|
pendingPurchaseUpdateAndroid:
|
|
542
550
|
nitroPurchase.pendingPurchaseUpdateAndroid ?? null,
|
|
551
|
+
userIdAmazon: amazonUserId,
|
|
552
|
+
userMarketplaceAmazon: amazonUserMarketplace,
|
|
543
553
|
};
|
|
544
554
|
|
|
545
555
|
return androidPurchase;
|
|
@@ -592,17 +602,80 @@ export function validateNitroPurchase(nitroPurchase: NitroPurchase): boolean {
|
|
|
592
602
|
return false;
|
|
593
603
|
}
|
|
594
604
|
|
|
595
|
-
const
|
|
596
|
-
|
|
605
|
+
const invalidField = (field: string): false => {
|
|
606
|
+
RnIapConsole.error(`NitroPurchase has invalid required field: ${field}`);
|
|
607
|
+
return false;
|
|
608
|
+
};
|
|
609
|
+
if (typeof nitroPurchase.id !== 'string' || !nitroPurchase.id) {
|
|
610
|
+
return invalidField('id');
|
|
611
|
+
}
|
|
612
|
+
if (typeof nitroPurchase.productId !== 'string' || !nitroPurchase.productId) {
|
|
613
|
+
return invalidField('productId');
|
|
614
|
+
}
|
|
615
|
+
if (
|
|
616
|
+
typeof nitroPurchase.transactionDate !== 'number' ||
|
|
617
|
+
!Number.isFinite(nitroPurchase.transactionDate)
|
|
618
|
+
) {
|
|
619
|
+
return invalidField('transactionDate');
|
|
620
|
+
}
|
|
621
|
+
if (typeof nitroPurchase.store !== 'string' || !nitroPurchase.store) {
|
|
622
|
+
return invalidField('store');
|
|
623
|
+
}
|
|
624
|
+
if (
|
|
625
|
+
typeof nitroPurchase.purchaseState !== 'string' ||
|
|
626
|
+
!nitroPurchase.purchaseState
|
|
627
|
+
) {
|
|
628
|
+
return invalidField('purchaseState');
|
|
629
|
+
}
|
|
630
|
+
if (
|
|
631
|
+
typeof nitroPurchase.quantity !== 'number' ||
|
|
632
|
+
!Number.isInteger(nitroPurchase.quantity)
|
|
633
|
+
) {
|
|
634
|
+
return invalidField('quantity');
|
|
635
|
+
}
|
|
636
|
+
if (typeof nitroPurchase.isAutoRenewing !== 'boolean') {
|
|
637
|
+
return invalidField('isAutoRenewing');
|
|
638
|
+
}
|
|
639
|
+
if (
|
|
640
|
+
nitroPurchase.store === 'apple' &&
|
|
641
|
+
(typeof nitroPurchase.transactionId !== 'string' ||
|
|
642
|
+
!nitroPurchase.transactionId)
|
|
643
|
+
) {
|
|
644
|
+
return invalidField('transactionId');
|
|
645
|
+
}
|
|
646
|
+
const candidate = nitroPurchase as unknown as Record<string, unknown>;
|
|
647
|
+
if (
|
|
648
|
+
candidate.ids != null &&
|
|
649
|
+
(!Array.isArray(candidate.ids) ||
|
|
650
|
+
candidate.ids.some((id) => typeof id !== 'string'))
|
|
651
|
+
) {
|
|
652
|
+
return invalidField('ids');
|
|
653
|
+
}
|
|
654
|
+
for (const field of [
|
|
655
|
+
'pendingPurchaseUpdateAndroid',
|
|
656
|
+
'renewalInfoIOS',
|
|
657
|
+
'commitmentInfoIOS',
|
|
658
|
+
'advancedCommerceInfoIOS',
|
|
659
|
+
]) {
|
|
660
|
+
const nested = candidate[field];
|
|
597
661
|
if (
|
|
598
|
-
|
|
599
|
-
|
|
662
|
+
nested != null &&
|
|
663
|
+
(typeof nested !== 'object' || Array.isArray(nested))
|
|
600
664
|
) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
665
|
+
return invalidField(field);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (candidate.offerIOS != null) {
|
|
669
|
+
if (typeof candidate.offerIOS !== 'string') {
|
|
670
|
+
return invalidField('offerIOS');
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
const offer = JSON.parse(candidate.offerIOS);
|
|
674
|
+
if (offer == null || typeof offer !== 'object' || Array.isArray(offer)) {
|
|
675
|
+
return invalidField('offerIOS');
|
|
676
|
+
}
|
|
677
|
+
} catch {
|
|
678
|
+
return invalidField('offerIOS');
|
|
606
679
|
}
|
|
607
680
|
}
|
|
608
681
|
|