react-native-iap 14.1.1-rc.1 → 14.2.0

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 (46) hide show
  1. package/NitroIap.podspec +2 -0
  2. package/README.md +9 -9
  3. package/android/build.gradle +2 -1
  4. package/android/consumer-rules.pro +7 -0
  5. package/app.plugin.js +1 -1
  6. package/ios/HybridRnIap.swift +380 -1192
  7. package/lib/module/helpers/subscription.js.map +1 -1
  8. package/lib/module/hooks/useIAP.js +74 -58
  9. package/lib/module/hooks/useIAP.js.map +1 -1
  10. package/lib/module/index.js +20 -3
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/types.js +8 -0
  13. package/lib/module/types.js.map +1 -1
  14. package/lib/module/utils/error.js.map +1 -1
  15. package/lib/module/utils/errorMapping.js +33 -0
  16. package/lib/module/utils/errorMapping.js.map +1 -0
  17. package/lib/module/utils/type-bridge.js +19 -0
  18. package/lib/module/utils/type-bridge.js.map +1 -1
  19. package/lib/typescript/src/helpers/subscription.d.ts.map +1 -1
  20. package/lib/typescript/src/hooks/useIAP.d.ts +4 -4
  21. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  22. package/lib/typescript/src/index.d.ts +7 -3
  23. package/lib/typescript/src/index.d.ts.map +1 -1
  24. package/lib/typescript/src/types.d.ts +19 -0
  25. package/lib/typescript/src/types.d.ts.map +1 -1
  26. package/lib/typescript/src/utils/error.d.ts.map +1 -1
  27. package/lib/typescript/src/utils/errorMapping.d.ts +5 -0
  28. package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -0
  29. package/lib/typescript/src/utils/type-bridge.d.ts +3 -2
  30. package/lib/typescript/src/utils/type-bridge.d.ts.map +1 -1
  31. package/package.json +5 -2
  32. package/plugin/tsconfig.tsbuildinfo +1 -1
  33. package/src/helpers/subscription.ts +30 -30
  34. package/src/hooks/useIAP.ts +252 -230
  35. package/src/index.ts +366 -340
  36. package/src/types.ts +21 -0
  37. package/src/utils/error.ts +19 -19
  38. package/src/utils/errorMapping.ts +44 -0
  39. package/src/utils/type-bridge.ts +127 -93
  40. package/ios/ErrorUtils.swift +0 -153
  41. package/ios/ProductStore.swift +0 -43
  42. package/ios/reactnativeiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  43. package/ios/reactnativeiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  44. package/plugin/build/src/withIAP.d.ts +0 -3
  45. package/plugin/build/src/withIAP.js +0 -81
  46. package/plugin/build/tsconfig.tsbuildinfo +0 -1
@@ -1,10 +1,9 @@
1
1
  // External dependencies
2
- import {useCallback, useEffect, useState, useRef} from 'react';
3
- import {Platform} from 'react-native';
2
+ import { useCallback, useEffect, useState, useRef } from 'react'
3
+ import { Platform } from 'react-native'
4
4
 
5
5
  // Internal modules
6
6
  import {
7
- endConnection,
8
7
  initConnection,
9
8
  purchaseErrorListener,
10
9
  purchaseUpdatedListener,
@@ -16,8 +15,9 @@ import {
16
15
  validateReceipt as validateReceiptInternal,
17
16
  getActiveSubscriptions,
18
17
  hasActiveSubscriptions,
19
- } from '../';
20
- import {syncIOS, requestPromotedProductIOS, buyPromotedProductIOS} from '../';
18
+ restorePurchases as restorePurchasesTopLevel,
19
+ } from '../'
20
+ import { getPromotedProductIOS, requestPurchaseOnPromotedProductIOS } from '../'
21
21
 
22
22
  // Types
23
23
  import type {
@@ -29,270 +29,287 @@ import type {
29
29
  RequestPurchaseProps,
30
30
  RequestSubscriptionProps,
31
31
  ActiveSubscription,
32
- } from '../types';
32
+ } from '../types'
33
33
 
34
34
  // Types for event subscriptions
35
35
  interface EventSubscription {
36
- remove(): void;
36
+ remove(): void
37
37
  }
