@idealyst/payments 1.2.110 → 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 +96 -8
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
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// v14 API changes from earlier versions:
|
|
6
6
|
// - getProducts/getSubscriptions → fetchProducts({ skus, type })
|
|
7
|
-
// - requestPurchase
|
|
7
|
+
// - requestPurchase returns void — results come via purchaseUpdatedListener
|
|
8
|
+
// - Errors come via purchaseErrorListener
|
|
8
9
|
// - Product fields: id (not productId), displayPrice (not localizedPrice)
|
|
9
10
|
// - Purchase fields: productId, id (transaction ID)
|
|
10
11
|
// ============================================================================
|
|
@@ -38,6 +39,16 @@ try {
|
|
|
38
39
|
let _status: IAPProviderStatus = { ...INITIAL_PROVIDER_STATUS };
|
|
39
40
|
let _config: IAPConfig = {};
|
|
40
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
|
+
|
|
41
52
|
/**
|
|
42
53
|
* Get the current IAP provider status.
|
|
43
54
|
*/
|
|
@@ -47,6 +58,8 @@ export function getIAPStatus(): IAPProviderStatus {
|
|
|
47
58
|
|
|
48
59
|
/**
|
|
49
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.
|
|
50
63
|
*/
|
|
51
64
|
export async function initializeIAP(config?: IAPConfig): Promise<void> {
|
|
52
65
|
if (!RNIap) {
|
|
@@ -67,6 +80,29 @@ export async function initializeIAP(config?: IAPConfig): Promise<void> {
|
|
|
67
80
|
try {
|
|
68
81
|
await RNIap.initConnection();
|
|
69
82
|
|
|
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
|
+
);
|
|
105
|
+
|
|
70
106
|
_status = {
|
|
71
107
|
state: 'ready',
|
|
72
108
|
isStoreAvailable: true,
|
|
@@ -117,7 +153,7 @@ export async function purchaseProduct(sku: string): Promise<IAPPurchase> {
|
|
|
117
153
|
assertReady();
|
|
118
154
|
|
|
119
155
|
try {
|
|
120
|
-
const
|
|
156
|
+
const rawPurchase = await requestPurchaseWithEvents({
|
|
121
157
|
request: {
|
|
122
158
|
apple: { sku },
|
|
123
159
|
google: { skus: [sku] },
|
|
@@ -125,10 +161,10 @@ export async function purchaseProduct(sku: string): Promise<IAPPurchase> {
|
|
|
125
161
|
type: 'in-app',
|
|
126
162
|
});
|
|
127
163
|
|
|
128
|
-
const mapped = mapPurchase(
|
|
164
|
+
const mapped = mapPurchase(rawPurchase);
|
|
129
165
|
|
|
130
166
|
if (_config.autoFinishTransactions) {
|
|
131
|
-
await RNIap!.finishTransaction({ purchase, isConsumable: false });
|
|
167
|
+
await RNIap!.finishTransaction({ purchase: rawPurchase, isConsumable: false });
|
|
132
168
|
}
|
|
133
169
|
|
|
134
170
|
return mapped;
|
|
@@ -154,7 +190,7 @@ export async function purchaseSubscription(
|
|
|
154
190
|
googleRequest.offerToken = offerToken;
|
|
155
191
|
}
|
|
156
192
|
|
|
157
|
-
const
|
|
193
|
+
const rawPurchase = await requestPurchaseWithEvents({
|
|
158
194
|
request: {
|
|
159
195
|
apple: { sku },
|
|
160
196
|
google: googleRequest,
|
|
@@ -162,10 +198,10 @@ export async function purchaseSubscription(
|
|
|
162
198
|
type: 'subs',
|
|
163
199
|
});
|
|
164
200
|
|
|
165
|
-
const mapped = mapPurchase(
|
|
201
|
+
const mapped = mapPurchase(rawPurchase);
|
|
166
202
|
|
|
167
203
|
if (_config.autoFinishTransactions) {
|
|
168
|
-
await RNIap!.finishTransaction({ purchase, isConsumable: false });
|
|
204
|
+
await RNIap!.finishTransaction({ purchase: rawPurchase, isConsumable: false });
|
|
169
205
|
}
|
|
170
206
|
|
|
171
207
|
return mapped;
|
|
@@ -218,22 +254,74 @@ export async function restorePurchases(): Promise<IAPPurchase[]> {
|
|
|
218
254
|
|
|
219
255
|
/**
|
|
220
256
|
* End the IAP connection. Call on cleanup (e.g., app unmount).
|
|
257
|
+
* Removes purchase listeners and rejects any pending purchase promise.
|
|
221
258
|
*/
|
|
222
259
|
export async function endConnection(): Promise<void> {
|
|
223
260
|
if (!RNIap) return;
|
|
224
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
|
+
|
|
225
280
|
try {
|
|
226
281
|
await RNIap.endConnection();
|
|
227
|
-
_status = { ...INITIAL_PROVIDER_STATUS };
|
|
228
282
|
} catch {
|
|
229
283
|
// Ignore errors during cleanup
|
|
230
284
|
}
|
|
285
|
+
|
|
286
|
+
_status = { ...INITIAL_PROVIDER_STATUS };
|
|
231
287
|
}
|
|
232
288
|
|
|
233
289
|
// ============================================================================
|
|
234
290
|
// Internal Helpers
|
|
235
291
|
// ============================================================================
|
|
236
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
|
+
|
|
237
325
|
function assertReady(): void {
|
|
238
326
|
if (!RNIap) {
|
|
239
327
|
throw createIAPError(
|