@idealyst/payments 1.2.109 → 1.2.111
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/package.json +1 -1
- package/src/errors.ts +25 -2
- package/src/payments.native.ts +160 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/payments",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.111",
|
|
4
4
|
"description": "Cross-platform In-App Purchase wrapper for React Native (StoreKit 2, Google Play Billing)",
|
|
5
5
|
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/payments#readme",
|
|
6
6
|
"readme": "README.md",
|
package/src/errors.ts
CHANGED
|
@@ -11,56 +11,79 @@ export function createIAPError(
|
|
|
11
11
|
/**
|
|
12
12
|
* Normalize a react-native-iap error (or any thrown value) into an IAPError.
|
|
13
13
|
*
|
|
14
|
-
* react-native-iap
|
|
15
|
-
*
|
|
14
|
+
* react-native-iap v14 uses kebab-case ErrorCode enum values (e.g. 'user-cancelled').
|
|
15
|
+
* Older versions used E_ prefix strings (e.g. 'E_USER_CANCELLED').
|
|
16
|
+
* Both formats are handled here.
|
|
16
17
|
*/
|
|
17
18
|
export function normalizeError(error: unknown): IAPError {
|
|
18
19
|
if (error && typeof error === 'object' && 'code' in error) {
|
|
19
20
|
const iapError = error as { code?: string; message?: string };
|
|
20
21
|
|
|
21
22
|
switch (iapError.code) {
|
|
23
|
+
// v14: 'user-cancelled' | legacy: 'E_USER_CANCELLED'
|
|
24
|
+
case 'user-cancelled':
|
|
22
25
|
case 'E_USER_CANCELLED':
|
|
23
26
|
return createIAPError(
|
|
24
27
|
'user_cancelled',
|
|
25
28
|
'Purchase was cancelled by user',
|
|
26
29
|
error,
|
|
27
30
|
);
|
|
31
|
+
// v14: 'already-owned' | legacy: 'E_ALREADY_OWNED'
|
|
32
|
+
case 'already-owned':
|
|
28
33
|
case 'E_ALREADY_OWNED':
|
|
29
34
|
return createIAPError(
|
|
30
35
|
'already_owned',
|
|
31
36
|
'This item has already been purchased',
|
|
32
37
|
error,
|
|
33
38
|
);
|
|
39
|
+
// v14: 'not-prepared' | legacy: 'E_NOT_PREPARED'
|
|
40
|
+
case 'not-prepared':
|
|
34
41
|
case 'E_NOT_PREPARED':
|
|
35
42
|
return createIAPError(
|
|
36
43
|
'not_initialized',
|
|
37
44
|
'IAP connection not initialized. Call initializeIAP() first.',
|
|
38
45
|
error,
|
|
39
46
|
);
|
|
47
|
+
// v14: 'deferred-payment' | legacy: 'E_DEFERRED'
|
|
48
|
+
case 'deferred-payment':
|
|
49
|
+
case 'pending':
|
|
40
50
|
case 'E_DEFERRED':
|
|
41
51
|
return createIAPError(
|
|
42
52
|
'purchase_pending',
|
|
43
53
|
'Purchase is pending approval (e.g., Ask to Buy)',
|
|
44
54
|
error,
|
|
45
55
|
);
|
|
56
|
+
// v14: 'item-unavailable' | legacy: 'E_ITEM_UNAVAILABLE'
|
|
57
|
+
case 'item-unavailable':
|
|
46
58
|
case 'E_ITEM_UNAVAILABLE':
|
|
47
59
|
return createIAPError(
|
|
48
60
|
'item_unavailable',
|
|
49
61
|
'The requested product is not available in the store',
|
|
50
62
|
error,
|
|
51
63
|
);
|
|
64
|
+
// v14: 'network-error' | legacy: 'E_NETWORK_ERROR'
|
|
65
|
+
case 'network-error':
|
|
52
66
|
case 'E_NETWORK_ERROR':
|
|
53
67
|
return createIAPError(
|
|
54
68
|
'network_error',
|
|
55
69
|
iapError.message || 'A network error occurred',
|
|
56
70
|
error,
|
|
57
71
|
);
|
|
72
|
+
// v14: 'service-error' | legacy: 'E_SERVICE_ERROR'
|
|
73
|
+
case 'service-error':
|
|
58
74
|
case 'E_SERVICE_ERROR':
|
|
59
75
|
return createIAPError(
|
|
60
76
|
'store_error',
|
|
61
77
|
iapError.message || 'The store service encountered an error',
|
|
62
78
|
error,
|
|
63
79
|
);
|
|
80
|
+
// v14: 'iap-not-available'
|
|
81
|
+
case 'iap-not-available':
|
|
82
|
+
return createIAPError(
|
|
83
|
+
'not_available',
|
|
84
|
+
'In-App Purchases are not available on this device',
|
|
85
|
+
error,
|
|
86
|
+
);
|
|
64
87
|
default:
|
|
65
88
|
return createIAPError(
|
|
66
89
|
'unknown',
|
package/src/payments.native.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// Native IAP Implementation
|
|
3
|
-
// Wraps react-native-iap for StoreKit 2 (iOS) and Google Play Billing (Android)
|
|
3
|
+
// Wraps react-native-iap v14.x for StoreKit 2 (iOS) and Google Play Billing (Android)
|
|
4
|
+
//
|
|
5
|
+
// v14 API changes from earlier versions:
|
|
6
|
+
// - getProducts/getSubscriptions → fetchProducts({ skus, type })
|
|
7
|
+
// - requestPurchase returns void — results come via purchaseUpdatedListener
|
|
8
|
+
// - Errors come via purchaseErrorListener
|
|
9
|
+
// - Product fields: id (not productId), displayPrice (not localizedPrice)
|
|
10
|
+
// - Purchase fields: productId, id (transaction ID)
|
|
4
11
|
// ============================================================================
|
|
5
12
|
|
|
6
13
|
import { Platform } from 'react-native';
|
|
@@ -32,6 +39,16 @@ try {
|
|
|
32
39
|
let _status: IAPProviderStatus = { ...INITIAL_PROVIDER_STATUS };
|
|
33
40
|
let _config: IAPConfig = {};
|
|
34
41
|
|
|
42
|
+
// Listener subscriptions (EmitterSubscription-like objects with .remove())
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
let _purchaseUpdateSubscription: any = null;
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
let _purchaseErrorSubscription: any = null;
|
|
47
|
+
|
|
48
|
+
// Pending purchase promise callbacks — one purchase at a time
|
|
49
|
+
let _pendingResolve: ((raw: unknown) => void) | null = null;
|
|
50
|
+
let _pendingReject: ((error: unknown) => void) | null = null;
|
|
51
|
+
|
|
35
52
|
/**
|
|
36
53
|
* Get the current IAP provider status.
|
|
37
54
|
*/
|
|
@@ -41,6 +58,8 @@ export function getIAPStatus(): IAPProviderStatus {
|
|
|
41
58
|
|
|
42
59
|
/**
|
|
43
60
|
* Initialize the IAP connection to the native store.
|
|
61
|
+
* Sets up purchaseUpdatedListener and purchaseErrorListener to bridge
|
|
62
|
+
* v14's event-based purchase flow back to Promises.
|
|
44
63
|
*/
|
|
45
64
|
export async function initializeIAP(config?: IAPConfig): Promise<void> {
|
|
46
65
|
if (!RNIap) {
|
|
@@ -61,14 +80,28 @@ export async function initializeIAP(config?: IAPConfig): Promise<void> {
|
|
|
61
80
|
try {
|
|
62
81
|
await RNIap.initConnection();
|
|
63
82
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
// Set up purchase listeners — these fire when requestPurchase completes or fails
|
|
84
|
+
_purchaseUpdateSubscription = RNIap.purchaseUpdatedListener(
|
|
85
|
+
(purchase: unknown) => {
|
|
86
|
+
if (_pendingResolve) {
|
|
87
|
+
const resolve = _pendingResolve;
|
|
88
|
+
_pendingResolve = null;
|
|
89
|
+
_pendingReject = null;
|
|
90
|
+
resolve(purchase);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
_purchaseErrorSubscription = RNIap.purchaseErrorListener(
|
|
96
|
+
(error: unknown) => {
|
|
97
|
+
if (_pendingReject) {
|
|
98
|
+
const reject = _pendingReject;
|
|
99
|
+
_pendingResolve = null;
|
|
100
|
+
_pendingReject = null;
|
|
101
|
+
reject(error);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
72
105
|
|
|
73
106
|
_status = {
|
|
74
107
|
state: 'ready',
|
|
@@ -90,8 +123,8 @@ export async function getProducts(skus: string[]): Promise<IAPProduct[]> {
|
|
|
90
123
|
assertReady();
|
|
91
124
|
|
|
92
125
|
try {
|
|
93
|
-
const products = await RNIap!.
|
|
94
|
-
return products.map(mapProduct);
|
|
126
|
+
const products = await RNIap!.fetchProducts({ skus, type: 'in-app' });
|
|
127
|
+
return products.map((p: unknown) => mapProduct(p, 'iap'));
|
|
95
128
|
} catch (error) {
|
|
96
129
|
throw normalizeError(error);
|
|
97
130
|
}
|
|
@@ -106,8 +139,8 @@ export async function getSubscriptions(
|
|
|
106
139
|
assertReady();
|
|
107
140
|
|
|
108
141
|
try {
|
|
109
|
-
const
|
|
110
|
-
return
|
|
142
|
+
const products = await RNIap!.fetchProducts({ skus, type: 'subs' });
|
|
143
|
+
return products.map(mapSubscription);
|
|
111
144
|
} catch (error) {
|
|
112
145
|
throw normalizeError(error);
|
|
113
146
|
}
|
|
@@ -120,13 +153,21 @@ export async function purchaseProduct(sku: string): Promise<IAPPurchase> {
|
|
|
120
153
|
assertReady();
|
|
121
154
|
|
|
122
155
|
try {
|
|
123
|
-
const
|
|
156
|
+
const rawPurchase = await requestPurchaseWithEvents({
|
|
157
|
+
request: {
|
|
158
|
+
apple: { sku },
|
|
159
|
+
google: { skus: [sku] },
|
|
160
|
+
},
|
|
161
|
+
type: 'in-app',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const mapped = mapPurchase(rawPurchase);
|
|
124
165
|
|
|
125
166
|
if (_config.autoFinishTransactions) {
|
|
126
|
-
await RNIap!.finishTransaction({ purchase, isConsumable: false });
|
|
167
|
+
await RNIap!.finishTransaction({ purchase: rawPurchase, isConsumable: false });
|
|
127
168
|
}
|
|
128
169
|
|
|
129
|
-
return
|
|
170
|
+
return mapped;
|
|
130
171
|
} catch (error) {
|
|
131
172
|
throw normalizeError(error);
|
|
132
173
|
}
|
|
@@ -143,19 +184,27 @@ export async function purchaseSubscription(
|
|
|
143
184
|
assertReady();
|
|
144
185
|
|
|
145
186
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
+
const googleRequest: any = { skus: [sku] };
|
|
189
|
+
if (offerToken) {
|
|
190
|
+
googleRequest.offerToken = offerToken;
|
|
150
191
|
}
|
|
151
192
|
|
|
152
|
-
const
|
|
193
|
+
const rawPurchase = await requestPurchaseWithEvents({
|
|
194
|
+
request: {
|
|
195
|
+
apple: { sku },
|
|
196
|
+
google: googleRequest,
|
|
197
|
+
},
|
|
198
|
+
type: 'subs',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const mapped = mapPurchase(rawPurchase);
|
|
153
202
|
|
|
154
203
|
if (_config.autoFinishTransactions) {
|
|
155
|
-
await RNIap!.finishTransaction({ purchase, isConsumable: false });
|
|
204
|
+
await RNIap!.finishTransaction({ purchase: rawPurchase, isConsumable: false });
|
|
156
205
|
}
|
|
157
206
|
|
|
158
|
-
return
|
|
207
|
+
return mapped;
|
|
159
208
|
} catch (error) {
|
|
160
209
|
throw normalizeError(error);
|
|
161
210
|
}
|
|
@@ -174,8 +223,14 @@ export async function finishTransaction(
|
|
|
174
223
|
assertReady();
|
|
175
224
|
|
|
176
225
|
try {
|
|
226
|
+
// finishTransaction expects the raw purchase object; we reconstruct the
|
|
227
|
+
// minimal shape that react-native-iap needs (id + productId + purchaseToken)
|
|
177
228
|
await RNIap!.finishTransaction({
|
|
178
|
-
purchase: {
|
|
229
|
+
purchase: {
|
|
230
|
+
id: purchase.transactionId,
|
|
231
|
+
productId: purchase.sku,
|
|
232
|
+
purchaseToken: purchase.purchaseToken,
|
|
233
|
+
},
|
|
179
234
|
isConsumable: isConsumable ?? false,
|
|
180
235
|
});
|
|
181
236
|
} catch (error) {
|
|
@@ -199,22 +254,74 @@ export async function restorePurchases(): Promise<IAPPurchase[]> {
|
|
|
199
254
|
|
|
200
255
|
/**
|
|
201
256
|
* End the IAP connection. Call on cleanup (e.g., app unmount).
|
|
257
|
+
* Removes purchase listeners and rejects any pending purchase promise.
|
|
202
258
|
*/
|
|
203
259
|
export async function endConnection(): Promise<void> {
|
|
204
260
|
if (!RNIap) return;
|
|
205
261
|
|
|
262
|
+
// Reject any pending purchase
|
|
263
|
+
if (_pendingReject) {
|
|
264
|
+
const reject = _pendingReject;
|
|
265
|
+
_pendingResolve = null;
|
|
266
|
+
_pendingReject = null;
|
|
267
|
+
reject(createIAPError('not_initialized', 'IAP connection ended while purchase was pending'));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Remove listeners
|
|
271
|
+
if (_purchaseUpdateSubscription) {
|
|
272
|
+
_purchaseUpdateSubscription.remove();
|
|
273
|
+
_purchaseUpdateSubscription = null;
|
|
274
|
+
}
|
|
275
|
+
if (_purchaseErrorSubscription) {
|
|
276
|
+
_purchaseErrorSubscription.remove();
|
|
277
|
+
_purchaseErrorSubscription = null;
|
|
278
|
+
}
|
|
279
|
+
|
|
206
280
|
try {
|
|
207
281
|
await RNIap.endConnection();
|
|
208
|
-
_status = { ...INITIAL_PROVIDER_STATUS };
|
|
209
282
|
} catch {
|
|
210
283
|
// Ignore errors during cleanup
|
|
211
284
|
}
|
|
285
|
+
|
|
286
|
+
_status = { ...INITIAL_PROVIDER_STATUS };
|
|
212
287
|
}
|
|
213
288
|
|
|
214
289
|
// ============================================================================
|
|
215
290
|
// Internal Helpers
|
|
216
291
|
// ============================================================================
|
|
217
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Bridge v14's fire-and-forget requestPurchase to a Promise.
|
|
295
|
+
* Stores resolve/reject callbacks that are settled by the purchase listeners
|
|
296
|
+
* set up in initializeIAP.
|
|
297
|
+
*/
|
|
298
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
299
|
+
function requestPurchaseWithEvents(params: any): Promise<any> {
|
|
300
|
+
if (_pendingResolve || _pendingReject) {
|
|
301
|
+
return Promise.reject(
|
|
302
|
+
createIAPError('unknown', 'A purchase is already in progress'),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
307
|
+
return new Promise<any>((resolve, reject) => {
|
|
308
|
+
_pendingResolve = resolve;
|
|
309
|
+
_pendingReject = reject;
|
|
310
|
+
|
|
311
|
+
// requestPurchase returns void in v14 — result comes through listeners
|
|
312
|
+
RNIap!.requestPurchase(params).catch((error: unknown) => {
|
|
313
|
+
// If requestPurchase itself throws (e.g., invalid params),
|
|
314
|
+
// settle the promise immediately
|
|
315
|
+
if (_pendingReject) {
|
|
316
|
+
const pendingReject = _pendingReject;
|
|
317
|
+
_pendingResolve = null;
|
|
318
|
+
_pendingReject = null;
|
|
319
|
+
pendingReject(error);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
218
325
|
function assertReady(): void {
|
|
219
326
|
if (!RNIap) {
|
|
220
327
|
throw createIAPError(
|
|
@@ -235,26 +342,25 @@ function getPlatform(): ProductPlatform {
|
|
|
235
342
|
}
|
|
236
343
|
|
|
237
344
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
238
|
-
function mapProduct(raw: any): IAPProduct {
|
|
345
|
+
function mapProduct(raw: any, type: 'iap' | 'sub' = 'iap'): IAPProduct {
|
|
239
346
|
return {
|
|
240
|
-
sku: raw.
|
|
241
|
-
title: raw.title ?? '',
|
|
347
|
+
sku: raw.id ?? raw.productId ?? '',
|
|
348
|
+
title: raw.title ?? raw.displayName ?? '',
|
|
242
349
|
description: raw.description ?? '',
|
|
243
|
-
price: parseFloat(raw.price ?? '0'),
|
|
244
|
-
priceFormatted: raw.localizedPrice ?? raw.price ?? '',
|
|
350
|
+
price: typeof raw.price === 'number' ? raw.price : parseFloat(raw.price ?? '0'),
|
|
351
|
+
priceFormatted: raw.displayPrice ?? raw.localizedPrice ?? String(raw.price ?? ''),
|
|
245
352
|
currency: raw.currency ?? '',
|
|
246
|
-
type
|
|
353
|
+
type,
|
|
247
354
|
platform: getPlatform(),
|
|
248
355
|
};
|
|
249
356
|
}
|
|
250
357
|
|
|
251
358
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
252
359
|
function mapSubscription(raw: any): IAPSubscription {
|
|
253
|
-
const base = mapProduct(raw);
|
|
360
|
+
const base = mapProduct(raw, 'sub');
|
|
254
361
|
|
|
255
362
|
return {
|
|
256
363
|
...base,
|
|
257
|
-
type: 'sub',
|
|
258
364
|
subscriptionPeriod: parseSubscriptionPeriod(raw),
|
|
259
365
|
introductoryPrice: raw.introductoryPrice
|
|
260
366
|
? mapDiscount(raw.introductoryPrice, 'introductory')
|
|
@@ -275,7 +381,19 @@ function parseSubscriptionPeriod(raw: any): SubscriptionPeriod {
|
|
|
275
381
|
};
|
|
276
382
|
}
|
|
277
383
|
|
|
278
|
-
// Android:
|
|
384
|
+
// Android: subscriptionOfferDetailsAndroid may contain period info
|
|
385
|
+
if (raw.subscriptionOfferDetailsAndroid?.length) {
|
|
386
|
+
const offer = raw.subscriptionOfferDetailsAndroid[0];
|
|
387
|
+
const pricingPhases = offer?.pricingPhases?.pricingPhaseList;
|
|
388
|
+
if (pricingPhases?.length) {
|
|
389
|
+
const billingPeriod = pricingPhases[0]?.billingPeriod;
|
|
390
|
+
if (billingPeriod) {
|
|
391
|
+
return parseISO8601Period(billingPeriod);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Fallback: subscriptionPeriodAndroid (older format)
|
|
279
397
|
if (raw.subscriptionPeriodAndroid) {
|
|
280
398
|
return parseISO8601Period(raw.subscriptionPeriodAndroid);
|
|
281
399
|
}
|
|
@@ -332,7 +450,7 @@ function mapDiscount(raw: any, type: 'introductory' | 'promotional'): Subscripti
|
|
|
332
450
|
return {
|
|
333
451
|
identifier: raw.identifier,
|
|
334
452
|
price: parseFloat(raw.price ?? '0'),
|
|
335
|
-
priceFormatted: raw.localizedPrice ?? raw.price ?? '',
|
|
453
|
+
priceFormatted: raw.displayPrice ?? raw.localizedPrice ?? raw.price ?? '',
|
|
336
454
|
period: raw.subscriptionPeriod
|
|
337
455
|
? parseISO8601Period(raw.subscriptionPeriod)
|
|
338
456
|
: { unit: 'month', numberOfUnits: 1 },
|
|
@@ -344,13 +462,16 @@ function mapDiscount(raw: any, type: 'introductory' | 'promotional'): Subscripti
|
|
|
344
462
|
|
|
345
463
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
346
464
|
function mapPurchase(raw: any, stateOverride?: IAPPurchase['purchaseState']): IAPPurchase {
|
|
465
|
+
// v14: transaction ID is in `id`, product is in `productId`
|
|
347
466
|
return {
|
|
348
|
-
sku: raw.productId ?? raw.
|
|
349
|
-
transactionId: raw.transactionId ?? '',
|
|
467
|
+
sku: raw.productId ?? raw.id ?? '',
|
|
468
|
+
transactionId: raw.id ?? raw.transactionId ?? '',
|
|
350
469
|
transactionDate: raw.transactionDate
|
|
351
|
-
?
|
|
470
|
+
? (typeof raw.transactionDate === 'number'
|
|
471
|
+
? raw.transactionDate
|
|
472
|
+
: parseInt(raw.transactionDate, 10))
|
|
352
473
|
: Date.now(),
|
|
353
|
-
transactionReceipt: raw.transactionReceipt ?? '',
|
|
474
|
+
transactionReceipt: raw.transactionReceipt ?? raw.dataAndroid ?? '',
|
|
354
475
|
purchaseToken: raw.purchaseToken,
|
|
355
476
|
isAcknowledged: raw.isAcknowledgedAndroid,
|
|
356
477
|
purchaseState: stateOverride ?? 'purchased',
|