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.
- package/NitroIap.podspec +2 -0
- package/README.md +9 -9
- package/android/build.gradle +2 -1
- package/android/consumer-rules.pro +7 -0
- package/app.plugin.js +1 -1
- package/ios/HybridRnIap.swift +380 -1192
- package/lib/module/helpers/subscription.js.map +1 -1
- package/lib/module/hooks/useIAP.js +74 -58
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +20 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +8 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/error.js.map +1 -1
- package/lib/module/utils/errorMapping.js +33 -0
- package/lib/module/utils/errorMapping.js.map +1 -0
- package/lib/module/utils/type-bridge.js +19 -0
- package/lib/module/utils/type-bridge.js.map +1 -1
- package/lib/typescript/src/helpers/subscription.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +4 -4
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +7 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +19 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/error.d.ts.map +1 -1
- package/lib/typescript/src/utils/errorMapping.d.ts +5 -0
- package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -0
- package/lib/typescript/src/utils/type-bridge.d.ts +3 -2
- package/lib/typescript/src/utils/type-bridge.d.ts.map +1 -1
- package/package.json +5 -2
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/helpers/subscription.ts +30 -30
- package/src/hooks/useIAP.ts +252 -230
- package/src/index.ts +366 -340
- package/src/types.ts +21 -0
- package/src/utils/error.ts +19 -19
- package/src/utils/errorMapping.ts +44 -0
- package/src/utils/type-bridge.ts +127 -93
- package/ios/ErrorUtils.swift +0 -153
- package/ios/ProductStore.swift +0 -43
- package/ios/reactnativeiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/reactnativeiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/plugin/build/src/withIAP.d.ts +0 -3
- package/plugin/build/src/withIAP.js +0 -81
- package/plugin/build/tsconfig.tsbuildinfo +0 -1
package/src/hooks/useIAP.ts
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
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://
|
|
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
|
-
|
|
148
|
-
}, [
|
|
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(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
460
|
+
getPromotedProductIOS,
|
|
461
|
+
requestPurchaseOnPromotedProductIOS,
|
|
440
462
|
getActiveSubscriptions: getActiveSubscriptionsInternal,
|
|
441
463
|
hasActiveSubscriptions: hasActiveSubscriptionsInternal,
|
|
442
|
-
}
|
|
464
|
+
}
|
|
443
465
|
}
|