38
38
 
39
39
  type UseIap = {
40
- connected: boolean;
41
- products: Product[];
42
- promotedProductsIOS: Purchase[];
43
- promotedProductIdIOS?: string;
44
- subscriptions: SubscriptionProduct[];
45
- availablePurchases: Purchase[];
46
- currentPurchase?: Purchase;
47
- currentPurchaseError?: PurchaseError;
48
- promotedProductIOS?: Product;
49
- activeSubscriptions: ActiveSubscription[];
50
- clearCurrentPurchase: () => void;
51
- clearCurrentPurchaseError: () => void;
40
+ connected: boolean
41
+ products: Product[]
42
+ promotedProductsIOS: Purchase[]
43
+ promotedProductIdIOS?: string
44
+ subscriptions: SubscriptionProduct[]
45
+ availablePurchases: Purchase[]
46
+ currentPurchase?: Purchase
47
+ currentPurchaseError?: PurchaseError
48
+ promotedProductIOS?: Product
49
+ activeSubscriptions: ActiveSubscription[]
50
+ clearCurrentPurchase: () => void
51
+ clearCurrentPurchaseError: () => void
52
52
  finishTransaction: ({
53
53
  purchase,
54
54
  isConsumable,
55
55
  }: {
56
- purchase: Purchase;
57
- isConsumable?: boolean;
58
- }) => Promise<PurchaseResult | boolean>;
59
- getAvailablePurchases: () => Promise<void>;
56
+ purchase: Purchase
57
+ isConsumable?: boolean
58
+ }) => Promise<PurchaseResult | boolean>
59
+ getAvailablePurchases: (skus?: string[]) => Promise<void>
60
60
  fetchProducts: (params: {
61
- skus: string[];
62
- type?: 'inapp' | 'subs';
63
- }) => Promise<void>;
61
+ skus: string[]
62
+ type?: 'inapp' | 'subs'
63
+ }) => Promise<void>
64
64
  /**
65
65
  * @deprecated Use fetchProducts({ skus, type: 'inapp' }) instead. This method will be removed in version 3.0.0.
66
66
  * Note: This method internally uses fetchProducts, so no deprecation warning is shown.
67
67
  */
68
- getProducts: (skus: string[]) => Promise<void>;
68
+ getProducts: (skus: string[]) => Promise<void>
69
69
  /**
70
70
  * @deprecated Use fetchProducts({ skus, type: 'subs' }) instead. This method will be removed in version 3.0.0.
71
71
  * Note: This method internally uses fetchProducts, so no deprecation warning is shown.
72
72
  */
73
- getSubscriptions: (skus: string[]) => Promise<void>;
73
+ getSubscriptions: (skus: string[]) => Promise<void>
74
74
  requestPurchase: (params: {
75
- request: RequestPurchaseProps | RequestSubscriptionProps;
76
- type?: 'inapp' | 'subs';
77
- }) => Promise<any>;
75
+ request: RequestPurchaseProps | RequestSubscriptionProps
76
+ type?: 'inapp' | 'subs'
77
+ }) => Promise<any>
78
78
  validateReceipt: (
79
79
  sku: string,
80
80
  androidOptions?: {
81
- packageName: string;
82
- productToken: string;
83
- accessToken: string;
84
- isSub?: boolean;
85
- },
86
- ) => Promise<any>;
87
- restorePurchases: () => Promise<void>; // 구매 복원 함수 추가
88
- requestPromotedProductIOS: () => Promise<any | null>;
89
- buyPromotedProductIOS: () => Promise<void>;
81
+ packageName: string
82
+ productToken: string
83
+ accessToken: string
84
+ isSub?: boolean
85
+ }
86
+ ) => Promise<any>
87
+ restorePurchases: () => Promise<void>
88
+ getPromotedProductIOS: () => Promise<Product | null>
89
+ requestPurchaseOnPromotedProductIOS: () => Promise<void>
90
90
  getActiveSubscriptions: (
91
- subscriptionIds?: string[],
92
- ) => Promise<ActiveSubscription[]>;
93
- hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
94
- };
91
+ subscriptionIds?: string[]
92
+ ) => Promise<ActiveSubscription[]>
93
+ hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>
94
+ }
95
95
 
