expo-iap 3.1.35 → 3.1.37
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/CLAUDE.md +2 -3
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +71 -9
- package/build/index.d.ts +38 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +48 -0
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +4 -2
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +2 -0
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +2 -1
- package/build/modules/ios.js.map +1 -1
- package/build/types.d.ts +109 -51
- package/build/types.d.ts.map +1 -1
- package/build/types.js +3 -0
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +5 -2
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +9 -1
- package/build/useIAP.js.map +1 -1
- package/build/utils/errorMapping.d.ts.map +1 -1
- package/build/utils/errorMapping.js +3 -0
- package/build/utils/errorMapping.js.map +1 -1
- package/coverage/clover.xml +185 -176
- package/coverage/coverage-final.json +4 -4
- package/coverage/lcov-report/index.html +18 -18
- package/coverage/lcov-report/src/index.html +18 -18
- package/coverage/lcov-report/src/index.ts.html +178 -10
- package/coverage/lcov-report/src/modules/android.ts.html +10 -4
- package/coverage/lcov-report/src/modules/index.html +1 -1
- package/coverage/lcov-report/src/modules/ios.ts.html +10 -13
- package/coverage/lcov-report/src/utils/debug.ts.html +1 -1
- package/coverage/lcov-report/src/utils/errorMapping.ts.html +18 -3
- package/coverage/lcov-report/src/utils/index.html +1 -1
- package/coverage/lcov.info +338 -319
- package/ios/ExpoIapHelper.swift +18 -0
- package/ios/ExpoIapModule.swift +56 -15
- package/openiap-versions.json +3 -3
- package/package.json +1 -1
- package/src/index.ts +56 -0
- package/src/modules/android.ts +4 -2
- package/src/modules/ios.ts +7 -8
- package/src/types.ts +126 -57
- package/src/useIAP.ts +27 -5
- package/src/utils/errorMapping.ts +5 -0
package/ios/ExpoIapHelper.swift
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
1
2
|
import Foundation
|
|
2
3
|
import OpenIAP
|
|
3
4
|
|
|
5
|
+
/// Exception wrapper for PurchaseError that preserves OpenIAP error codes
|
|
6
|
+
/// This ensures consistent error format between try-catch and onPurchaseError callback
|
|
7
|
+
class IapException: GenericException<(code: String, message: String, productId: String?)> {
|
|
8
|
+
override var code: String { param.code }
|
|
9
|
+
override var reason: String { param.message }
|
|
10
|
+
|
|
11
|
+
var productId: String? { param.productId }
|
|
12
|
+
|
|
13
|
+
static func from(_ error: PurchaseError) -> IapException {
|
|
14
|
+
let payload = OpenIapSerialization.encode(error)
|
|
15
|
+
let code = payload["code"] as? String ?? "unknown"
|
|
16
|
+
let message = payload["message"] as? String ?? error.localizedDescription
|
|
17
|
+
let productId = payload["productId"] as? String
|
|
18
|
+
return IapException((code: code, message: message, productId: productId))
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
enum ExpoIapHelper {
|
|
5
23
|
private static var listeners: [Subscription] = []
|
|
6
24
|
|
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -13,9 +13,9 @@ public final class ExpoIapModule: Module {
|
|
|
13
13
|
nonisolated public func definition() -> ModuleDefinition {
|
|
14
14
|
Name("ExpoIap")
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
Constant("ERROR_CODES") {
|
|
17
|
+
OpenIapSerialization.errorCodes()
|
|
18
|
+
}
|
|
19
19
|
|
|
20
20
|
Events(
|
|
21
21
|
OpenIapEvent.purchaseUpdated.rawValue,
|
|
@@ -61,10 +61,8 @@ public final class ExpoIapModule: Module {
|
|
|
61
61
|
|
|
62
62
|
AsyncFunction("requestPurchase") { (payload: [String: Any]) async throws -> Any? in
|
|
63
63
|
ExpoIapLog.payload("requestPurchase", payload: payload)
|
|
64
|
-
print("🔍 [ExpoIap] Raw payload useAlternativeBilling: \(payload["useAlternativeBilling"] ?? "nil")")
|
|
65
64
|
try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
|
|
66
65
|
let props = try ExpoIapHelper.decodeRequestPurchaseProps(from: payload)
|
|
67
|
-
print("🔍 [ExpoIap] Decoded props useAlternativeBilling: \(props.useAlternativeBilling ?? false)")
|
|
68
66
|
|
|
69
67
|
do {
|
|
70
68
|
guard let result = try await OpenIapModule.shared.requestPurchase(props) else {
|
|
@@ -86,10 +84,11 @@ public final class ExpoIapModule: Module {
|
|
|
86
84
|
}
|
|
87
85
|
} catch let error as PurchaseError {
|
|
88
86
|
ExpoIapLog.failure("requestPurchase", error: error)
|
|
89
|
-
throw error
|
|
87
|
+
throw IapException.from(error)
|
|
90
88
|
} catch {
|
|
91
89
|
ExpoIapLog.failure("requestPurchase", error: error)
|
|
92
|
-
throw
|
|
90
|
+
throw IapException.from(
|
|
91
|
+
PurchaseError.make(code: .purchaseError, message: error.localizedDescription))
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
|
|
@@ -189,18 +188,60 @@ public final class ExpoIapModule: Module {
|
|
|
189
188
|
ExpoIapLog.payload("validateReceiptIOS", payload: ["sku": sku])
|
|
190
189
|
try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
|
|
191
190
|
do {
|
|
192
|
-
let props = try OpenIapSerialization.
|
|
193
|
-
let result = try await OpenIapModule.shared.
|
|
191
|
+
let props = try OpenIapSerialization.verifyPurchaseProps(from: ["sku": sku])
|
|
192
|
+
let result = try await OpenIapModule.shared.verifyPurchase(props)
|
|
194
193
|
var payload = OpenIapSerialization.encode(result)
|
|
195
|
-
|
|
194
|
+
|
|
195
|
+
// Extract jwsRepresentation from the result
|
|
196
|
+
if case .verifyPurchaseResultIos(let iosResult) = result {
|
|
197
|
+
payload["purchaseToken"] = iosResult.jwsRepresentation
|
|
198
|
+
}
|
|
199
|
+
|
|
196
200
|
let sanitized = ExpoIapHelper.sanitizeDictionary(payload)
|
|
197
201
|
ExpoIapLog.result("validateReceiptIOS", value: sanitized)
|
|
198
202
|
return sanitized
|
|
199
203
|
} catch let error as PurchaseError {
|
|
200
204
|
ExpoIapLog.failure("validateReceiptIOS", error: error)
|
|
201
|
-
throw error
|
|
205
|
+
throw IapException.from(error)
|
|
202
206
|
} catch {
|
|
203
207
|
ExpoIapLog.failure("validateReceiptIOS", error: error)
|
|
208
|
+
throw IapException.from(PurchaseError.make(code: .receiptFailed))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
AsyncFunction("verifyPurchase") { (params: [String: Any]) async throws -> [String: Any] in
|
|
213
|
+
ExpoIapLog.payload("verifyPurchase", payload: params)
|
|
214
|
+
try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
|
|
215
|
+
do {
|
|
216
|
+
let props = try OpenIapSerialization.verifyPurchaseProps(from: params)
|
|
217
|
+
let result = try await OpenIapModule.shared.verifyPurchase(props)
|
|
218
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
|
|
219
|
+
ExpoIapLog.result("verifyPurchase", value: sanitized)
|
|
220
|
+
return sanitized
|
|
221
|
+
} catch let error as PurchaseError {
|
|
222
|
+
ExpoIapLog.failure("verifyPurchase", error: error)
|
|
223
|
+
throw error
|
|
224
|
+
} catch {
|
|
225
|
+
ExpoIapLog.failure("verifyPurchase", error: error)
|
|
226
|
+
throw PurchaseError.make(code: .receiptFailed)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
AsyncFunction("verifyPurchaseWithProvider") { (params: [String: Any]) async throws -> [String: Any] in
|
|
231
|
+
ExpoIapLog.payload("verifyPurchaseWithProvider", payload: params)
|
|
232
|
+
try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
|
|
233
|
+
do {
|
|
234
|
+
let jsonData = try JSONSerialization.data(withJSONObject: params)
|
|
235
|
+
let props = try JSONDecoder().decode(VerifyPurchaseWithProviderProps.self, from: jsonData)
|
|
236
|
+
let result = try await OpenIapModule.shared.verifyPurchaseWithProvider(props)
|
|
237
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
|
|
238
|
+
ExpoIapLog.result("verifyPurchaseWithProvider", value: sanitized)
|
|
239
|
+
return sanitized
|
|
240
|
+
} catch let error as PurchaseError {
|
|
241
|
+
ExpoIapLog.failure("verifyPurchaseWithProvider", error: error)
|
|
242
|
+
throw error
|
|
243
|
+
} catch {
|
|
244
|
+
ExpoIapLog.failure("verifyPurchaseWithProvider", error: error)
|
|
204
245
|
throw PurchaseError.make(code: .receiptFailed)
|
|
205
246
|
}
|
|
206
247
|
}
|
|
@@ -312,10 +353,10 @@ public final class ExpoIapModule: Module {
|
|
|
312
353
|
return nil
|
|
313
354
|
} catch let error as PurchaseError {
|
|
314
355
|
ExpoIapLog.failure("currentEntitlementIOS", error: error)
|
|
315
|
-
throw error
|
|
356
|
+
throw IapException.from(error)
|
|
316
357
|
} catch {
|
|
317
358
|
ExpoIapLog.failure("currentEntitlementIOS", error: error)
|
|
318
|
-
throw PurchaseError.make(code: .skuNotFound, productId: sku)
|
|
359
|
+
throw IapException.from(PurchaseError.make(code: .skuNotFound, productId: sku))
|
|
319
360
|
}
|
|
320
361
|
}
|
|
321
362
|
|
|
@@ -332,10 +373,10 @@ public final class ExpoIapModule: Module {
|
|
|
332
373
|
return nil
|
|
333
374
|
} catch let error as PurchaseError {
|
|
334
375
|
ExpoIapLog.failure("latestTransactionIOS", error: error)
|
|
335
|
-
throw error
|
|
376
|
+
throw IapException.from(error)
|
|
336
377
|
} catch {
|
|
337
378
|
ExpoIapLog.failure("latestTransactionIOS", error: error)
|
|
338
|
-
throw PurchaseError.make(code: .skuNotFound, productId: sku)
|
|
379
|
+
throw IapException.from(PurchaseError.make(code: .skuNotFound, productId: sku))
|
|
339
380
|
}
|
|
340
381
|
}
|
|
341
382
|
|
package/openiap-versions.json
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -708,6 +708,8 @@ export const deepLinkToSubscriptions: MutationField<
|
|
|
708
708
|
* For production apps, always validate receipts on your secure server:
|
|
709
709
|
* - iOS: Send receipt data to Apple's verification endpoint from your server
|
|
710
710
|
* - Android: Use Google Play Developer API with service account credentials
|
|
711
|
+
*
|
|
712
|
+
* @deprecated Use verifyPurchase instead
|
|
711
713
|
*/
|
|
712
714
|
export const validateReceipt: MutationField<'validateReceipt'> = async (
|
|
713
715
|
options,
|
|
@@ -741,6 +743,60 @@ export const validateReceipt: MutationField<'validateReceipt'> = async (
|
|
|
741
743
|
throw new Error('Platform not supported');
|
|
742
744
|
};
|
|
743
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Verify purchase with the configured providers
|
|
748
|
+
*
|
|
749
|
+
* This function uses the native OpenIAP verifyPurchase implementation
|
|
750
|
+
* which validates purchases using platform-specific methods.
|
|
751
|
+
*
|
|
752
|
+
* @param options - Receipt validation options containing the SKU
|
|
753
|
+
* @returns Promise resolving to receipt validation result
|
|
754
|
+
*/
|
|
755
|
+
export const verifyPurchase: MutationField<'verifyPurchase'> = async (
|
|
756
|
+
options,
|
|
757
|
+
) => {
|
|
758
|
+
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
759
|
+
return ExpoIapModule.verifyPurchase(options);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
throw new Error(`Unsupported platform: ${Platform.OS}`);
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Verify purchase with a specific provider (e.g., IAPKit)
|
|
767
|
+
*
|
|
768
|
+
* This function allows you to verify purchases using external verification
|
|
769
|
+
* services like IAPKit, which provide additional validation and security.
|
|
770
|
+
*
|
|
771
|
+
* @param options - Verification options including provider and credentials
|
|
772
|
+
* @returns Promise resolving to provider-specific verification result
|
|
773
|
+
*
|
|
774
|
+
* @example
|
|
775
|
+
* ```typescript
|
|
776
|
+
* const result = await verifyPurchaseWithProvider({
|
|
777
|
+
* provider: 'iapkit',
|
|
778
|
+
* iapkit: {
|
|
779
|
+
* apiKey: 'your-api-key',
|
|
780
|
+
* apple: {
|
|
781
|
+
* jws: purchase.purchaseToken // JWS from purchase
|
|
782
|
+
* },
|
|
783
|
+
* google: {
|
|
784
|
+
* purchaseToken: purchase.purchaseToken
|
|
785
|
+
* }
|
|
786
|
+
* }
|
|
787
|
+
* });
|
|
788
|
+
* ```
|
|
789
|
+
*/
|
|
790
|
+
export const verifyPurchaseWithProvider: MutationField<
|
|
791
|
+
'verifyPurchaseWithProvider'
|
|
792
|
+
> = async (options) => {
|
|
793
|
+
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
794
|
+
return ExpoIapModule.verifyPurchaseWithProvider(options);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
throw new Error(`Unsupported platform: ${Platform.OS}`);
|
|
798
|
+
};
|
|
799
|
+
|
|
744
800
|
export * from './useIAP';
|
|
745
801
|
export {
|
|
746
802
|
ErrorCodeUtils,
|
package/src/modules/android.ts
CHANGED
|
@@ -8,7 +8,7 @@ import ExpoIapModule from '../ExpoIapModule';
|
|
|
8
8
|
import type {
|
|
9
9
|
DeepLinkOptions,
|
|
10
10
|
MutationField,
|
|
11
|
-
|
|
11
|
+
VerifyPurchaseResultAndroid,
|
|
12
12
|
} from '../types';
|
|
13
13
|
|
|
14
14
|
type NativeAndroidModule = {
|
|
@@ -80,6 +80,8 @@ export const deepLinkToSubscriptionsAndroid = async (
|
|
|
80
80
|
* Validate receipt for Android. NOTE: This method is here for debugging purposes only. Including
|
|
81
81
|
* your access token in the binary you ship to users is potentially dangerous.
|
|
82
82
|
* Use server side validation instead for your production builds
|
|
83
|
+
*
|
|
84
|
+
* @deprecated Use verifyPurchase instead
|
|
83
85
|
* @param {Object} params - The parameters object
|
|
84
86
|
* @param {string} params.packageName - package name of your app.
|
|
85
87
|
* @param {string} params.productId - product id for your in app product.
|
|
@@ -100,7 +102,7 @@ export const validateReceiptAndroid = async ({
|
|
|
100
102
|
productToken: string;
|
|
101
103
|
accessToken: string;
|
|
102
104
|
isSub?: boolean;
|
|
103
|
-
}): Promise<
|
|
105
|
+
}): Promise<VerifyPurchaseResultAndroid> => {
|
|
104
106
|
const type = isSub ? 'subscriptions' : 'products';
|
|
105
107
|
|
|
106
108
|
const url =
|
package/src/modules/ios.ts
CHANGED
|
@@ -13,8 +13,8 @@ import type {
|
|
|
13
13
|
Purchase,
|
|
14
14
|
PurchaseIOS,
|
|
15
15
|
QueryField,
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
VerifyPurchaseProps,
|
|
17
|
+
VerifyPurchaseResultIOS,
|
|
18
18
|
SubscriptionStatusIOS,
|
|
19
19
|
} from '../types';
|
|
20
20
|
import type {PurchaseError} from '../utils/errorMapping';
|
|
@@ -226,7 +226,8 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
|
|
|
226
226
|
* NOTE: For proper security, Apple recommends verifying receipts on your server using
|
|
227
227
|
* the verifyReceipt endpoint rather than relying solely on client-side verification.
|
|
228
228
|
*
|
|
229
|
-
* @
|
|
229
|
+
* @deprecated Use verifyPurchase instead
|
|
230
|
+
* @param props The product's SKU or verification props
|
|
230
231
|
* @returns {Promise<{
|
|
231
232
|
* isValid: boolean;
|
|
232
233
|
* receiptData: string;
|
|
@@ -234,11 +235,9 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
|
|
|
234
235
|
* latestTransaction?: Purchase;
|
|
235
236
|
* }>}
|
|
236
237
|
*/
|
|
237
|
-
const validateReceiptIOSImpl = async (
|
|
238
|
-
props: ReceiptValidationProps | string,
|
|
239
|
-
) => {
|
|
238
|
+
const validateReceiptIOSImpl = async (props: VerifyPurchaseProps | string) => {
|
|
240
239
|
const sku =
|
|
241
|
-
typeof props === 'string' ? props : (props as
|
|
240
|
+
typeof props === 'string' ? props : (props as VerifyPurchaseProps)?.sku;
|
|
242
241
|
|
|
243
242
|
if (!sku) {
|
|
244
243
|
throw new Error('validateReceiptIOS requires a SKU');
|
|
@@ -246,7 +245,7 @@ const validateReceiptIOSImpl = async (
|
|
|
246
245
|
|
|
247
246
|
return (await ExpoIapModule.validateReceiptIOS(
|
|
248
247
|
sku,
|
|
249
|
-
)) as
|
|
248
|
+
)) as VerifyPurchaseResultIOS;
|
|
250
249
|
};
|
|
251
250
|
|
|
252
251
|
export const validateReceiptIOS =
|
package/src/types.ts
CHANGED
|
@@ -28,6 +28,11 @@ export interface ActiveSubscription {
|
|
|
28
28
|
renewalInfoIOS?: (RenewalInfoIOS | null);
|
|
29
29
|
transactionDate: number;
|
|
30
30
|
transactionId: string;
|
|
31
|
+
/**
|
|
32
|
+
* @deprecated iOS only - use daysUntilExpirationIOS instead.
|
|
33
|
+
* Whether the subscription will expire soon (within 7 days).
|
|
34
|
+
* Consider using daysUntilExpirationIOS for more precise control.
|
|
35
|
+
*/
|
|
31
36
|
willExpireSoon?: (boolean | null);
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -131,6 +136,9 @@ export enum ErrorCode {
|
|
|
131
136
|
NotPrepared = 'not-prepared',
|
|
132
137
|
Pending = 'pending',
|
|
133
138
|
PurchaseError = 'purchase-error',
|
|
139
|
+
PurchaseVerificationFailed = 'purchase-verification-failed',
|
|
140
|
+
PurchaseVerificationFinishFailed = 'purchase-verification-finish-failed',
|
|
141
|
+
PurchaseVerificationFinished = 'purchase-verification-finished',
|
|
134
142
|
QueryProduct = 'query-product',
|
|
135
143
|
ReceiptFailed = 'receipt-failed',
|
|
136
144
|
ReceiptFinished = 'receipt-finished',
|
|
@@ -172,6 +180,11 @@ export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product
|
|
|
172
180
|
|
|
173
181
|
export type IapPlatform = 'ios' | 'android';
|
|
174
182
|
|
|
183
|
+
/** Unified purchase states from IAPKit verification response. */
|
|
184
|
+
export type IapkitPurchaseState = 'entitled' | 'pending-acknowledgment' | 'pending' | 'canceled' | 'expired' | 'ready-to-consume' | 'consumed' | 'unknown' | 'inauthentic';
|
|
185
|
+
|
|
186
|
+
export type IapkitStore = 'apple' | 'google';
|
|
187
|
+
|
|
175
188
|
/** Connection initialization configuration */
|
|
176
189
|
export interface InitConnectionConfig {
|
|
177
190
|
/**
|
|
@@ -241,8 +254,15 @@ export interface Mutation {
|
|
|
241
254
|
showManageSubscriptionsIOS: Promise<PurchaseIOS[]>;
|
|
242
255
|
/** Force a StoreKit sync for transactions (iOS 15+) */
|
|
243
256
|
syncIOS: Promise<boolean>;
|
|
244
|
-
/**
|
|
245
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Validate purchase receipts with the configured providers
|
|
259
|
+
* @deprecated Use verifyPurchase
|
|
260
|
+
*/
|
|
261
|
+
validateReceipt: Promise<VerifyPurchaseResult>;
|
|
262
|
+
/** Verify purchases with the configured providers */
|
|
263
|
+
verifyPurchase: Promise<VerifyPurchaseResult>;
|
|
264
|
+
/** Verify purchases with a specific provider (e.g., IAPKit) */
|
|
265
|
+
verifyPurchaseWithProvider: Promise<VerifyPurchaseWithProviderResult>;
|
|
246
266
|
}
|
|
247
267
|
|
|
248
268
|
|
|
@@ -282,7 +302,11 @@ export type MutationRequestPurchaseArgs =
|
|
|
282
302
|
};
|
|
283
303
|
|
|
284
304
|
|
|
285
|
-
export type MutationValidateReceiptArgs =
|
|
305
|
+
export type MutationValidateReceiptArgs = VerifyPurchaseProps;
|
|
306
|
+
|
|
307
|
+
export type MutationVerifyPurchaseArgs = VerifyPurchaseProps;
|
|
308
|
+
|
|
309
|
+
export type MutationVerifyPurchaseWithProviderArgs = VerifyPurchaseWithProviderProps;
|
|
286
310
|
|
|
287
311
|
export type PaymentModeIOS = 'empty' | 'free-trial' | 'pay-as-you-go' | 'pay-up-front';
|
|
288
312
|
|
|
@@ -330,11 +354,11 @@ export interface ProductCommon {
|
|
|
330
354
|
displayName?: (string | null);
|
|
331
355
|
displayPrice: string;
|
|
332
356
|
id: string;
|
|
333
|
-
platform:
|
|
357
|
+
platform: 'android' | 'ios';
|
|
334
358
|
price?: (number | null);
|
|
335
359
|
title: string;
|
|
336
|
-
type:
|
|
337
|
-
}
|
|
360
|
+
type: 'in-app' | 'subs';
|
|
361
|
+
}
|
|
338
362
|
|
|
339
363
|
export interface ProductIOS extends ProductCommon {
|
|
340
364
|
currency: string;
|
|
@@ -522,6 +546,8 @@ export interface PurchaseOptions {
|
|
|
522
546
|
|
|
523
547
|
export type PurchaseState = 'pending' | 'purchased' | 'failed' | 'restored' | 'deferred' | 'unknown';
|
|
524
548
|
|
|
549
|
+
export type PurchaseVerificationProvider = 'iapkit';
|
|
550
|
+
|
|
525
551
|
export interface Query {
|
|
526
552
|
/** Check if external purchase notice sheet can be presented (iOS 18.2+) */
|
|
527
553
|
canPresentExternalPurchaseNoticeIOS: Promise<boolean>;
|
|
@@ -560,8 +586,11 @@ export interface Query {
|
|
|
560
586
|
latestTransactionIOS?: Promise<(PurchaseIOS | null)>;
|
|
561
587
|
/** Get StoreKit 2 subscription status details (iOS 15+) */
|
|
562
588
|
subscriptionStatusIOS: Promise<SubscriptionStatusIOS[]>;
|
|
563
|
-
/**
|
|
564
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Validate a receipt for a specific product
|
|
591
|
+
* @deprecated Use verifyPurchase
|
|
592
|
+
*/
|
|
593
|
+
validateReceiptIOS: Promise<VerifyPurchaseResultIOS>;
|
|
565
594
|
}
|
|
566
595
|
|
|
567
596
|
|
|
@@ -586,55 +615,7 @@ export type QueryLatestTransactionIosArgs = string;
|
|
|
586
615
|
|
|
587
616
|
export type QuerySubscriptionStatusIosArgs = string;
|
|
588
617
|
|
|
589
|
-
export type QueryValidateReceiptIosArgs =
|
|
590
|
-
|
|
591
|
-
export interface ReceiptValidationAndroidOptions {
|
|
592
|
-
accessToken: string;
|
|
593
|
-
isSub?: (boolean | null);
|
|
594
|
-
packageName: string;
|
|
595
|
-
productToken: string;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
export interface ReceiptValidationProps {
|
|
599
|
-
/** Android-specific validation options */
|
|
600
|
-
androidOptions?: (ReceiptValidationAndroidOptions | null);
|
|
601
|
-
/** Product SKU to validate */
|
|
602
|
-
sku: string;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
export type ReceiptValidationResult = ReceiptValidationResultAndroid | ReceiptValidationResultIOS;
|
|
606
|
-
|
|
607
|
-
export interface ReceiptValidationResultAndroid {
|
|
608
|
-
autoRenewing: boolean;
|
|
609
|
-
betaProduct: boolean;
|
|
610
|
-
cancelDate?: (number | null);
|
|
611
|
-
cancelReason?: (string | null);
|
|
612
|
-
deferredDate?: (number | null);
|
|
613
|
-
deferredSku?: (string | null);
|
|
614
|
-
freeTrialEndDate: number;
|
|
615
|
-
gracePeriodEndDate: number;
|
|
616
|
-
parentProductId: string;
|
|
617
|
-
productId: string;
|
|
618
|
-
productType: string;
|
|
619
|
-
purchaseDate: number;
|
|
620
|
-
quantity: number;
|
|
621
|
-
receiptId: string;
|
|
622
|
-
renewalDate: number;
|
|
623
|
-
term: string;
|
|
624
|
-
termSku: string;
|
|
625
|
-
testTransaction: boolean;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
export interface ReceiptValidationResultIOS {
|
|
629
|
-
/** Whether the receipt is valid */
|
|
630
|
-
isValid: boolean;
|
|
631
|
-
/** JWS representation */
|
|
632
|
-
jwsRepresentation: string;
|
|
633
|
-
/** Latest transaction if available */
|
|
634
|
-
latestTransaction?: (Purchase | null);
|
|
635
|
-
/** Receipt data string */
|
|
636
|
-
receiptData: string;
|
|
637
|
-
}
|
|
618
|
+
export type QueryValidateReceiptIosArgs = VerifyPurchaseProps;
|
|
638
619
|
|
|
639
620
|
export interface RefundResultIOS {
|
|
640
621
|
message?: (string | null);
|
|
@@ -769,6 +750,33 @@ export interface RequestSubscriptionPropsByPlatforms {
|
|
|
769
750
|
ios?: (RequestSubscriptionIosProps | null);
|
|
770
751
|
}
|
|
771
752
|
|
|
753
|
+
export interface RequestVerifyPurchaseWithIapkitAppleProps {
|
|
754
|
+
/** The JWS token returned with the purchase response. */
|
|
755
|
+
jws: string;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export interface RequestVerifyPurchaseWithIapkitGoogleProps {
|
|
759
|
+
/** The token provided to the user's device when the product or subscription was purchased. */
|
|
760
|
+
purchaseToken: string;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export interface RequestVerifyPurchaseWithIapkitProps {
|
|
764
|
+
/** API key used for the Authorization header (Bearer {apiKey}). */
|
|
765
|
+
apiKey?: (string | null);
|
|
766
|
+
/** Apple verification parameters. */
|
|
767
|
+
apple?: (RequestVerifyPurchaseWithIapkitAppleProps | null);
|
|
768
|
+
/** Google verification parameters. */
|
|
769
|
+
google?: (RequestVerifyPurchaseWithIapkitGoogleProps | null);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export interface RequestVerifyPurchaseWithIapkitResult {
|
|
773
|
+
/** Whether the purchase is valid (not falsified). */
|
|
774
|
+
isValid: boolean;
|
|
775
|
+
/** The current state of the purchase. */
|
|
776
|
+
state: IapkitPurchaseState;
|
|
777
|
+
store: IapkitStore;
|
|
778
|
+
}
|
|
779
|
+
|
|
772
780
|
export interface Subscription {
|
|
773
781
|
/** Fires when the App Store surfaces a promoted product (iOS only) */
|
|
774
782
|
promotedProductIOS: string;
|
|
@@ -826,6 +834,65 @@ export interface UserChoiceBillingDetails {
|
|
|
826
834
|
products: string[];
|
|
827
835
|
}
|
|
828
836
|
|
|
837
|
+
export interface VerifyPurchaseAndroidOptions {
|
|
838
|
+
accessToken: string;
|
|
839
|
+
isSub?: (boolean | null);
|
|
840
|
+
packageName: string;
|
|
841
|
+
productToken: string;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export interface VerifyPurchaseProps {
|
|
845
|
+
/** Android-specific validation options */
|
|
846
|
+
androidOptions?: (VerifyPurchaseAndroidOptions | null);
|
|
847
|
+
/** Product SKU to validate */
|
|
848
|
+
sku: string;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export type VerifyPurchaseResult = VerifyPurchaseResultAndroid | VerifyPurchaseResultIOS;
|
|
852
|
+
|
|
853
|
+
export interface VerifyPurchaseResultAndroid {
|
|
854
|
+
autoRenewing: boolean;
|
|
855
|
+
betaProduct: boolean;
|
|
856
|
+
cancelDate?: (number | null);
|
|
857
|
+
cancelReason?: (string | null);
|
|
858
|
+
deferredDate?: (number | null);
|
|
859
|
+
deferredSku?: (string | null);
|
|
860
|
+
freeTrialEndDate: number;
|
|
861
|
+
gracePeriodEndDate: number;
|
|
862
|
+
parentProductId: string;
|
|
863
|
+
productId: string;
|
|
864
|
+
productType: string;
|
|
865
|
+
purchaseDate: number;
|
|
866
|
+
quantity: number;
|
|
867
|
+
receiptId: string;
|
|
868
|
+
renewalDate: number;
|
|
869
|
+
term: string;
|
|
870
|
+
termSku: string;
|
|
871
|
+
testTransaction: boolean;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export interface VerifyPurchaseResultIOS {
|
|
875
|
+
/** Whether the receipt is valid */
|
|
876
|
+
isValid: boolean;
|
|
877
|
+
/** JWS representation */
|
|
878
|
+
jwsRepresentation: string;
|
|
879
|
+
/** Latest transaction if available */
|
|
880
|
+
latestTransaction?: (Purchase | null);
|
|
881
|
+
/** Receipt data string */
|
|
882
|
+
receiptData: string;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export interface VerifyPurchaseWithProviderProps {
|
|
886
|
+
iapkit?: (RequestVerifyPurchaseWithIapkitProps | null);
|
|
887
|
+
provider: PurchaseVerificationProvider;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export interface VerifyPurchaseWithProviderResult {
|
|
891
|
+
/** IAPKit verification results (can include Apple and Google entries) */
|
|
892
|
+
iapkit: RequestVerifyPurchaseWithIapkitResult[];
|
|
893
|
+
provider: PurchaseVerificationProvider;
|
|
894
|
+
}
|
|
895
|
+
|
|
829
896
|
export type VoidResult = void;
|
|
830
897
|
|
|
831
898
|
// -- Query helper types (auto-generated)
|
|
@@ -884,6 +951,8 @@ export type MutationArgsMap = {
|
|
|
884
951
|
showManageSubscriptionsIOS: never;
|
|
885
952
|
syncIOS: never;
|
|
886
953
|
validateReceipt: MutationValidateReceiptArgs;
|
|
954
|
+
verifyPurchase: MutationVerifyPurchaseArgs;
|
|
955
|
+
verifyPurchaseWithProvider: MutationVerifyPurchaseWithProviderArgs;
|
|
887
956
|
};
|
|
888
957
|
|
|
889
958
|
export type MutationField<K extends keyof Mutation> =
|
package/src/useIAP.ts
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
requestPurchase as requestPurchaseInternal,
|
|
16
16
|
fetchProducts,
|
|
17
17
|
validateReceipt as validateReceiptInternal,
|
|
18
|
+
verifyPurchase as verifyPurchaseInternal,
|
|
19
|
+
verifyPurchaseWithProvider as verifyPurchaseWithProviderInternal,
|
|
18
20
|
getActiveSubscriptions,
|
|
19
21
|
hasActiveSubscriptions,
|
|
20
22
|
type ActiveSubscription,
|
|
@@ -41,8 +43,10 @@ import type {
|
|
|
41
43
|
Purchase,
|
|
42
44
|
MutationRequestPurchaseArgs,
|
|
43
45
|
PurchaseInput,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
VerifyPurchaseProps,
|
|
47
|
+
VerifyPurchaseResult,
|
|
48
|
+
VerifyPurchaseWithProviderProps,
|
|
49
|
+
VerifyPurchaseWithProviderResult,
|
|
46
50
|
ProductAndroid,
|
|
47
51
|
ProductSubscriptionIOS,
|
|
48
52
|
} from './types';
|
|
@@ -77,9 +81,14 @@ type UseIap = {
|
|
|
77
81
|
requestPurchase: (
|
|
78
82
|
params: MutationRequestPurchaseArgs,
|
|
79
83
|
) => ReturnType<typeof requestPurchaseInternal>;
|
|
84
|
+
/** @deprecated Use verifyPurchase instead */
|
|
80
85
|
validateReceipt: (
|
|
81
|
-
props:
|
|
82
|
-
) => Promise<
|
|
86
|
+
props: VerifyPurchaseProps,
|
|
87
|
+
) => Promise<VerifyPurchaseResult>;
|
|
88
|
+
verifyPurchase: (props: VerifyPurchaseProps) => Promise<VerifyPurchaseResult>;
|
|
89
|
+
verifyPurchaseWithProvider: (
|
|
90
|
+
props: VerifyPurchaseWithProviderProps,
|
|
91
|
+
) => Promise<VerifyPurchaseWithProviderResult>;
|
|
83
92
|
restorePurchases: () => Promise<void>;
|
|
84
93
|
getPromotedProductIOS: () => Promise<Product | null>;
|
|
85
94
|
requestPurchaseOnPromotedProductIOS: () => Promise<boolean>;
|
|
@@ -383,10 +392,21 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
383
392
|
}
|
|
384
393
|
}, []);
|
|
385
394
|
|
|
386
|
-
const validateReceipt = useCallback(async (props:
|
|
395
|
+
const validateReceipt = useCallback(async (props: VerifyPurchaseProps) => {
|
|
387
396
|
return validateReceiptInternal(props);
|
|
388
397
|
}, []);
|
|
389
398
|
|
|
399
|
+
const verifyPurchase = useCallback(async (props: VerifyPurchaseProps) => {
|
|
400
|
+
return verifyPurchaseInternal(props);
|
|
401
|
+
}, []);
|
|
402
|
+
|
|
403
|
+
const verifyPurchaseWithProvider = useCallback(
|
|
404
|
+
async (props: VerifyPurchaseWithProviderProps) => {
|
|
405
|
+
return verifyPurchaseWithProviderInternal(props);
|
|
406
|
+
},
|
|
407
|
+
[],
|
|
408
|
+
);
|
|
409
|
+
|
|
390
410
|
const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
|
|
391
411
|
// CRITICAL: Register listeners BEFORE initConnection to avoid race condition
|
|
392
412
|
// Events might fire immediately after initConnection, so listeners must be ready
|
|
@@ -481,6 +501,8 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
481
501
|
fetchProducts: fetchProductsInternal,
|
|
482
502
|
requestPurchase: requestPurchaseWithReset,
|
|
483
503
|
validateReceipt,
|
|
504
|
+
verifyPurchase,
|
|
505
|
+
verifyPurchaseWithProvider,
|
|
484
506
|
restorePurchases: restorePurchasesInternal,
|
|
485
507
|
// internal getters kept for hook state management
|
|
486
508
|
getPromotedProductIOS,
|
|
@@ -80,6 +80,11 @@ const COMMON_ERROR_CODE_MAP: Record<ErrorCode, string> = {
|
|
|
80
80
|
[ErrorCode.BillingUnavailable]: ErrorCode.BillingUnavailable,
|
|
81
81
|
[ErrorCode.FeatureNotSupported]: ErrorCode.FeatureNotSupported,
|
|
82
82
|
[ErrorCode.EmptySkuList]: ErrorCode.EmptySkuList,
|
|
83
|
+
[ErrorCode.PurchaseVerificationFailed]: ErrorCode.PurchaseVerificationFailed,
|
|
84
|
+
[ErrorCode.PurchaseVerificationFinishFailed]:
|
|
85
|
+
ErrorCode.PurchaseVerificationFinishFailed,
|
|
86
|
+
[ErrorCode.PurchaseVerificationFinished]:
|
|
87
|
+
ErrorCode.PurchaseVerificationFinished,
|
|
83
88
|
};
|
|
84
89
|
|
|
85
90
|
export const ErrorCodeMapping = {
|