@namiml/expo-nami-iap 3.4.0-dev.202605060437

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.
Files changed (43) hide show
  1. package/README.md +121 -0
  2. package/android/build.gradle +46 -0
  3. package/android/gradle.properties +1 -0
  4. package/android/libs/.gitkeep +0 -0
  5. package/android/src/amazon/java/ml/nami/expo/iap/amazon/AmazonIapBackend.kt +427 -0
  6. package/android/src/amazon/java/ml/nami/expo/iap/amazon/AmazonProductMapper.kt +119 -0
  7. package/android/src/amazon/java/ml/nami/expo/iap/amazon/AmazonPurchasingListener.kt +35 -0
  8. package/android/src/amazon/java/ml/nami/expo/iap/amazon/ExpoNamiIapAmazon.kt +46 -0
  9. package/android/src/amazon/java/ml/nami/expo/iap/amazon/ExpoNamiIapAmazonReceiver.kt +48 -0
  10. package/android/src/google/java/ml/nami/expo/iap/google/PlayBillingBackend.kt +446 -0
  11. package/android/src/google/java/ml/nami/expo/iap/google/PlayBillingConnection.kt +104 -0
  12. package/android/src/main/AndroidManifest.xml +1 -0
  13. package/android/src/main/java/ml/nami/expo/iap/Errors.kt +94 -0
  14. package/android/src/main/java/ml/nami/expo/iap/ExpoNamiIapModule.kt +56 -0
  15. package/android/src/main/java/ml/nami/expo/iap/IapBackend.kt +72 -0
  16. package/android/src/main/java/ml/nami/expo/iap/ProductMapper.kt +191 -0
  17. package/android/src/test/amazon/java/ml/nami/expo/iap/amazon/AmazonIapBackendTest.kt +974 -0
  18. package/android/src/test/java/ml/nami/expo/iap/ErrorsTest.kt +184 -0
  19. package/android/src/test/java/ml/nami/expo/iap/ExpoNamiIapModuleTest.kt +86 -0
  20. package/android/src/test/java/ml/nami/expo/iap/PlaceholderTest.kt +8 -0
  21. package/android/src/test/java/ml/nami/expo/iap/ProductMapperTest.kt +335 -0
  22. package/android/src/test/java/ml/nami/expo/iap/google/PlayBillingBackendTest.kt +841 -0
  23. package/android/src/testAmazon/java/ml/nami/expo/iap/ExpoNamiIapModuleAmazonFlavorTest.kt +71 -0
  24. package/dist/index.cjs +56 -0
  25. package/dist/index.d.ts +62 -0
  26. package/dist/index.mjs +45 -0
  27. package/expo-module.config.json +9 -0
  28. package/ios/Errors.swift +178 -0
  29. package/ios/ExpoNamiIap.podspec +14 -0
  30. package/ios/ExpoNamiIapModule.swift +81 -0
  31. package/ios/ProductMapper.swift +130 -0
  32. package/ios/PromotionalOfferSigner.swift +78 -0
  33. package/ios/StoreKit2Helper.swift +252 -0
  34. package/ios/Tests/ErrorsTests.swift +189 -0
  35. package/ios/Tests/ProductMapperTests.swift +165 -0
  36. package/ios/Tests/Resources/Products.storekit +210 -0
  37. package/ios/Tests/StoreKit2HelperTests.swift +360 -0
  38. package/ios/TransactionObserver.swift +54 -0
  39. package/package.json +64 -0
  40. package/src/ExpoNamiIapModule.kepler.ts +200 -0
  41. package/src/ExpoNamiIapModule.ts +18 -0
  42. package/src/index.ts +63 -0
  43. package/src/types.ts +46 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * ExpoNamiIapModule — Kepler/Vega implementation.