96
96
  export interface UseIapOptions {
97
- onPurchaseSuccess?: (purchase: Purchase) => void;
98
- onPurchaseError?: (error: PurchaseError) => void;
99
- onSyncError?: (error: Error) => void;
100
- shouldAutoSyncPurchases?: boolean; // New option to control auto-syncing
101
- onPromotedProductIOS?: (product: Product) => void;
97
+ onPurchaseSuccess?: (purchase: Purchase) => void
98
+ onPurchaseError?: (error: PurchaseError) => void
99
+ onSyncError?: (error: Error) => void
100
+ shouldAutoSyncPurchases?: boolean // New option to control auto-syncing
101
+ onPromotedProductIOS?: (product: Product) => void
102
102
  }
103
103
 
104
104
  /**
105
105
  * React Hook for managing In-App Purchases.
106
- * See documentation at https://expo-iap.hyo.dev/docs/hooks/useIAP
106
+ * See documentation at https://react-native-iap.hyo.dev/docs/hooks/useIAP
107
107
  */
108
108
  export function useIAP(options?: UseIapOptions): UseIap {
109
- const [connected, setConnected] = useState<boolean>(false);
110
- const [products, setProducts] = useState<Product[]>([]);
111
- const [promotedProductsIOS] = useState<Purchase[]>([]);
112
- const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);
113
- const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([]);
114
- const [currentPurchase, setCurrentPurchase] = useState<Purchase>();
115
- const [promotedProductIOS, setPromotedProductIOS] = useState<Product>();
109
+ const [connected, setConnected] = useState<boolean>(false)
110
+ const [products, setProducts] = useState<Product[]>([])
111
+ const [promotedProductsIOS] = useState<Purchase[]>([])
112
+ const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([])
113
+ const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([])
114
+ const [currentPurchase, setCurrentPurchase] = useState<Purchase>()
115
+ const [promotedProductIOS, setPromotedProductIOS] = useState<Product>()
116
116
  const [currentPurchaseError, setCurrentPurchaseError] =
117
- useState<PurchaseError>();
118
- const [promotedProductIdIOS] = useState<string>();
117
+ useState<PurchaseError>()
118
+ const [promotedProductIdIOS] = useState<string>()
119
119
  const [activeSubscriptions, setActiveSubscriptions] = useState<
120
120
  ActiveSubscription[]
121
- >([]);
121
+ >([])
122
122
 
123
- const optionsRef = useRef<UseIapOptions | undefined>(options);
123
+ const optionsRef = useRef<UseIapOptions | undefined>(options)
124
+ const connectedRef = useRef<boolean>(false)
124
125
 
125
126
  // Helper function to merge arrays with duplicate checking
126
127
  const mergeWithDuplicateCheck = useCallback(
127
128
  <T>(
128
129
  existingItems: T[],
129
130
  newItems: T[],
130
- getKey: (item: T) => string,
131
+ getKey: (item: T) => string
131
132
  ): T[] => {
132
- const merged = [...existingItems];
133
+ const merged = [...existingItems]
133
134
  newItems.forEach((newItem) => {
134
135
  const isDuplicate = merged.some(
135
- (existingItem) => getKey(existingItem) === getKey(newItem),
136
- );
136
+ (existingItem) => getKey(existingItem) === getKey(newItem)
137
+ )
137
138
  if (!isDuplicate) {
138
- merged.push(newItem);
139
+ merged.push(newItem)
139
140
  }
140
- });
141
- return merged;
141
+ })
142
+ return merged
142
143
  },
143
- [],
144
- );
144
+ []
145
+ )
146
+
147
+ useEffect(() => {
148
+ optionsRef.current = options
149
+ }, [options])
145
150
 
146
151
  useEffect(() => {
147
- optionsRef.current = options;
148
- }, [options]);
152
+ connectedRef.current = connected
153
+ }, [connected])
149
154
 
150
155
  const subscriptionsRef = useRef<{
151
- purchaseUpdate?: EventSubscription;
152
- purchaseError?: EventSubscription;
153
- promotedProductsIOS?: EventSubscription;
154
- promotedProductIOS?: EventSubscription;
155
- }>({});
156
+ purchaseUpdate?: EventSubscription
157
+ purchaseError?: EventSubscription
158
+ promotedProductsIOS?: EventSubscription
159
+ promotedProductIOS?: EventSubscription
160
+ }>({})
156
161
 
