@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/payments",
3
- "version": "1.2.110",
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 errors have a `code` property with values like
15
- * "E_USER_CANCELLED", "E_ALREADY_OWNED", etc.
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',
@@ -4,7 +4,8 @@
4
4
  //
5
5
  // v14 API changes from earlier versions:
6
6
  // - getProducts/getSubscriptions → fetchProducts({ skus, type })
7
- // - requestPurchase/requestSubscription requestPurchase({ request, type })
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 purchase = await RNIap!.requestPurchase({
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(purchase);
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 purchase = await RNIap!.requestPurchase({
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(purchase);
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(