expo-iap 3.0.7 → 3.0.8

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/src/types.ts CHANGED
@@ -126,10 +126,7 @@ export enum ErrorCode {
126
126
  UserError = 'USER_ERROR'
127
127
  }
128
128
 
129
- export interface FetchProductsResult {
130
- products?: (Product[] | null);
131
- subscriptions?: (ProductSubscription[] | null);
132
- }
129
+ export type FetchProductsResult = Product[] | ProductSubscription[] | null;
133
130
 
134
131
  export type IapEvent = 'promoted-product-ios' | 'purchase-error' | 'purchase-updated';
135
132
 
@@ -137,57 +134,46 @@ export type IapPlatform = 'android' | 'ios';
137
134
 
138
135
  export interface Mutation {
139
136
  /** Acknowledge a non-consumable purchase or subscription */
140
- acknowledgePurchaseAndroid: Promise<VoidResult>;
137
+ acknowledgePurchaseAndroid: Promise<boolean>;
141
138
  /** Initiate a refund request for a product (iOS 15+) */
142
- beginRefundRequestIOS: Promise<RefundResultIOS>;
139
+ beginRefundRequestIOS?: Promise<(string | null)>;
143
140
  /** Clear pending transactions from the StoreKit payment queue */
144
- clearTransactionIOS: Promise<VoidResult>;
141
+ clearTransactionIOS: Promise<boolean>;
145
142
  /** Consume a purchase token so it can be repurchased */
146
- consumePurchaseAndroid: Promise<VoidResult>;
143
+ consumePurchaseAndroid: Promise<boolean>;
147
144
  /** Open the native subscription management surface */
148
- deepLinkToSubscriptions: Promise<VoidResult>;
145
+ deepLinkToSubscriptions: Promise<void>;
149
146
  /** Close the platform billing connection */
150
147
  endConnection: Promise<boolean>;
151
148
  /** Finish a transaction after validating receipts */
152
- finishTransaction: Promise<VoidResult>;
149
+ finishTransaction: Promise<void>;
153
150
  /** Establish the platform billing connection */
154
151
  initConnection: Promise<boolean>;
155
152
  /** Present the App Store code redemption sheet */
156
- presentCodeRedemptionSheetIOS: Promise<VoidResult>;
153
+ presentCodeRedemptionSheetIOS: Promise<boolean>;
157
154
  /** Initiate a purchase flow; rely on events for final state */
158
- requestPurchase?: Promise<(RequestPurchaseResult | null)>;
155
+ requestPurchase?: Promise<(Purchase | Purchase[] | null)>;
159
156
  /** Purchase the promoted product surfaced by the App Store */
160
- requestPurchaseOnPromotedProductIOS: Promise<PurchaseIOS>;
157
+ requestPurchaseOnPromotedProductIOS: Promise<boolean>;
161
158
  /** Restore completed purchases across platforms */
162
- restorePurchases: Promise<VoidResult>;
159
+ restorePurchases: Promise<void>;
163
160
  /** Open subscription management UI and return changed purchases (iOS 15+) */
164
161
  showManageSubscriptionsIOS: Promise<PurchaseIOS[]>;
165
162
  /** Force a StoreKit sync for transactions (iOS 15+) */
166
- syncIOS: Promise<VoidResult>;
163
+ syncIOS: Promise<boolean>;
167
164
  /** Validate purchase receipts with the configured providers */
168
165
  validateReceipt: Promise<ReceiptValidationResult>;
169
166
  }
170
167
 
171
168
 
172
- export interface MutationAcknowledgePurchaseAndroidArgs {
173
- purchaseToken: string;
174
- }
175
-
176
169
 
177
- export interface MutationBeginRefundRequestIosArgs {
178
- sku: string;
179
- }
170
+ export type MutationAcknowledgePurchaseAndroidArgs = string;
180
171
 
172
+ export type MutationBeginRefundRequestIosArgs = string;
181
173
 
182
- export interface MutationConsumePurchaseAndroidArgs {
183
- purchaseToken: string;
184
- }
185
-
186
-
187
- export interface MutationDeepLinkToSubscriptionsArgs {
188
- options?: (DeepLinkOptions | null);
189
- }
174
+ export type MutationConsumePurchaseAndroidArgs = string;
190
175
 
176
+ export type MutationDeepLinkToSubscriptionsArgs = (DeepLinkOptions | null) | undefined;
191
177
 