157
- const subscriptionsRefState = useRef<SubscriptionProduct[]>([]);
162
+ const subscriptionsRefState = useRef<SubscriptionProduct[]>([])
158
163
 
159
164
  useEffect(() => {
160
- subscriptionsRefState.current = subscriptions;
161
- }, [subscriptions]);
165
+ subscriptionsRefState.current = subscriptions
166
+ }, [subscriptions])
162
167
 
163
168
  const clearCurrentPurchase = useCallback(() => {
164
- setCurrentPurchase(undefined);
165
- }, []);
169
+ setCurrentPurchase(undefined)
170
+ }, [])
166
171
 
167
172
  const clearCurrentPurchaseError = useCallback(() => {
168
- setCurrentPurchaseError(undefined);
169
- }, []);
173
+ setCurrentPurchaseError(undefined)
174
+ }, [])
170
175
 
171
176
  const getProductsInternal = useCallback(
172
177
  async (skus: string[]): Promise<void> => {
173
178
  try {
174
- const result = await fetchProducts({skus, type: 'inapp'});
179
+ const result = await fetchProducts({ skus, type: 'inapp' })
175
180
  setProducts((prevProducts: Product[]) =>
176
181
  mergeWithDuplicateCheck(
177
182
  prevProducts,
178
183
  result as Product[],
179
- (product: Product) => product.id,
180
- ),
181
- );
184
+ (product: Product) => product.id
185
+ )
186
+ )
182
187
  } catch (error) {
183
- console.error('Error fetching products:', error);
188
+ console.error('Error fetching products:', error)
184
189
  }
185
190
  },
186
- [mergeWithDuplicateCheck],
187
- );
191
+ [mergeWithDuplicateCheck]
192
+ )
188
193
 
189
194
  const getSubscriptionsInternal = useCallback(
190
195
  async (skus: string[]): Promise<void> => {
191
196
  try {
192
- const result = await fetchProducts({skus, type: 'subs'});
197
+ const result = await fetchProducts({ skus, type: 'subs' })
193
198
  setSubscriptions((prevSubscriptions: SubscriptionProduct[]) =>
194
199
  mergeWithDuplicateCheck(
195
200
  prevSubscriptions,
196
201
  result as SubscriptionProduct[],
197
- (subscription: SubscriptionProduct) => subscription.id,
198
- ),
199
- );
202
+ (subscription: SubscriptionProduct) => subscription.id
203
+ )
204
+ )
200
205
  } catch (error) {
201
- console.error('Error fetching subscriptions:', error);
206
+ console.error('Error fetching subscriptions:', error)
202
207
  }
203
208
  },
204
- [mergeWithDuplicateCheck],
205
- );
209
+ [mergeWithDuplicateCheck]
210
+ )
206
211
 
207
212
  const fetchProductsInternal = useCallback(
208
213
  async (params: {
209
- skus: string[];
210
- type?: 'inapp' | 'subs';
214
+ skus: string[]
215
+ type?: 'inapp' | 'subs'
211
216
  }): Promise<void> => {
217
+ if (!connectedRef.current) {
218
+ console.warn(
219
+ '[useIAP] fetchProducts called before connection; skipping'
220
+ )
221
+ return
222
+ }
212
223
  try {
213
- const result = await fetchProducts(params);
224
+ const result = await fetchProducts(params)
214
225
  if (params.type === 'subs') {
215
226
  setSubscriptions((prevSubscriptions: SubscriptionProduct[]) =>
216
227
  mergeWithDuplicateCheck(
217
228
  prevSubscriptions,
218
229
  result as SubscriptionProduct[],
219
- (subscription: SubscriptionProduct) => subscription.id,
220
- ),
221
- );
230
+ (subscription: SubscriptionProduct) => subscription.id
231
+ )
232
+ )
222
233
  } else {
223
234
  setProducts((prevProducts: Product[]) =>
224
235
  mergeWithDuplicateCheck(
225
236
  prevProducts,
226
237
  result as Product[],
227
- (product: Product) => product.id,
228
- ),
229
- );
238
+ (product: Product) => product.id
239
+ )
240
+ )
230
241
  }