3
+ *
4
+ * Metro resolves this file instead of `index.ts` when `Platform.OS === 'kepler'`.
5
+ * It wraps `@amazon-devices/keplerscript-appstore-iap-lib` to provide the same
6
+ * public API surface as the native-backed module.
7
+ */
8
+
9
+ import type {
10
+ ConfigureOptions,
11
+ EligibilityResult,
12
+ ExpoNamiIapError,
13
+ ExpoNamiIapProduct,
14
+ ExpoNamiIapTransaction,
15
+ PurchaseOptions,
16
+ } from './types';
17
+
18
+ export * from './types';
19
+
20
+ /* ---------- Kepler IAP library (lazy require) ---------- */
21
+
22
+ function getKeplerIAP() {
23
+ return require('@amazon-devices/keplerscript-appstore-iap-lib');
24
+ }
25
+
26
+ /* ---------- In-memory event emitter ---------- */
27
+
28
+ type TransactionListener = (tx: ExpoNamiIapTransaction) => void;
29
+ type ErrorListener = (err: ExpoNamiIapError) => void;
30
+
31
+ const transactionListeners = new Set<TransactionListener>();
32
+ const errorListeners = new Set<ErrorListener>();
33
+
34
+ function emitTransaction(tx: ExpoNamiIapTransaction): void {
35
+ for (const cb of transactionListeners) {
36
+ cb(tx);
37
+ }
38
+ }
39
+
40
+ /* ---------- Public API ---------- */
41
+
42
+ /**
43
+ * No-op on Kepler — no native configuration needed.
44
+ */
45
+ export function configure(_options: ConfigureOptions): void {
46
+ // Intentional no-op
47
+ }
48
+
49
+ /**
50
+ * No-op on Kepler — the IAP library is promise-based, no listener to register.
51
+ */
52
+ export async function initConnection(): Promise<void> {
53
+ // Intentional no-op
54
+ }
55
+
56
+ /**
57
+ * No-op on Kepler — nothing to tear down.
58
+ */
59
+ export async function endConnection(): Promise<void> {
60
+ // Intentional no-op
61
+ }
62
+
63
+ /**
64
+ * Fetch product details from the Kepler IAP service.
65
+ * Prices are returned in micro-units and divided by 1,000,000.
66
+ */
67
+ export async function getProducts(skuIds: string[]): Promise<ExpoNamiIapProduct[]> {
68
+ const IAP = getKeplerIAP();
69
+ const request = new IAP.ProductDataRequest(skuIds);
70
+ const response = await IAP.PurchaseManager.getProductData(request);
71
+
72
+ return Object.values(response.productData ?? {}).map((product: any) => ({
73
+ product_ref_id: product.sku,
74
+ name: product.title,
75
+ description: product.description,
76
+ product_type:
77
+ product.productType === 'SUBSCRIPTION'
78
+ ? ('subscription' as const)
79
+ : ('one_time_purchase' as const),
80
+ offers: [
81
+ {
82
+ offer_type: 'default' as const,
83
+ price: (product.price?.priceValue ?? 0) / 1_000_000,
84
+ currency: product.price?.currencyCode ?? 'USD',
85
+ },
86
+ ],
87
+ })) as ExpoNamiIapProduct[];
88
+ }
89
+
90
+ /**
91
+ * Best-effort eligibility check.
92
+ * Returns `introEligible: true` when no active receipt exists for the SKU.
93
+ */
94
+ export async function checkEligibility(skuIds: string[]): Promise<EligibilityResult[]> {
95
+ const IAP = getKeplerIAP();
96
+ const request = new IAP.PurchaseUpdatesRequest(true);
97
+ const response = await IAP.PurchaseManager.getPurchaseUpdates(request);
98
+
99
+ const ownedSkus = new Set<string>(
100
+ (response.receipts ?? []).map((r: any) => r.sku),
101
+ );
102
+
103
+ return skuIds.map((skuId) => ({
104
+ skuId,
105
+ introEligible: !ownedSkus.has(skuId),
106
+ promoEligible: false,
107
+ }));
108
+ }
109
+
110
+ /**
111
+ * Initiate a purchase and notify fulfillment on success.
112
+ * The resulting transaction is also emitted to registered listeners.
113
+ */
114
+ export async function purchase(options: PurchaseOptions): Promise<ExpoNamiIapTransaction> {
115
+ const IAP = getKeplerIAP();
116
+ const skuId = options.skuId;
117
+
118
+ const request = new IAP.PurchaseRequest(skuId);
119
+ const response = await IAP.PurchaseManager.purchase(request);
120
+
121
+ // Notify fulfillment
122
+ if (response.receiptId) {
123
+ const fulfillment = new IAP.NotifyFulfillmentRequest(
124
+ response.receiptId,
125
+ IAP.FulfillmentResult.SUCCEEDED,
126
+ );
127
+ await IAP.PurchaseManager.notifyFulfillment(fulfillment);
128
+ }
129
+
130
+ const tx: ExpoNamiIapTransaction = {
131
+ skuId: response.sku ?? skuId,
132
+ transactionId: response.receiptId,
133
+ originalTransactionId: response.receiptId,
134
+ receipt: response.receiptId,
135
+ purchaseToken: response.receiptId,
136
+ purchaseTime: Date.now(),
137
+ };
138
+
139
+ emitTransaction(tx);
140
+
141
+ return tx;
142
+ }
143
+
144
+ /**
145
+ * Restore previous purchases from the Kepler IAP service.
146
+ */
147
+ export async function restorePurchases(): Promise<ExpoNamiIapTransaction[]> {
148
+ const IAP = getKeplerIAP();
149
+ const request = new IAP.PurchaseUpdatesRequest(true);
150
+ const response = await IAP.PurchaseManager.getPurchaseUpdates(request);
151
+
152
+ return (response.receipts ?? []).map((receipt: any) => ({
153
+ skuId: receipt.sku,
154
+ transactionId: receipt.receiptId,
155
+ originalTransactionId: receipt.receiptId,
156
+ receipt: receipt.receiptId,
157
+ purchaseToken: receipt.receiptId,
158
+ purchaseTime: receipt.purchaseDate ?? receipt.cancelDate,
159
+ }));
160
+ }
161
+
162
+ /**
163
+ * Finish (acknowledge) a transaction by notifying fulfillment.
164
+ */
165
+ export async function finishTransaction(transactionId: string): Promise<void> {
166
+ const IAP = getKeplerIAP();
167
+ const fulfillment = new IAP.NotifyFulfillmentRequest(
168
+ transactionId,
169
+ IAP.FulfillmentResult.SUCCEEDED,
170
+ );
171
+ await IAP.PurchaseManager.notifyFulfillment(fulfillment);
172
+ }
173
+
174
+ /**
175
+ * Subscribe to transaction updates. Returns a subscription with a `remove()` method.
176
+ */
177
+ export function addTransactionListener(
178
+ cb: (tx: ExpoNamiIapTransaction) => void,
179
+ ): { remove: () => void } {
180
+ transactionListeners.add(cb);
181
+ return {
182
+ remove: () => {
183
+ transactionListeners.delete(cb);
184
+ },
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Subscribe to error events. Returns a subscription with a `remove()` method.
190
+ */
191
+ export function addErrorListener(
192
+ cb: (err: ExpoNamiIapError) => void,
193
+ ): { remove: () => void } {
194
+ errorListeners.add(cb);
195
+ return {
196
+ remove: () => {
197
+ errorListeners.delete(cb);
198
+ },
199
+ };
200
+ }
@@ -0,0 +1,18 @@
1
+ import { EventEmitter, requireNativeModule } from 'expo-modules-core';
2
+
3
+ export interface NativeExpoNamiIapModule {
4
+ configure(options: unknown): void;
5
+ initConnection(): Promise<void>;
6
+ endConnection(): Promise<void>;
7
+ getProducts(skuIds: string[]): Promise<unknown[]>;
8
+ checkEligibility(skuIds: string[]): Promise<unknown[]>;
9
+ purchase(options: unknown): Promise<unknown>;
10
+ restorePurchases(): Promise<unknown[]>;
11
+ finishTransaction(transactionId: string): Promise<void>;
12
+ }
13
+
14
+ const NativeModule = requireNativeModule<NativeExpoNamiIapModule>('ExpoNamiIapModule');
15
+
16
+ export const emitter = new EventEmitter(NativeModule);
17
+
18
+ export default NativeModule;
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ import NativeModule, { emitter } from './ExpoNamiIapModule';
2
+ import type {
3
+ ConfigureOptions,
4
+ EligibilityResult,
5
+ ExpoNamiIapError,
6
+ ExpoNamiIapProduct,
7
+ ExpoNamiIapTransaction,
8
+ PurchaseOptions,
9
+ } from './types';
10
+
11
+ export * from './types';
12
+
13
+ export function configure(options: ConfigureOptions): void {
14
+ NativeModule.configure(options);
15
+ }
16
+
17
+ export function initConnection(): Promise<void> {
18
+ return NativeModule.initConnection();
19
+ }
20
+
21
+ export function endConnection(): Promise<void> {
22
+ return NativeModule.endConnection();
23
+ }
24
+
25
+ export function getProducts(skuIds: string[]): Promise<ExpoNamiIapProduct[]> {
26
+ return NativeModule.getProducts(skuIds) as Promise<ExpoNamiIapProduct[]>;
27
+ }
28
+
29
+ export function checkEligibility(skuIds: string[]): Promise<EligibilityResult[]> {
30
+ return NativeModule.checkEligibility(skuIds) as Promise<EligibilityResult[]>;
31
+ }
32
+
33
+ export function purchase(options: PurchaseOptions): Promise<ExpoNamiIapTransaction> {
34
+ return NativeModule.purchase(options) as Promise<ExpoNamiIapTransaction>;
35
+ }
36
+
37
+ export function restorePurchases(): Promise<ExpoNamiIapTransaction[]> {
38
+ return NativeModule.restorePurchases() as Promise<ExpoNamiIapTransaction[]>;
39
+ }
40
+
41
+ export function finishTransaction(transactionId: string): Promise<void> {
42
+ return NativeModule.finishTransaction(transactionId);
43
+ }
44
+
45
+ /**
46
+ * Subscribe to real-time transaction updates from the native layer.
47
+ * Returns a subscription object with a `remove()` method to unsubscribe.
48
+ */
49
+ export function addTransactionListener(
50
+ cb: (tx: ExpoNamiIapTransaction) => void,
51
+ ): { remove: () => void } {
52
+ return emitter.addListener('onTransactionUpdate', cb);
53
+ }
54
+
55
+ /**
56
+ * Subscribe to error events from the native layer.
57
+ * Returns a subscription object with a `remove()` method to unsubscribe.
58
+ */
59
+ export function addErrorListener(
60
+ cb: (err: ExpoNamiIapError) => void,
61
+ ): { remove: () => void } {
62
+ return emitter.addListener('onError', cb);
63
+ }
package/src/types.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { NamiProductDetails } from '@namiml/sdk-core';
2
+
3
+ export interface ExpoNamiIapProduct extends NamiProductDetails {
4
+ platform_raw?: unknown;
5
+ }
6
+
7
+ export interface ExpoNamiIapTransaction {
8
+ skuId: string;
9
+ transactionId: string;
10
+ originalTransactionId?: string;
11
+ receipt?: string;
12
+ purchaseToken?: string;
13
+ purchaseTime?: number;
14
+ isAcknowledged?: boolean;
15
+ }
16
+
17
+ export type ExpoNamiIapErrorCode =
18
+ | 'user_cancelled'
19
+ | 'network'
20
+ | 'item_unavailable'
21
+ | 'already_owned'
22
+ | 'not_allowed'
23
+ | 'unknown';
24
+
25
+ export interface ExpoNamiIapError {
26
+ code: ExpoNamiIapErrorCode;
27
+ message: string;
28
+ nativeCode?: string;
29
+ }
30
+
31
+ export interface EligibilityResult {
32
+ skuId: string;
33
+ introEligible: boolean;
34
+ promoEligible: boolean;
35
+ }
36
+
37
+ export interface ConfigureOptions {
38
+ amazon?: { autoRegisterListener?: boolean };
39
+ }
40
+
41
+ export interface PurchaseOptions {
42
+ skuId: string;
43
+ offerId?: string;
44
+ offerSignature?: string;
45
+ appAccountToken?: string;
46
+ }