192
178
  export interface MutationFinishTransactionArgs {
193
179
  isConsumable?: (boolean | null);
@@ -195,14 +181,20 @@ export interface MutationFinishTransactionArgs {
195
181
  }
196
182
 
197
183
 
198
- export interface MutationRequestPurchaseArgs {
199
- params: RequestPurchaseProps;
200
- }
184
+ export type MutationRequestPurchaseArgs =
185
+ | {
186
+ /** Per-platform purchase request props */
187
+ request: RequestPurchasePropsByPlatforms;
188
+ type: 'in-app';
189
+ }
190
+ | {
191
+ /** Per-platform subscription request props */
192
+ request: RequestSubscriptionPropsByPlatforms;
193
+ type: 'subs';
194
+ };
201
195
 
202
196
 
203
- export interface MutationValidateReceiptArgs {
204
- options: ReceiptValidationProps;
205
- }
197
+ export type MutationValidateReceiptArgs = ReceiptValidationProps;
206
198
 
207
199
  export type PaymentModeIOS = 'empty' | 'free-trial' | 'pay-as-you-go' | 'pay-up-front';
208
200
 
@@ -440,9 +432,9 @@ export type PurchaseState = 'deferred' | 'failed' | 'pending' | 'purchased' | 'r
440
432
 
441
433
  export interface Query {
442
434
  /** Get current StoreKit 2 entitlements (iOS 15+) */
443
- currentEntitlementIOS: Promise<EntitlementIOS[]>;
435
+ currentEntitlementIOS?: Promise<(PurchaseIOS | null)>;
444
436
  /** Retrieve products or subscriptions from the store */
445
- fetchProducts: Promise<FetchProductsResult>;
437
+ fetchProducts: Promise<(Product[] | ProductSubscription[] | null)>;
446
438
  /** Get active subscriptions (filters by subscriptionIds when provided) */
447
439
  getActiveSubscriptions: Promise<ActiveSubscription[]>;
448
440
  /** Fetch the current app transaction (iOS 16+) */
@@ -454,14 +446,14 @@ export interface Query {
454
446
  /** Get the currently promoted product (iOS 11+) */
455
447
  getPromotedProductIOS?: Promise<(ProductIOS | null)>;
456
448
  /** Get base64-encoded receipt data for validation */
457
- getReceiptDataIOS: Promise<string>;
449
+ getReceiptDataIOS?: Promise<(string | null)>;
458
450
  /** Get the current App Store storefront country code */
459
451
  getStorefrontIOS: Promise<string>;
460
452
  /** Get the transaction JWS (StoreKit 2) */
461
- getTransactionJwsIOS: Promise<string>;
453
+ getTransactionJwsIOS?: Promise<(string | null)>;
462
454
  /** Check whether the user has active subscriptions */
463
455
  hasActiveSubscriptions: Promise<boolean>;
464
- /** Check introductory offer eligibility for specific products */
456
+ /** Check introductory offer eligibility for a subscription group */
465
457
  isEligibleForIntroOfferIOS: Promise<boolean>;
466
458
  /** Verify a StoreKit 2 transaction signature */
467
459
  isTransactionVerifiedIOS: Promise<boolean>;
@@ -469,57 +461,33 @@ export interface Query {
469
461
  latestTransactionIOS?: Promise<(PurchaseIOS | null)>;
470
462
  /** Get StoreKit 2 subscription status details (iOS 15+) */
471
463
  subscriptionStatusIOS: Promise<SubscriptionStatusIOS[]>;
464
+ /** Validate a receipt for a specific product */
465
+ validateReceiptIOS: Promise<ReceiptValidationResultIOS>;
472
466
  }
473
467
 
474
468
 
475
- export interface QueryCurrentEntitlementIosArgs {
476
- skus?: (string[] | null);
477
- }
478
-
479
-
480
- export interface QueryFetchProductsArgs {
481
- params: ProductRequest;
482
- }
483
-
484
-
485
- export interface QueryGetActiveSubscriptionsArgs {
486
- subscriptionIds?: (string[] | null);
487
- }
488
469
 
470
+ export type QueryCurrentEntitlementIosArgs = string;
489
471
 
490
- export interface QueryGetAvailablePurchasesArgs {
491
- options?: (PurchaseOptions | null);
492
- }
493
-
494
-
495
- export interface QueryGetTransactionJwsIosArgs {
496
- transactionId: string;
497
- }
498
-
472
+ export type QueryFetchProductsArgs = ProductRequest;
499
473
 
500
- export interface QueryHasActiveSubscriptionsArgs {
501
- subscriptionIds?: (string[] | null);
502
- }
474
+ export type QueryGetActiveSubscriptionsArgs = (string[] | null) | undefined;
503
475
 
476
+ export type QueryGetAvailablePurchasesArgs = (PurchaseOptions | null) | undefined;
504
477
 
505
- export interface QueryIsEligibleForIntroOfferIosArgs {
506
- productIds: string[];
507
- }
478
+ export type QueryGetTransactionJwsIosArgs = string;
508
479
 
480
+ export type QueryHasActiveSubscriptionsArgs = (string[] | null) | undefined;
509
481
 
510
- export interface QueryIsTransactionVerifiedIosArgs {
511
- transactionId: string;
512
- }
482
+ export type QueryIsEligibleForIntroOfferIosArgs = string;
513
483
 
484
+ export type QueryIsTransactionVerifiedIosArgs = string;
514
485
 
515
- export interface QueryLatestTransactionIosArgs {
516
- sku: string;
517
- }
486
+ export type QueryLatestTransactionIosArgs = string;
518
487
 
488
+ export type QuerySubscriptionStatusIosArgs = string;
519
489
 
520
- export interface QuerySubscriptionStatusIosArgs {
521
- skus?: (string[] | null);
522
- }
490
+ export type QueryValidateReceiptIosArgs = ReceiptValidationProps;
523
491
 
524
492
  export interface ReceiptValidationAndroidOptions {
525
493
  accessToken: string;
@@ -623,10 +591,7 @@ export interface RequestPurchasePropsByPlatforms {
623
591
  ios?: (RequestPurchaseIosProps | null);
624
592
  }
625
593
 
626
- export interface RequestPurchaseResult {
627
- purchase?: (Purchase | null);
628
- purchases?: (Purchase[] | null);
629
- }
594
+ export type RequestPurchaseResult = Purchase | Purchase[] | null;
630
595
 
631
596
  export interface RequestSubscriptionAndroidProps {
632
597
  /** Personalized offer flag */
@@ -669,6 +634,7 @@ export interface Subscription {
669
634
  purchaseUpdated: Purchase;
670
635
  }
671
636
 
637
+
672
638
  export interface SubscriptionInfoIOS {
673
639
  introductoryOffer?: (SubscriptionOfferIOS | null);
674
640
  promotionalOffers?: (SubscriptionOfferIOS[] | null);
@@ -700,6 +666,86 @@ export interface SubscriptionStatusIOS {
700
666
  state: string;
701
667
  }
702
668
 
703
- export interface VoidResult {
704
- success: boolean;
705
- }
669
+ export type VoidResult = void;
670
+
671
+ // -- Query helper types (auto-generated)
672
+ export type QueryArgsMap = {
673
+ currentEntitlementIOS: QueryCurrentEntitlementIosArgs;
674
+ fetchProducts: QueryFetchProductsArgs;
675
+ getActiveSubscriptions: QueryGetActiveSubscriptionsArgs;
676
+ getAppTransactionIOS: never;
677
+ getAvailablePurchases: QueryGetAvailablePurchasesArgs;
678
+ getPendingTransactionsIOS: never;
679
+ getPromotedProductIOS: never;
680
+ getReceiptDataIOS: never;
681
+ getStorefrontIOS: never;
682
+ getTransactionJwsIOS: QueryGetTransactionJwsIosArgs;
683
+ hasActiveSubscriptions: QueryHasActiveSubscriptionsArgs;
684
+ isEligibleForIntroOfferIOS: QueryIsEligibleForIntroOfferIosArgs;
685
+ isTransactionVerifiedIOS: QueryIsTransactionVerifiedIosArgs;
686
+ latestTransactionIOS: QueryLatestTransactionIosArgs;
687
+ subscriptionStatusIOS: QuerySubscriptionStatusIosArgs;
688
+ validateReceiptIOS: QueryValidateReceiptIosArgs;
689
+ };
690
+
691
+ export type QueryField<K extends keyof Query> =
692
+ QueryArgsMap[K] extends never
693
+ ? () => NonNullable<Query[K]>
694
+ : undefined extends QueryArgsMap[K]
695
+ ? (args?: QueryArgsMap[K]) => NonNullable<Query[K]>
696
+ : (args: QueryArgsMap[K]) => NonNullable<Query[K]>;
697
+
698
+ export type QueryFieldMap = {
699
+ [K in keyof Query]?: QueryField<K>;
700
+ };
701
+ // -- End query helper types
702
+
703
+ // -- Mutation helper types (auto-generated)
704
+ export type MutationArgsMap = {
705
+ acknowledgePurchaseAndroid: MutationAcknowledgePurchaseAndroidArgs;
706
+ beginRefundRequestIOS: MutationBeginRefundRequestIosArgs;
707
+ clearTransactionIOS: never;
708
+ consumePurchaseAndroid: MutationConsumePurchaseAndroidArgs;
709
+ deepLinkToSubscriptions: MutationDeepLinkToSubscriptionsArgs;
710
+ endConnection: never;
711
+ finishTransaction: MutationFinishTransactionArgs;
712
+ initConnection: never;
713
+ presentCodeRedemptionSheetIOS: never;
714
+ requestPurchase: MutationRequestPurchaseArgs;
715
+ requestPurchaseOnPromotedProductIOS: never;
716
+ restorePurchases: never;
717
+ showManageSubscriptionsIOS: never;
718
+ syncIOS: never;
719
+ validateReceipt: MutationValidateReceiptArgs;
720
+ };
721
+
722
+ export type MutationField<K extends keyof Mutation> =
723
+ MutationArgsMap[K] extends never
724
+ ? () => NonNullable<Mutation[K]>
725
+ : undefined extends MutationArgsMap[K]
726
+ ? (args?: MutationArgsMap[K]) => NonNullable<Mutation[K]>
727
+ : (args: MutationArgsMap[K]) => NonNullable<Mutation[K]>;
728
+
729
+ export type MutationFieldMap = {
730
+ [K in keyof Mutation]?: MutationField<K>;
731
+ };
732
+ // -- End mutation helper types
733
+
734
+ // -- Subscription helper types (auto-generated)
735
+ export type SubscriptionArgsMap = {
736
+ promotedProductIOS: never;
737
+ purchaseError: never;
738
+ purchaseUpdated: never;
739
+ };
740
+
741
+ export type SubscriptionField<K extends keyof Subscription> =
742
+ SubscriptionArgsMap[K] extends never
743
+ ? () => NonNullable<Subscription[K]>
744
+ : undefined extends SubscriptionArgsMap[K]
745
+ ? (args?: SubscriptionArgsMap[K]) => NonNullable<Subscription[K]>
746
+ : (args: SubscriptionArgsMap[K]) => NonNullable<Subscription[K]>;
747
+
748
+ export type SubscriptionFieldMap = {
749
+ [K in keyof Subscription]?: SubscriptionField<K>;
750
+ };
751
+ // -- End subscription helper types
package/src/useIAP.ts CHANGED
@@ -19,7 +19,6 @@ import {
19
19
  hasActiveSubscriptions,
20
20
  type ActiveSubscription,
21
21
  type ProductTypeInput,
22
- type PurchaseRequestInput,
23
22
  restorePurchases,
24
23
  } from './index';
25
24
  import {
@@ -28,14 +27,18 @@ import {
28
27
  } from './modules/ios';
29
28
 
30
29
  // Types
31
- import {
30
+ import type {
32
31
  Product,
33
- Purchase,
34
32
  ProductSubscription,
35
- ErrorCode,
36
- VoidResult,
33
+ ProductQueryType,
34
+ ProductRequest,
35
+ Purchase,
36
+ MutationRequestPurchaseArgs,
37
+ PurchaseInput,
38
+ ReceiptValidationProps,
37
39
  ReceiptValidationResult,
38
40
  } from './types';
41
+ import {ErrorCode} from './types';
39
42
  import {PurchaseError} from './purchase-error';
40
43
  import {
41
44
  getUserFriendlyErrorMessage,
@@ -58,7 +61,7 @@ type UseIap = {
58
61
  }: {
59
62
  purchase: Purchase;
60
63
  isConsumable?: boolean;
61
- }) => Promise<VoidResult | boolean>;
64
+ }) => Promise<void>;
62
65
  getAvailablePurchases: () => Promise<void>;
63
66
  fetchProducts: (params: {
64
67
  skus: string[];
@@ -66,20 +69,14 @@ type UseIap = {
66
69
  }) => Promise<void>;
67
70
 
68
71
  requestPurchase: (
69
- params: PurchaseRequestInput,
72
+ params: MutationRequestPurchaseArgs,
70
73
  ) => ReturnType<typeof requestPurchaseInternal>;
71
74
  validateReceipt: (
72
- sku: string,
73
- androidOptions?: {
74
- packageName: string;
75
- productToken: string;
76
- accessToken: string;
77
- isSub?: boolean;
78
- },
75
+ props: ReceiptValidationProps,
79
76
  ) => Promise<ReceiptValidationResult>;
80
77
  restorePurchases: () => Promise<void>;
81
78
  getPromotedProductIOS: () => Promise<Product | null>;
82
- requestPurchaseOnPromotedProductIOS: () => Promise<void>;
79
+ requestPurchaseOnPromotedProductIOS: () => Promise<boolean>;
83
80
  getActiveSubscriptions: (subscriptionIds?: string[]) => Promise<void>;
84
81
  hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
85
82
  };
@@ -154,14 +151,40 @@ export function useIAP(options?: UseIAPOptions): UseIap {
154
151
  subscriptionsRefState.current = subscriptions;
155
152
  }, [subscriptions]);
156
153
 
154
+ const normalizeProductQueryType = useCallback(
155
+ (type?: ProductTypeInput): ProductQueryType => {
156
+ if (!type || type === 'inapp' || type === 'in-app') {
157
+ return 'in-app';
158
+ }
159
+ return type;
160
+ },
161
+ [],
162
+ );
163
+
164
+ const toPurchaseInput = useCallback(
165
+ (purchase: Purchase): PurchaseInput => ({
166
+ id: purchase.id,
167
+ ids: purchase.ids ?? undefined,
168
+ isAutoRenewing: purchase.isAutoRenewing,
169
+ platform: purchase.platform,
170
+ productId: purchase.productId,
171
+ purchaseState: purchase.purchaseState,
172
+ purchaseToken: purchase.purchaseToken ?? null,
173
+ quantity: purchase.quantity,
174
+ transactionDate: purchase.transactionDate,
175
+ }),
176
+ [],
177
+ );
178
+
157
179
  const getSubscriptionsInternal = useCallback(
158
180
  async (skus: string[]): Promise<void> => {
159
181
  try {
160
182
  const result = await fetchProducts({skus, type: 'subs'});
183
+ const subscriptionsResult = (result ?? []) as ProductSubscription[];
161
184
  setSubscriptions((prevSubscriptions) =>
162
185
  mergeWithDuplicateCheck(
163
186
  prevSubscriptions,
164
- result as ProductSubscription[],
187
+ subscriptionsResult,
165
188
  (subscription) => subscription.id,
166
189
  ),
167
190
  );
@@ -178,30 +201,58 @@ export function useIAP(options?: UseIAPOptions): UseIap {
178
201
  type?: ProductTypeInput;
179
202
  }): Promise<void> => {
180
203
  try {
181
- const result = await fetchProducts(params);
204
+ const queryType = normalizeProductQueryType(params.type);
205
+ const request: ProductRequest = {skus: params.skus, type: queryType};
206
+ const result = await fetchProducts(request);
207
+ const items = (result ?? []) as (Product | ProductSubscription)[];
182
208
 
183
- if (params.type === 'subs') {
209
+ if (queryType === 'subs') {
210
+ const subscriptionsResult = items as ProductSubscription[];
184
211
  setSubscriptions((prevSubscriptions) =>
185
212
  mergeWithDuplicateCheck(
186
213
  prevSubscriptions,
187
- result as ProductSubscription[],
214
+ subscriptionsResult,
188
215
  (subscription) => subscription.id,
189
216
  ),
190
217
  );
218
+ } else if (queryType === 'in-app') {
219
+ const productsResult = items as Product[];
220
+ setProducts((prevProducts) =>
221
+ mergeWithDuplicateCheck(
222
+ prevProducts,
223
+ productsResult,
224
+ (product) => product.id,
225
+ ),
226
+ );
191
227
  } else {
228
+ const productItems = items.filter(
229
+ (item) => item.type === 'in-app',
230
+ ) as Product[];
231
+ const subscriptionItems = items.filter(
232
+ (item) => item.type === 'subs',
233
+ ) as ProductSubscription[];
234
+
192
235
  setProducts((prevProducts) =>
193
236
  mergeWithDuplicateCheck(
194
237
  prevProducts,
195
- result as Product[],
238
+ productItems,
196
239
  (product) => product.id,
197
240
  ),
198
241
  );
242
+
243
+ setSubscriptions((prevSubscriptions) =>
244
+ mergeWithDuplicateCheck(
245
+ prevSubscriptions,
246
+ subscriptionItems,
247
+ (subscription) => subscription.id,
248
+ ),
249
+ );
199
250
  }
200
251
  } catch (error) {
201
252
  console.error('Error fetching products:', error);
202
253
  }
203
254
  },
204
- [mergeWithDuplicateCheck],
255
+ [mergeWithDuplicateCheck, normalizeProductQueryType],
205
256
  );
206
257
 
207
258
  const getAvailablePurchasesInternal = useCallback(async (): Promise<void> => {
@@ -242,23 +293,23 @@ export function useIAP(options?: UseIAPOptions): UseIap {
242
293
  );
243
294
 
244
295
  const finishTransaction = useCallback(
245
- ({
296
+ async ({
246
297
  purchase,
247
298
  isConsumable,
248
299
  }: {
249
300
  purchase: Purchase;
250
301
  isConsumable?: boolean;
251
- }): Promise<VoidResult | boolean> => {
252
- return finishTransactionInternal({
253
- purchase,
302
+ }): Promise<void> => {
303
+ await finishTransactionInternal({
304
+ purchase: toPurchaseInput(purchase),
254
305
  isConsumable,
255
306
  });
256
307
  },
257
- [],
308
+ [toPurchaseInput],
258
309
  );
259
310
 
260
311
  const requestPurchaseWithReset = useCallback(
261
- (requestObj: PurchaseRequestInput) => {
312
+ (requestObj: MutationRequestPurchaseArgs) => {
262
313
  return requestPurchaseInternal(requestObj);
263
314
  },
264
315
  [],
@@ -283,7 +334,8 @@ export function useIAP(options?: UseIAPOptions): UseIap {
283
334
  // Android: fetch available purchases directly.
284
335
  const restorePurchasesInternal = useCallback(async (): Promise<void> => {
285
336
  try {
286
- const purchases = await restorePurchases({
337
+ await restorePurchases();
338
+ const purchases = await getAvailablePurchases({
287
339
  alsoPublishToEventListenerIOS: false,
288
340
  onlyIncludeActiveItemsIOS: true,
289
341
  });
@@ -293,20 +345,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
293
345
  }
294
346
  }, []);
295
347
 
296
- const validateReceipt = useCallback(
297
- async (
298
- sku: string,
299
- androidOptions?: {
300
- packageName: string;
301
- productToken: string;
302
- accessToken: string;
303
- isSub?: boolean;
304
- },
305
- ) => {
306
- return validateReceiptInternal(sku, androidOptions);
307
- },
308
- [],
309
- );
348
+ const validateReceipt = useCallback(async (props: ReceiptValidationProps) => {
349
+ return validateReceiptInternal(props);
350
+ }, []);
310
351
 
311
352
  const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
312
353
  // CRITICAL: Register listeners BEFORE initConnection to avoid race condition
@@ -0,0 +1,52 @@
1
+ import type {Purchase} from '../types';
2
+
3
+ const isPurchaseObject = (value: unknown): value is Record<string, unknown> =>
4
+ Boolean(value) && typeof value === 'object';
5
+
6
+ /**
7
+ * Normalizes purchase identifiers so `id` reflects the transaction identifier (if present).
8
+ * Falls back to the existing `id` when no transaction identifier is available.
9
+ */
10
+ export const normalizePurchaseId = <T extends Purchase | null | undefined>(
11
+ purchase: T,
12
+ ): T => {
13
+ if (!isPurchaseObject(purchase)) {
14
+ return purchase;
15
+ }
16
+
17
+ const transactionId = (purchase as Record<string, unknown>).transactionId;
18
+ if (typeof transactionId !== 'string' || transactionId.length === 0) {
19
+ return purchase;
20
+ }
21
+
22
+ if (purchase.id === transactionId) {
23
+ return purchase;
24
+ }
25
+
26
+ return {...purchase, id: transactionId} as T;
27
+ };
28
+
29
+ export const normalizePurchaseList = <T extends Purchase>(
30
+ purchases: T[] | null | undefined,
31
+ ): T[] => {
32
+ if (!Array.isArray(purchases)) {
33
+ return purchases ?? [];
34
+ }
35
+
36
+ if (purchases.length === 0) {
37
+ return purchases;
38
+ }
39
+
40
+ return purchases.map((purchase) => normalizePurchaseId(purchase)) as T[];
41
+ };
42
+
43
+ export const normalizePurchasePayload = <T extends Purchase | null | undefined>(
44
+ payload: T | T[] | null | undefined,
45
+ ): typeof payload => {
46
+ if (Array.isArray(payload)) {
47
+ return payload.map((purchase) =>
48
+ normalizePurchaseId(purchase),
49
+ ) as typeof payload;
50
+ }
51
+ return normalizePurchaseId(payload as T) as typeof payload;
52
+ };