231
242
  } catch (error) {
232
- console.error('Error fetching products:', error);
243
+ console.error('Error fetching products:', error)
233
244
  }
234
245
  },
235
- [mergeWithDuplicateCheck],
236
- );
237
-
238
- const getAvailablePurchasesInternal = useCallback(async (): Promise<void> => {
239
- try {
240
- const result = await getAvailablePurchases();
241
- setAvailablePurchases(result);
242
- } catch (error) {
243
- console.error('Error fetching available purchases:', error);
244
- }
245
- }, []);
246
+ [mergeWithDuplicateCheck]
247
+ )
248
+
249
+ const getAvailablePurchasesInternal = useCallback(
250
+ async (_skus?: string[]): Promise<void> => {
251
+ try {
252
+ const result = await getAvailablePurchases({
253
+ alsoPublishToEventListenerIOS: false,
254
+ onlyIncludeActiveItemsIOS: true,
255
+ })
256
+ setAvailablePurchases(result)
257
+ } catch (error) {
258
+ console.error('Error fetching available purchases:', error)
259
+ }
260
+ },
261
+ []
262
+ )
246
263
 
247
264
  const getActiveSubscriptionsInternal = useCallback(
248
265
  async (subscriptionIds?: string[]): Promise<ActiveSubscription[]> => {
249
266
  try {
250
- const result = await getActiveSubscriptions(subscriptionIds);
251
- setActiveSubscriptions(result);
252
- return result;
267
+ const result = await getActiveSubscriptions(subscriptionIds)
268
+ setActiveSubscriptions(result)
269
+ return result
253
270
  } catch (error) {
254
- console.error('Error getting active subscriptions:', error);
271
+ console.error('Error getting active subscriptions:', error)
255
272
  // Don't clear existing activeSubscriptions on error - preserve current state
256
273
  // This prevents the UI from showing empty state when there are temporary network issues
257
- return [];
274
+ return []
258
275
  }
259
276
  },
260
- [],
261
- );
277
+ []
278
+ )
262
279
 
263
280
  const hasActiveSubscriptionsInternal = useCallback(
264
281
  async (subscriptionIds?: string[]): Promise<boolean> => {
265
282
  try {
266
- return await hasActiveSubscriptions(subscriptionIds);
283
+ return await hasActiveSubscriptions(subscriptionIds)
267
284
  } catch (error) {
268
- console.error('Error checking active subscriptions:', error);
269
- return false;
285
+ console.error('Error checking active subscriptions:', error)
286
+ return false
270
287
  }
271
288
  },
272
- [],
273
- );
289
+ []
290
+ )
274
291
 
275
292
  const finishTransaction = useCallback(
276
293
  async ({
277
294
  purchase,
278
295
  isConsumable,
279
296
  }: {
280
- purchase: Purchase;
281
- isConsumable?: boolean;
297
+ purchase: Purchase
298
+ isConsumable?: boolean
282
299
  }): Promise<PurchaseResult | boolean> => {
283
300
  try {
284
301
  return await finishTransactionInternal({
285
302
  purchase,
286
303
  isConsumable,
287
- });
304
+ })
288
305
  } catch (err) {
289
- throw err;
306
+ throw err
290
307
  } finally {
291
308
  if (purchase.id === currentPurchase?.id) {
292
- clearCurrentPurchase();
309
+ clearCurrentPurchase()
293
310
  }
294
311
  if (purchase.id === currentPurchaseError?.productId) {
295
- clearCurrentPurchaseError();
312
+ clearCurrentPurchaseError()
296
313
  }
297
314
  }
298
315
  },
@@ -301,118 +318,113 @@ export function useIAP(options?: UseIapOptions): UseIap {
301
318
  currentPurchaseError?.productId,
302
319
  clearCurrentPurchase,
303
320
  clearCurrentPurchaseError,
304
- ],
305
- );
321
+ ]
322
+ )
306
323
 
