expo-iap 3.0.2-rc.1 → 3.0.3
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/CHANGELOG.md +11 -1
- package/README.md +4 -6
- package/build/ExpoIap.types.d.ts +49 -36
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js +79 -70
- package/build/ExpoIap.types.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +2 -3
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/ExpoIapIOS.types.d.ts +7 -1
- package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
- package/build/types/ExpoIapIOS.types.js +7 -1
- package/build/types/ExpoIapIOS.types.js.map +1 -1
- package/build/useIAP.d.ts +2 -2
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +13 -2
- package/build/useIAP.js.map +1 -1
- package/build/utils/errorMapping.d.ts +10 -4
- package/build/utils/errorMapping.d.ts.map +1 -1
- package/build/utils/errorMapping.js +54 -46
- package/build/utils/errorMapping.js.map +1 -1
- package/bun.lock +340 -137
- package/ios/ExpoIapModule.swift +24 -11
- package/package.json +1 -1
- package/plugin/src/withIAP.ts +5 -1
- package/plugin/src/withLocalOpenIAP.ts +3 -1
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/ExpoIap.types.ts +89 -74
- package/src/index.ts +6 -6
- package/src/types/ExpoIapAndroid.types.ts +2 -5
- package/src/types/ExpoIapIOS.types.ts +8 -3
- package/src/useIAP.ts +21 -10
- package/src/utils/errorMapping.ts +67 -54
- package/coverage/clover.xml +0 -358
- package/coverage/coverage-final.json +0 -7
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -176
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -196
- package/coverage/lcov-report/src/ExpoIap.types.ts.html +0 -1396
- package/coverage/lcov-report/src/helpers/index.html +0 -116
- package/coverage/lcov-report/src/helpers/subscription.ts.html +0 -532
- package/coverage/lcov-report/src/index.html +0 -116
- package/coverage/lcov-report/src/index.ts.html +0 -1945
- package/coverage/lcov-report/src/modules/android.ts.html +0 -496
- package/coverage/lcov-report/src/modules/index.html +0 -131
- package/coverage/lcov-report/src/modules/ios.ts.html +0 -1012
- package/coverage/lcov-report/src/types/ExpoIapAndroid.types.ts.html +0 -502
- package/coverage/lcov-report/src/types/index.html +0 -116
- package/coverage/lcov-report/src/useIAP.ts.html +0 -1654
- package/coverage/lcov-report/src/utils/constants.ts.html +0 -127
- package/coverage/lcov-report/src/utils/errorMapping.ts.html +0 -427
- package/coverage/lcov-report/src/utils/index.html +0 -116
- package/coverage/lcov.info +0 -685
- package/ios/expoiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/expoiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -12,6 +12,19 @@ private func logDebug(_ message: String) {
|
|
|
12
12
|
#endif
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// MARK: - Swift helpers for optional dictionary compaction
|
|
16
|
+
private extension Sequence where Element == [String: Any?] {
|
|
17
|
+
func compactingValues() -> [[String: Any]] {
|
|
18
|
+
return self.map { $0.compactMapValues { $0 } }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private extension Dictionary where Key == String, Value == Any? {
|
|
23
|
+
func compactingValues() -> [String: Any] {
|
|
24
|
+
return self.compactMapValues { $0 }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
// Event names
|
|
16
29
|
struct OpenIapEvent {
|
|
17
30
|
static let PurchaseUpdated = "purchase-updated"
|
|
@@ -145,7 +158,7 @@ public class ExpoIapModule: Module {
|
|
|
145
158
|
logDebug("Product: \(product.id) - \(product.title) - \(product.displayPrice)")
|
|
146
159
|
}
|
|
147
160
|
// Ensure non-optional values for Expo bridge
|
|
148
|
-
return OpenIapSerialization.products(products).
|
|
161
|
+
return OpenIapSerialization.products(products).compactingValues()
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
// MARK: - Purchase Operations
|
|
@@ -233,7 +246,7 @@ public class ExpoIapModule: Module {
|
|
|
233
246
|
)
|
|
234
247
|
}
|
|
235
248
|
let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
|
|
236
|
-
return OpenIapSerialization.purchases(purchases).
|
|
249
|
+
return OpenIapSerialization.purchases(purchases).compactingValues()
|
|
237
250
|
}
|
|
238
251
|
|
|
239
252
|
// Legacy function for backward compatibility
|
|
@@ -246,7 +259,7 @@ public class ExpoIapModule: Module {
|
|
|
246
259
|
onlyIncludeActiveItemsIOS: onlyIncludeActiveItems
|
|
247
260
|
)
|
|
248
261
|
let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
|
|
249
|
-
return OpenIapSerialization.purchases(purchases).
|
|
262
|
+
return OpenIapSerialization.purchases(purchases).compactingValues()
|
|
250
263
|
}
|
|
251
264
|
|
|
252
265
|
AsyncFunction("getPendingTransactionsIOS") { () async throws -> [[String: Any]] in
|
|
@@ -254,7 +267,7 @@ public class ExpoIapModule: Module {
|
|
|
254
267
|
logDebug("getPendingTransactionsIOS called")
|
|
255
268
|
|
|
256
269
|
let pendingTransactions = try await OpenIapModule.shared.getPendingTransactionsIOS()
|
|
257
|
-
return OpenIapSerialization.purchases(pendingTransactions).
|
|
270
|
+
return OpenIapSerialization.purchases(pendingTransactions).compactingValues()
|
|
258
271
|
}
|
|
259
272
|
|
|
260
273
|
AsyncFunction("clearTransactionIOS") { () async throws -> Bool in
|
|
@@ -299,9 +312,9 @@ public class ExpoIapModule: Module {
|
|
|
299
312
|
"jwsRepresentation": result.jwsRepresentation,
|
|
300
313
|
// Populate unified purchaseToken for iOS as alias of JWS
|
|
301
314
|
"purchaseToken": result.jwsRepresentation,
|
|
302
|
-
"latestTransaction": result.latestTransaction.map { OpenIapSerialization.purchase($0).
|
|
315
|
+
"latestTransaction": result.latestTransaction.map { OpenIapSerialization.purchase($0).compactingValues() },
|
|
303
316
|
]
|
|
304
|
-
return dict.
|
|
317
|
+
return dict.compactingValues()
|
|
305
318
|
} catch {
|
|
306
319
|
throw OpenIapError.make(code: OpenIapError.E_RECEIPT_FAILED)
|
|
307
320
|
}
|
|
@@ -321,7 +334,7 @@ public class ExpoIapModule: Module {
|
|
|
321
334
|
logDebug("showManageSubscriptionsIOS called")
|
|
322
335
|
// OpenIAP 1.1.9 returns already-serialized dictionaries here.
|
|
323
336
|
let purchases = try await OpenIapModule.shared.showManageSubscriptionsIOS()
|
|
324
|
-
return purchases.
|
|
337
|
+
return purchases.compactingValues()
|
|
325
338
|
}
|
|
326
339
|
|
|
327
340
|
AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws in
|
|
@@ -350,7 +363,7 @@ public class ExpoIapModule: Module {
|
|
|
350
363
|
// Fetch full product info by SKU to conform to OpenIapProduct
|
|
351
364
|
let request = OpenIapProductRequest(skus: [promoted.productIdentifier], type: .all)
|
|
352
365
|
let products = try await OpenIapModule.shared.fetchProducts(request)
|
|
353
|
-
let serialized = OpenIapSerialization.products(products).
|
|
366
|
+
let serialized = OpenIapSerialization.products(products).compactingValues()
|
|
354
367
|
return serialized.first
|
|
355
368
|
}
|
|
356
369
|
return nil
|
|
@@ -408,7 +421,7 @@ public class ExpoIapModule: Module {
|
|
|
408
421
|
dict["renewalInfo"] = renewalInfo
|
|
409
422
|
}
|
|
410
423
|
|
|
411
|
-
return dict.
|
|
424
|
+
return dict.compactingValues()
|
|
412
425
|
}
|
|
413
426
|
}
|
|
414
427
|
return nil
|
|
@@ -419,7 +432,7 @@ public class ExpoIapModule: Module {
|
|
|
419
432
|
logDebug("currentEntitlementIOS called for sku: \(sku)")
|
|
420
433
|
do {
|
|
421
434
|
if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
|
|
422
|
-
return OpenIapSerialization.purchase(entitlement).
|
|
435
|
+
return OpenIapSerialization.purchase(entitlement).compactingValues()
|
|
423
436
|
}
|
|
424
437
|
return nil
|
|
425
438
|
} catch {
|
|
@@ -432,7 +445,7 @@ public class ExpoIapModule: Module {
|
|
|
432
445
|
logDebug("latestTransactionIOS called for sku: \(sku)")
|
|
433
446
|
do {
|
|
434
447
|
if let transaction = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
|
|
435
|
-
return OpenIapSerialization.purchase(transaction).
|
|
448
|
+
return OpenIapSerialization.purchase(transaction).compactingValues()
|
|
436
449
|
}
|
|
437
450
|
return nil
|
|
438
451
|
} catch {
|
package/package.json
CHANGED
package/plugin/src/withIAP.ts
CHANGED
|
@@ -66,7 +66,11 @@ const modifyAppBuildGradle = (
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Ensure the desired dependency line is present
|
|
69
|
-
if (
|
|
69
|
+
if (
|
|
70
|
+
!new RegExp(
|
|
71
|
+
String.raw`io\.github\.hyochan\.openiap:openiap-google:1\.1\.0`,
|
|
72
|
+
).test(modified)
|
|
73
|
+
) {
|
|
70
74
|
// Insert just after the opening `dependencies {` line
|
|
71
75
|
modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 1);
|
|
72
76
|
logOnce(
|
|
@@ -213,7 +213,9 @@ const withLocalOpenIAP: ConfigPlugin<{localPath?: LocalPathOption} | void> = (
|
|
|
213
213
|
}
|
|
214
214
|
if (removedAny) {
|
|
215
215
|
gradle.contents = contents;
|
|
216
|
-
console.log(
|
|
216
|
+
console.log(
|
|
217
|
+
'🧹 Removed Maven openiap-google to use local :openiap-google',
|
|
218
|
+
);
|
|
217
219
|
}
|
|
218
220
|
if (!gradle.contents.includes(dependencyLine)) {
|
|
219
221
|
const anchor = /dependencies\s*\{/m;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/
|
|
1
|
+
{"root":["./src/withIAP.ts","./src/withLocalOpenIAP.ts"],"version":"5.9.2"}
|
package/src/ExpoIap.types.ts
CHANGED
|
@@ -16,6 +16,15 @@ export type ChangeEventPayload = {
|
|
|
16
16
|
|
|
17
17
|
export type ProductType = 'inapp' | 'subs';
|
|
18
18
|
|
|
19
|
+
export enum PurchaseState {
|
|
20
|
+
Pending = 'pending',
|
|
21
|
+
Purchased = 'purchased',
|
|
22
|
+
Failed = 'failed',
|
|
23
|
+
Restored = 'restored',
|
|
24
|
+
Deferred = 'deferred',
|
|
25
|
+
Unknown = 'unknown',
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
// =============================================================================
|
|
20
29
|
// COMMON TYPES (Base types shared across all platforms)
|
|
21
30
|
// =============================================================================
|
|
@@ -37,10 +46,13 @@ export type PurchaseCommon = {
|
|
|
37
46
|
id: string; // Transaction identifier - used by finishTransaction
|
|
38
47
|
productId: string; // Product identifier - which product was purchased
|
|
39
48
|
ids?: string[]; // Product identifiers for purchases that include multiple products
|
|
49
|
+
transactionId?: string; // Legacy identifier
|
|
40
50
|
transactionDate: number;
|
|
41
|
-
|
|
42
|
-
purchaseToken?: string; // Unified purchase token (jwsRepresentation for iOS, purchaseToken for Android)
|
|
51
|
+
purchaseToken?: string; // Unified token (iOS: JWS, Android: purchaseToken)
|
|
43
52
|
platform?: string;
|
|
53
|
+
quantity?: number;
|
|
54
|
+
purchaseState?: PurchaseState;
|
|
55
|
+
isAutoRenewing?: boolean;
|
|
44
56
|
};
|
|
45
57
|
|
|
46
58
|
export type ProductSubscriptionCommon = ProductCommon & {
|
|
@@ -56,10 +68,13 @@ export type Product =
|
|
|
56
68
|
| (ProductAndroid & AndroidPlatform)
|
|
57
69
|
| (ProductIOS & IosPlatform);
|
|
58
70
|
|
|
59
|
-
export type
|
|
71
|
+
export type ProductSubscription =
|
|
60
72
|
| (ProductSubscriptionAndroid & AndroidPlatform)
|
|
61
73
|
| (ProductSubscriptionIOS & IosPlatform);
|
|
62
74
|
|
|
75
|
+
// Legacy naming retained for backward compatibility
|
|
76
|
+
export type SubscriptionProduct = ProductSubscription;
|
|
77
|
+
|
|
63
78
|
// Re-export all platform-specific types to avoid deep imports
|
|
64
79
|
export * from './types/ExpoIapAndroid.types';
|
|
65
80
|
export * from './types/ExpoIapIOS.types';
|
|
@@ -81,41 +96,41 @@ export type PurchaseResult = {
|
|
|
81
96
|
* These are mapped to platform-specific error codes and provide consistent error handling
|
|
82
97
|
*/
|
|
83
98
|
export enum ErrorCode {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
Unknown = 'E_UNKNOWN',
|
|
100
|
+
UserCancelled = 'E_USER_CANCELLED',
|
|
101
|
+
UserError = 'E_USER_ERROR',
|
|
102
|
+
ItemUnavailable = 'E_ITEM_UNAVAILABLE',
|
|
103
|
+
RemoteError = 'E_REMOTE_ERROR',
|
|
104
|
+
NetworkError = 'E_NETWORK_ERROR',
|
|
105
|
+
ServiceError = 'E_SERVICE_ERROR',
|
|
106
|
+
ReceiptFailed = 'E_RECEIPT_FAILED',
|
|
107
|
+
ReceiptFinished = 'E_RECEIPT_FINISHED',
|
|
108
|
+
ReceiptFinishedFailed = 'E_RECEIPT_FINISHED_FAILED',
|
|
109
|
+
NotPrepared = 'E_NOT_PREPARED',
|
|
110
|
+
NotEnded = 'E_NOT_ENDED',
|
|
111
|
+
AlreadyOwned = 'E_ALREADY_OWNED',
|
|
112
|
+
DeveloperError = 'E_DEVELOPER_ERROR',
|
|
113
|
+
BillingResponseJsonParseError = 'E_BILLING_RESPONSE_JSON_PARSE_ERROR',
|
|
114
|
+
DeferredPayment = 'E_DEFERRED_PAYMENT',
|
|
115
|
+
Interrupted = 'E_INTERRUPTED',
|
|
116
|
+
IapNotAvailable = 'E_IAP_NOT_AVAILABLE',
|
|
117
|
+
PurchaseError = 'E_PURCHASE_ERROR',
|
|
118
|
+
SyncError = 'E_SYNC_ERROR',
|
|
119
|
+
TransactionValidationFailed = 'E_TRANSACTION_VALIDATION_FAILED',
|
|
120
|
+
ActivityUnavailable = 'E_ACTIVITY_UNAVAILABLE',
|
|
121
|
+
AlreadyPrepared = 'E_ALREADY_PREPARED',
|
|
122
|
+
Pending = 'E_PENDING',
|
|
123
|
+
ConnectionClosed = 'E_CONNECTION_CLOSED',
|
|
109
124
|
// Additional detailed errors (Android-focused, kept cross-platform)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
InitConnection = 'E_INIT_CONNECTION',
|
|
126
|
+
ServiceDisconnected = 'E_SERVICE_DISCONNECTED',
|
|
127
|
+
QueryProduct = 'E_QUERY_PRODUCT',
|
|
128
|
+
SkuNotFound = 'E_SKU_NOT_FOUND',
|
|
129
|
+
SkuOfferMismatch = 'E_SKU_OFFER_MISMATCH',
|
|
130
|
+
ItemNotOwned = 'E_ITEM_NOT_OWNED',
|
|
131
|
+
BillingUnavailable = 'E_BILLING_UNAVAILABLE',
|
|
132
|
+
FeatureNotSupported = 'E_FEATURE_NOT_SUPPORTED',
|
|
133
|
+
EmptySkuList = 'E_EMPTY_SKU_LIST',
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
// Fast lookup set for validating standardized error code strings
|
|
@@ -129,42 +144,41 @@ const OPENIAP_ERROR_CODE_SET: Set<string> = new Set(
|
|
|
129
144
|
*/
|
|
130
145
|
// Shared OpenIAP string code mapping for both platforms
|
|
131
146
|
const COMMON_ERROR_CODE_MAP = {
|
|
132
|
-
[ErrorCode.
|
|
133
|
-
[ErrorCode.
|
|
134
|
-
[ErrorCode.
|
|
135
|
-
[ErrorCode.
|
|
136
|
-
[ErrorCode.
|
|
137
|
-
[ErrorCode.
|
|
138
|
-
[ErrorCode.
|
|
139
|
-
[ErrorCode.
|
|
140
|
-
[ErrorCode.
|
|
141
|
-
[ErrorCode.
|
|
142
|
-
[ErrorCode.
|
|
143
|
-
[ErrorCode.
|
|
144
|
-
[ErrorCode.
|
|
145
|
-
[ErrorCode.
|
|
146
|
-
[ErrorCode.
|
|
147
|
+
[ErrorCode.Unknown]: 'E_UNKNOWN',
|
|
148
|
+
[ErrorCode.UserCancelled]: 'E_USER_CANCELLED',
|
|
149
|
+
[ErrorCode.UserError]: 'E_USER_ERROR',
|
|
150
|
+
[ErrorCode.ItemUnavailable]: 'E_ITEM_UNAVAILABLE',
|
|
151
|
+
[ErrorCode.RemoteError]: 'E_REMOTE_ERROR',
|
|
152
|
+
[ErrorCode.NetworkError]: 'E_NETWORK_ERROR',
|
|
153
|
+
[ErrorCode.ServiceError]: 'E_SERVICE_ERROR',
|
|
154
|
+
[ErrorCode.ReceiptFailed]: 'E_RECEIPT_FAILED',
|
|
155
|
+
[ErrorCode.ReceiptFinished]: 'E_RECEIPT_FINISHED',
|
|
156
|
+
[ErrorCode.ReceiptFinishedFailed]: 'E_RECEIPT_FINISHED_FAILED',
|
|
157
|
+
[ErrorCode.NotPrepared]: 'E_NOT_PREPARED',
|
|
158
|
+
[ErrorCode.NotEnded]: 'E_NOT_ENDED',
|
|
159
|
+
[ErrorCode.AlreadyOwned]: 'E_ALREADY_OWNED',
|
|
160
|
+
[ErrorCode.DeveloperError]: 'E_DEVELOPER_ERROR',
|
|
161
|
+
[ErrorCode.BillingResponseJsonParseError]:
|
|
147
162
|
'E_BILLING_RESPONSE_JSON_PARSE_ERROR',
|
|
148
|
-
[ErrorCode.
|
|
149
|
-
[ErrorCode.
|
|
150
|
-
[ErrorCode.
|
|
151
|
-
[ErrorCode.
|
|
152
|
-
[ErrorCode.
|
|
153
|
-
[ErrorCode.
|
|
154
|
-
|
|
155
|
-
[ErrorCode.
|
|
156
|
-
[ErrorCode.
|
|
157
|
-
[ErrorCode.
|
|
158
|
-
[ErrorCode.
|
|
159
|
-
[ErrorCode.
|
|
160
|
-
[ErrorCode.
|
|
161
|
-
[ErrorCode.
|
|
162
|
-
[ErrorCode.
|
|
163
|
-
[ErrorCode.
|
|
164
|
-
[ErrorCode.
|
|
165
|
-
[ErrorCode.
|
|
166
|
-
[ErrorCode.
|
|
167
|
-
[ErrorCode.E_EMPTY_SKU_LIST]: 'E_EMPTY_SKU_LIST',
|
|
163
|
+
[ErrorCode.DeferredPayment]: 'E_DEFERRED_PAYMENT',
|
|
164
|
+
[ErrorCode.Interrupted]: 'E_INTERRUPTED',
|
|
165
|
+
[ErrorCode.IapNotAvailable]: 'E_IAP_NOT_AVAILABLE',
|
|
166
|
+
[ErrorCode.PurchaseError]: 'E_PURCHASE_ERROR',
|
|
167
|
+
[ErrorCode.SyncError]: 'E_SYNC_ERROR',
|
|
168
|
+
[ErrorCode.TransactionValidationFailed]: 'E_TRANSACTION_VALIDATION_FAILED',
|
|
169
|
+
[ErrorCode.ActivityUnavailable]: 'E_ACTIVITY_UNAVAILABLE',
|
|
170
|
+
[ErrorCode.AlreadyPrepared]: 'E_ALREADY_PREPARED',
|
|
171
|
+
[ErrorCode.Pending]: 'E_PENDING',
|
|
172
|
+
[ErrorCode.ConnectionClosed]: 'E_CONNECTION_CLOSED',
|
|
173
|
+
[ErrorCode.InitConnection]: 'E_INIT_CONNECTION',
|
|
174
|
+
[ErrorCode.ServiceDisconnected]: 'E_SERVICE_DISCONNECTED',
|
|
175
|
+
[ErrorCode.QueryProduct]: 'E_QUERY_PRODUCT',
|
|
176
|
+
[ErrorCode.SkuNotFound]: 'E_SKU_NOT_FOUND',
|
|
177
|
+
[ErrorCode.SkuOfferMismatch]: 'E_SKU_OFFER_MISMATCH',
|
|
178
|
+
[ErrorCode.ItemNotOwned]: 'E_ITEM_NOT_OWNED',
|
|
179
|
+
[ErrorCode.BillingUnavailable]: 'E_BILLING_UNAVAILABLE',
|
|
180
|
+
[ErrorCode.FeatureNotSupported]: 'E_FEATURE_NOT_SUPPORTED',
|
|
181
|
+
[ErrorCode.EmptySkuList]: 'E_EMPTY_SKU_LIST',
|
|
168
182
|
} as const;
|
|
169
183
|
|
|
170
184
|
export const ErrorCodeMapping = {
|
|
@@ -229,7 +243,7 @@ export class PurchaseError implements Error {
|
|
|
229
243
|
): PurchaseError {
|
|
230
244
|
const errorCode = errorData.code
|
|
231
245
|
? ErrorCodeUtils.fromPlatformCode(errorData.code, platform)
|
|
232
|
-
: ErrorCode.
|
|
246
|
+
: ErrorCode.Unknown;
|
|
233
247
|
|
|
234
248
|
return new PurchaseError({
|
|
235
249
|
message: errorData.message || 'Unknown error occurred',
|
|
@@ -299,7 +313,7 @@ export const ErrorCodeUtils = {
|
|
|
299
313
|
}
|
|
300
314
|
}
|
|
301
315
|
|
|
302
|
-
return ErrorCode.
|
|
316
|
+
return ErrorCode.Unknown;
|
|
303
317
|
},
|
|
304
318
|
|
|
305
319
|
/**
|
|
@@ -392,6 +406,7 @@ export interface RequestPurchaseAndroidProps {
|
|
|
392
406
|
*/
|
|
393
407
|
export interface RequestSubscriptionAndroidProps
|
|
394
408
|
extends RequestPurchaseAndroidProps {
|
|
409
|
+
readonly purchaseTokenAndroid?: string;
|
|
395
410
|
readonly replacementModeAndroid?: number;
|
|
396
411
|
readonly subscriptionOffers: {
|
|
397
412
|
sku: string;
|
package/src/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
PurchaseResult,
|
|
26
26
|
RequestSubscriptionProps,
|
|
27
27
|
RequestPurchaseProps,
|
|
28
|
-
|
|
28
|
+
ProductSubscription,
|
|
29
29
|
// Bring platform types from the barrel to avoid deep imports
|
|
30
30
|
PurchaseAndroid,
|
|
31
31
|
PaymentDiscount,
|
|
@@ -169,11 +169,11 @@ export const fetchProducts = async ({
|
|
|
169
169
|
}: {
|
|
170
170
|
skus: string[];
|
|
171
171
|
type?: 'inapp' | 'subs';
|
|
172
|
-
}): Promise<Product[] |
|
|
172
|
+
}): Promise<Product[] | ProductSubscription[]> => {
|
|
173
173
|
if (!skus?.length) {
|
|
174
174
|
throw new PurchaseError({
|
|
175
175
|
message: 'No SKUs provided',
|
|
176
|
-
code: ErrorCode.
|
|
176
|
+
code: ErrorCode.EmptySkuList,
|
|
177
177
|
});
|
|
178
178
|
}
|
|
179
179
|
|
|
@@ -195,7 +195,7 @@ export const fetchProducts = async ({
|
|
|
195
195
|
|
|
196
196
|
return type === 'inapp'
|
|
197
197
|
? (filteredItems as Product[])
|
|
198
|
-
: (filteredItems as
|
|
198
|
+
: (filteredItems as ProductSubscription[]);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
if (Platform.OS === 'android') {
|
|
@@ -213,7 +213,7 @@ export const fetchProducts = async ({
|
|
|
213
213
|
|
|
214
214
|
return type === 'inapp'
|
|
215
215
|
? (filteredItems as Product[])
|
|
216
|
-
: (filteredItems as
|
|
216
|
+
: (filteredItems as ProductSubscription[]);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
throw new Error('Unsupported platform');
|
|
@@ -466,7 +466,7 @@ export const finishTransaction = ({
|
|
|
466
466
|
return Promise.reject(
|
|
467
467
|
new PurchaseError({
|
|
468
468
|
message: 'Purchase token is required to finish transaction',
|
|
469
|
-
code:
|
|
469
|
+
code: ErrorCode.DeveloperError,
|
|
470
470
|
productId: androidPurchase.productId,
|
|
471
471
|
platform: 'android',
|
|
472
472
|
}),
|
|
@@ -22,7 +22,7 @@ type PricingPhasesAndroid = {
|
|
|
22
22
|
|
|
23
23
|
type ProductSubscriptionAndroidOfferDetail = {
|
|
24
24
|
basePlanId: string;
|
|
25
|
-
offerId
|
|
25
|
+
offerId?: string | null;
|
|
26
26
|
offerToken: string;
|
|
27
27
|
offerTags: string[];
|
|
28
28
|
pricingPhases: PricingPhasesAndroid;
|
|
@@ -37,7 +37,7 @@ export type ProductAndroid = ProductCommon & {
|
|
|
37
37
|
|
|
38
38
|
type ProductSubscriptionAndroidOfferDetails = {
|
|
39
39
|
basePlanId: string;
|
|
40
|
-
offerId
|
|
40
|
+
offerId?: string | null;
|
|
41
41
|
offerToken: string;
|
|
42
42
|
pricingPhases: PricingPhasesAndroid;
|
|
43
43
|
offerTags: string[];
|
|
@@ -47,9 +47,6 @@ export type ProductSubscriptionAndroid = ProductAndroid & {
|
|
|
47
47
|
subscriptionOfferDetailsAndroid: ProductSubscriptionAndroidOfferDetails[];
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
// Legacy naming for backward compatibility
|
|
51
|
-
export type SubscriptionProductAndroid = ProductSubscriptionAndroid;
|
|
52
|
-
|
|
53
50
|
export type RequestPurchaseAndroidProps = {
|
|
54
51
|
skus: string[];
|
|
55
52
|
obfuscatedAccountIdAndroid?: string;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import {PurchaseCommon, ProductCommon} from '../ExpoIap.types';
|
|
2
2
|
|
|
3
|
+
export enum ProductTypeIOS {
|
|
4
|
+
Consumable = 'consumable',
|
|
5
|
+
NonConsumable = 'nonConsumable',
|
|
6
|
+
AutoRenewableSubscription = 'autoRenewableSubscription',
|
|
7
|
+
NonRenewingSubscription = 'nonRenewingSubscription',
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
type SubscriptionIosPeriod = 'DAY' | 'WEEK' | 'MONTH' | 'YEAR' | '';
|
|
4
11
|
type PaymentMode = '' | 'FREETRIAL' | 'PAYASYOUGO' | 'PAYUPFRONT';
|
|
5
12
|
|
|
@@ -31,6 +38,7 @@ export type ProductIOS = ProductCommon & {
|
|
|
31
38
|
isFamilyShareableIOS: boolean;
|
|
32
39
|
jsonRepresentationIOS: string;
|
|
33
40
|
platform: 'ios';
|
|
41
|
+
typeIOS: ProductTypeIOS;
|
|
34
42
|
subscriptionInfoIOS?: SubscriptionInfo;
|
|
35
43
|
introductoryPriceNumberOfPeriodsIOS?: string;
|
|
36
44
|
introductoryPriceSubscriptionPeriodIOS?: SubscriptionIosPeriod;
|
|
@@ -58,9 +66,6 @@ export type ProductSubscriptionIOS = ProductIOS & {
|
|
|
58
66
|
subscriptionPeriodUnitIOS?: SubscriptionIosPeriod;
|
|
59
67
|
};
|
|
60
68
|
|
|
61
|
-
// Legacy naming for backward compatibility
|
|
62
|
-
export type SubscriptionProductIOS = ProductSubscriptionIOS;
|
|
63
|
-
|
|
64
69
|
export type PaymentDiscount = {
|
|
65
70
|
/**
|
|
66
71
|
* A string used to uniquely identify a discount offer for a product.
|
package/src/useIAP.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
Purchase,
|
|
32
32
|
PurchaseError,
|
|
33
33
|
PurchaseResult,
|
|
34
|
-
|
|
34
|
+
ProductSubscription,
|
|
35
35
|
RequestPurchaseProps,
|
|
36
36
|
RequestSubscriptionProps,
|
|
37
37
|
ErrorCode,
|
|
@@ -42,12 +42,16 @@ import {
|
|
|
42
42
|
isRecoverableError,
|
|
43
43
|
} from './utils/errorMapping';
|
|
44
44
|
|
|
45
|
+
// Deduplicate purchase success events across re-mounts (dev StrictMode, nav returns)
|
|
46
|
+
// Keep minimal in-memory state; safe for subscriptions since renewals use new ids
|
|
47
|
+
const handledPurchaseIds = new Set<string>();
|
|
48
|
+
|
|
45
49
|
type UseIap = {
|
|
46
50
|
connected: boolean;
|
|
47
51
|
products: Product[];
|
|
48
52
|
promotedProductsIOS: Purchase[];
|
|
49
53
|
promotedProductIdIOS?: string;
|
|
50
|
-
subscriptions:
|
|
54
|
+
subscriptions: ProductSubscription[];
|
|
51
55
|
availablePurchases: Purchase[];
|
|
52
56
|
currentPurchase?: Purchase;
|
|
53
57
|
currentPurchaseError?: PurchaseError;
|
|
@@ -104,7 +108,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
104
108
|
const [connected, setConnected] = useState<boolean>(false);
|
|
105
109
|
const [products, setProducts] = useState<Product[]>([]);
|
|
106
110
|
const [promotedProductsIOS] = useState<Purchase[]>([]);
|
|
107
|
-
const [subscriptions, setSubscriptions] = useState<
|
|
111
|
+
const [subscriptions, setSubscriptions] = useState<ProductSubscription[]>([]);
|
|
108
112
|
|
|
109
113
|
const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([]);
|
|
110
114
|
const [currentPurchase, setCurrentPurchase] = useState<Purchase>();
|
|
@@ -155,7 +159,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
155
159
|
promotedProductIOS?: EventSubscription;
|
|
156
160
|
}>({});
|
|
157
161
|
|
|
158
|
-
const subscriptionsRefState = useRef<
|
|
162
|
+
const subscriptionsRefState = useRef<ProductSubscription[]>([]);
|
|
159
163
|
|
|
160
164
|
useEffect(() => {
|
|
161
165
|
subscriptionsRefState.current = subscriptions;
|
|
@@ -176,7 +180,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
176
180
|
setSubscriptions((prevSubscriptions) =>
|
|
177
181
|
mergeWithDuplicateCheck(
|
|
178
182
|
prevSubscriptions,
|
|
179
|
-
result as
|
|
183
|
+
result as ProductSubscription[],
|
|
180
184
|
(subscription) => subscription.id,
|
|
181
185
|
),
|
|
182
186
|
);
|
|
@@ -199,7 +203,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
199
203
|
setSubscriptions((prevSubscriptions) =>
|
|
200
204
|
mergeWithDuplicateCheck(
|
|
201
205
|
prevSubscriptions,
|
|
202
|
-
result as
|
|
206
|
+
result as ProductSubscription[],
|
|
203
207
|
(subscription) => subscription.id,
|
|
204
208
|
),
|
|
205
209
|
);
|
|
@@ -355,6 +359,15 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
355
359
|
subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
|
|
356
360
|
async (purchase: Purchase) => {
|
|
357
361
|
console.log('[useIAP] Purchase success callback triggered:', purchase);
|
|
362
|
+
|
|
363
|
+
// Guard against duplicate emissions for the same transaction
|
|
364
|
+
const dedupeKey = purchase.id;
|
|
365
|
+
if (dedupeKey && handledPurchaseIds.has(dedupeKey)) {
|
|
366
|
+
console.log('[useIAP] Duplicate purchase event ignored:', dedupeKey);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (dedupeKey) handledPurchaseIds.add(dedupeKey);
|
|
370
|
+
|
|
358
371
|
setCurrentPurchaseError(undefined);
|
|
359
372
|
setCurrentPurchase(purchase);
|
|
360
373
|
|
|
@@ -371,10 +384,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
371
384
|
// Register purchase error listener EARLY. Ignore init-related errors until connected.
|
|
372
385
|
subscriptionsRef.current.purchaseError = purchaseErrorListener(
|
|
373
386
|
(error: PurchaseError) => {
|
|
374
|
-
if (
|
|
375
|
-
!connectedRef.current &&
|
|
376
|
-
error.code === ErrorCode.E_INIT_CONNECTION
|
|
377
|
-
) {
|
|
387
|
+
if (!connectedRef.current && error.code === ErrorCode.InitConnection) {
|
|
378
388
|
return; // Ignore initialization error before connected
|
|
379
389
|
}
|
|
380
390
|
const friendly = getUserFriendlyErrorMessage(error);
|
|
@@ -440,6 +450,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
440
450
|
currentSubscriptions.promotedProductIOS?.remove();
|
|
441
451
|
endConnection();
|
|
442
452
|
setConnected(false);
|
|
453
|
+
handledPurchaseIds.clear();
|
|
443
454
|
};
|
|
444
455
|
}, [initIapWithSubscriptions]);
|
|
445
456
|
|