307
324
  const requestPurchaseWithReset = useCallback(
308
- async (requestObj: {request: any; type?: 'inapp' | 'subs'}) => {
309
- clearCurrentPurchase();
310
- clearCurrentPurchaseError();
325
+ async (requestObj: { request: any; type?: 'inapp' | 'subs' }) => {
326
+ clearCurrentPurchase()
327
+ clearCurrentPurchaseError()
311
328
 
312
329
  try {
313
- return await requestPurchaseInternal(requestObj);
330
+ return await requestPurchaseInternal(requestObj)
314
331
  } catch (error) {
315
- throw error;
332
+ throw error
316
333
  }
317
334
  },
318
- [clearCurrentPurchase, clearCurrentPurchaseError],
319
- );
320
-
321
- const restorePurchases = useCallback(async (): Promise<void> => {
322
- try {
323
- if (Platform.OS === 'ios') {
324
- await syncIOS().catch((error) => {
325
- if (optionsRef.current?.onSyncError) {
326
- optionsRef.current.onSyncError(error);
327
- } else {
328
- console.warn('Error restoring purchases:', error);
329
- }
330
- });
331
- }
332
- await getAvailablePurchasesInternal();
333
- } catch (error) {
334
- console.warn('Failed to restore purchases:', error);
335
- }
336
- }, [getAvailablePurchasesInternal]);
335
+ [clearCurrentPurchase, clearCurrentPurchaseError]
336
+ )
337
+
338
+ // No local restorePurchases; use the top-level helper via returned API
337
339
 
338
340
  const validateReceipt = useCallback(
339
341
  async (
340
342
  sku: string,
341
343
  androidOptions?: {
342
- packageName: string;
343
- productToken: string;
344
- accessToken: string;
345
- isSub?: boolean;
346
- },
344
+ packageName: string
345
+ productToken: string
346
+ accessToken: string
347
+ isSub?: boolean
348
+ }
347
349
  ) => {
348
- return validateReceiptInternal(sku, androidOptions);
350
+ return validateReceiptInternal(sku, androidOptions)
349
351
  },
350
- [],
351
- );
352
+ []
353
+ )
352
354
 
353
355
  const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
354
- const result = await initConnection();
355
- setConnected(result);
356
-
357
- if (result) {
358
- subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
359
- async (purchase: Purchase) => {
360
- setCurrentPurchaseError(undefined);
361
- setCurrentPurchase(purchase);
362
-
363
- // Always refresh subscription state after a purchase event
364
- try {
365
- await getActiveSubscriptionsInternal();
366
- await getAvailablePurchasesInternal();
367
- } catch (e) {
368
- // Non-fatal: UI will still update from event data
369
- console.warn('[useIAP] post-purchase refresh failed:', e);
370
- }
371
-
372
- if (optionsRef.current?.onPurchaseSuccess) {
373
- optionsRef.current.onPurchaseSuccess(purchase);
356
+ // Register listeners BEFORE initConnection to avoid race condition
357
+ subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
358
+ async (purchase: Purchase) => {
359
+ setCurrentPurchaseError(undefined)
360
+ setCurrentPurchase(purchase)
361
+ // Always refresh subscription state after a purchase event
362
+ try {
363
+ await getActiveSubscriptionsInternal()
364
+ await getAvailablePurchasesInternal()
365
+ } catch (e) {
366
+ console.warn('[useIAP] post-purchase refresh failed:', e)
367
+ }
368
+ if (optionsRef.current?.onPurchaseSuccess) {
369
+ optionsRef.current.onPurchaseSuccess(purchase)
370
+ }
371
+ }
372
+ )
373
+
374
+ subscriptionsRef.current.purchaseError = purchaseErrorListener(
375
+ (error: PurchaseError) => {
376
+ // Ignore init error until connected
377
+ if (
378
+ error &&
379
+ (error as any).code === 'E_INIT_CONNECTION' &&
380
+ !connectedRef.current
381
+ ) {
382
+ return
383
+ }
384
+ setCurrentPurchase(undefined)
385
+ setCurrentPurchaseError(error)
386
+ if (optionsRef.current?.onPurchaseError) {
387
+ optionsRef.current.onPurchaseError(error)
388
+ }
389
+ }
390
+ )
391
+
392
+ if (Platform.OS === 'ios') {
393
+ subscriptionsRef.current.promotedProductsIOS = promotedProductListenerIOS(
394
+ (product: Product) => {
395
+ setPromotedProductIOS(product)
396
+ if (optionsRef.current?.onPromotedProductIOS) {
397
+ optionsRef.current.onPromotedProductIOS(product)
374
398
  }
375
- },
376
- );
377
-
378
- subscriptionsRef.current.purchaseError = purchaseErrorListener(
379
- (error: PurchaseError) => {
380
- setCurrentPurchase(undefined);
381
- setCurrentPurchaseError(error);
399
+ }
400
+ )
401
+ }
382
402
 
383
- if (optionsRef.current?.onPurchaseError) {
384
- optionsRef.current.onPurchaseError(error);
385
- }
386
- },
387
- );
388
-
389
- if (Platform.OS === 'ios') {
390
- // iOS promoted products listener
391
- subscriptionsRef.current.promotedProductsIOS =
392
- promotedProductListenerIOS((product: Product) => {
393
- setPromotedProductIOS(product);
394
-
395
- if (optionsRef.current?.onPromotedProductIOS) {
396
- optionsRef.current.onPromotedProductIOS(product);
397
- }
398
- });
399
- }
403
+ const result = await initConnection()
404
+ setConnected(result)
405
+ if (!result) {
406
+ // Clean up some listeners but leave purchaseError for potential retries
407
+ subscriptionsRef.current.purchaseUpdate?.remove()
408
+ subscriptionsRef.current.promotedProductsIOS?.remove()
409
+ subscriptionsRef.current.purchaseUpdate = undefined
410
+ subscriptionsRef.current.promotedProductsIOS = undefined
411
+ return
400
412
  }
401
- }, [getActiveSubscriptionsInternal, getAvailablePurchasesInternal]);
413
+ }, [getActiveSubscriptionsInternal, getAvailablePurchasesInternal])
402
414
 
403
415
  useEffect(() => {
404
- initIapWithSubscriptions();
405
- const currentSubscriptions = subscriptionsRef.current;
416
+ initIapWithSubscriptions()
417
+ const currentSubscriptions = subscriptionsRef.current
406
418
 
407
419
  return () => {
408
- currentSubscriptions.purchaseUpdate?.remove();
409
- currentSubscriptions.purchaseError?.remove();
410
- currentSubscriptions.promotedProductsIOS?.remove();
411
- currentSubscriptions.promotedProductIOS?.remove();
412
- endConnection();
413
- setConnected(false);
414
- };
415
- }, [initIapWithSubscriptions]);
420
+ currentSubscriptions.purchaseUpdate?.remove()
421
+ currentSubscriptions.purchaseError?.remove()
422
+ currentSubscriptions.promotedProductsIOS?.remove()
423
+ currentSubscriptions.promotedProductIOS?.remove()
424
+ // Keep connection alive across screens to avoid race conditions
425
+ setConnected(false)
426
+ }
427
+ }, [initIapWithSubscriptions])
416
428
 
417
429
  return {
418
430
  connected,
@@ -432,12 +444,22 @@ export function useIAP(options?: UseIapOptions): UseIap {
432
444
  fetchProducts: fetchProductsInternal,
433
445
  requestPurchase: requestPurchaseWithReset,
434
446
  validateReceipt,
435
- restorePurchases,
447
+ restorePurchases: async () => {
448
+ try {
449
+ const purchases = await restorePurchasesTopLevel({
450
+ alsoPublishToEventListenerIOS: false,
451
+ onlyIncludeActiveItemsIOS: true,
452
+ })
453
+ setAvailablePurchases(purchases)
454
+ } catch (e) {
455
+ console.warn('Failed to restore purchases:', e)
456
+ }
457
+ },
436
458
  getProducts: getProductsInternal,
437
459
  getSubscriptions: getSubscriptionsInternal,
438
- requestPromotedProductIOS,
439
- buyPromotedProductIOS,
460
+ getPromotedProductIOS,
461
+ requestPurchaseOnPromotedProductIOS,
440
462
  getActiveSubscriptions: getActiveSubscriptionsInternal,
441
463
  hasActiveSubscriptions: hasActiveSubscriptionsInternal,
442
- };
464
+ }
443
465
  }