expo-iap 3.0.4 → 3.0.6
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/.eslintignore +1 -1
- package/.eslintrc.js +1 -0
- package/.prettierignore +1 -0
- package/CHANGELOG.md +10 -0
- package/CLAUDE.md +9 -1
- package/CONTRIBUTING.md +10 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +12 -12
- package/android/src/main/java/expo/modules/iap/PromiseUtils.kt +2 -2
- package/build/index.d.ts +28 -14
- package/build/index.d.ts.map +1 -1
- package/build/index.js +36 -15
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +3 -4
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +2 -4
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +2 -3
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +1 -3
- package/build/modules/ios.js.map +1 -1
- package/build/purchase-error.d.ts +8 -10
- package/build/purchase-error.d.ts.map +1 -1
- package/build/purchase-error.js +4 -2
- package/build/purchase-error.js.map +1 -1
- package/build/types.d.ts +159 -204
- package/build/types.d.ts.map +1 -1
- package/build/types.js +1 -59
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +5 -12
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +10 -75
- package/build/useIAP.js.map +1 -1
- package/ios/ExpoIap.podspec +1 -1
- package/ios/ExpoIapModule.swift +103 -89
- package/package.json +2 -1
- package/plugin/build/withIAP.js +4 -5
- package/plugin/src/withIAP.ts +4 -5
- package/scripts/update-types.mjs +61 -0
- package/src/index.ts +77 -29
- package/src/modules/android.ts +5 -7
- package/src/modules/ios.ts +3 -5
- package/src/purchase-error.ts +13 -16
- package/src/types.ts +183 -216
- package/src/useIAP.ts +19 -94
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
|
-
import StoreKit
|
|
3
|
-
import OpenIAP
|
|
4
2
|
import OSLog
|
|
3
|
+
import OpenIAP
|
|
4
|
+
import StoreKit
|
|
5
5
|
|
|
6
6
|
private let iapLogger = Logger(subsystem: "dev.hyo.expo-iap", category: "ExpoIapModule")
|
|
7
7
|
private func logDebug(_ message: String) {
|
|
8
8
|
// Use OSLog/Logger so logs are structured and filterable
|
|
9
9
|
// Suppress debug logs in Release builds
|
|
10
10
|
#if DEBUG
|
|
11
|
-
|
|
11
|
+
iapLogger.debug("\(message, privacy: .public)")
|
|
12
12
|
#endif
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// MARK: - Swift helpers for optional dictionary compaction
|
|
16
|
-
|
|
17
|
-
func compactingValues() -> [[String: Any]] {
|
|
16
|
+
extension Sequence where Element == [String: Any?] {
|
|
17
|
+
fileprivate func compactingValues() -> [[String: Any]] {
|
|
18
18
|
return self.map { $0.compactMapValues { $0 } }
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
func compactingValues() -> [String: Any] {
|
|
22
|
+
extension Dictionary where Key == String, Value == Any? {
|
|
23
|
+
fileprivate func compactingValues() -> [String: Any] {
|
|
24
24
|
return self.compactMapValues { $0 }
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -47,36 +47,36 @@ public class ExpoIapModule: Module {
|
|
|
47
47
|
if let s = sub { OpenIapModule.shared.removeListener(s) }
|
|
48
48
|
sub = nil
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
nonisolated public func definition() -> ModuleDefinition {
|
|
52
52
|
Name("ExpoIap")
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
Constants {
|
|
55
55
|
OpenIapSerialization.errorCodes()
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
Events(
|
|
59
59
|
OpenIapEvent.PurchaseUpdated,
|
|
60
60
|
OpenIapEvent.PurchaseError,
|
|
61
61
|
OpenIapEvent.PromotedProductIOS
|
|
62
62
|
)
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
OnCreate {
|
|
65
65
|
logDebug("Module created")
|
|
66
66
|
Task { @MainActor in
|
|
67
67
|
self.setupStore()
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
OnDestroy {
|
|
72
72
|
logDebug("Module destroyed")
|
|
73
73
|
Task { @MainActor in
|
|
74
74
|
await self.cleanupStore()
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
// MARK: - Connection Management
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
AsyncFunction("initConnection") { () async throws -> Bool in
|
|
81
81
|
logDebug("initConnection called")
|
|
82
82
|
let isConnected = try await OpenIapModule.shared.initConnection()
|
|
@@ -85,26 +85,26 @@ public class ExpoIapModule: Module {
|
|
|
85
85
|
logDebug("Connection initialized: \(isConnected)")
|
|
86
86
|
return isConnected
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
AsyncFunction("endConnection") { () async throws -> Bool in
|
|
90
90
|
logDebug("endConnection called")
|
|
91
91
|
let _ = try await OpenIapModule.shared.endConnection()
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
logDebug("Connection ended")
|
|
94
94
|
await MainActor.run { self.isInitialized = false }
|
|
95
95
|
return true
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
// MARK: - Product Management
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
AsyncFunction("fetchProducts") { (params: [String: Any]) async throws -> [[String: Any]] in
|
|
101
101
|
try await ensureConnection()
|
|
102
102
|
logDebug("fetchProducts raw params: \(params)")
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
// Handle both object format {skus: [...], type: "..."} and array format
|
|
105
105
|
var skus: [String] = []
|
|
106
106
|
var typeString = "all"
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
if let skusArray = params["skus"] as? [String] {
|
|
109
109
|
// Object format: {skus: [...], type: "..."}
|
|
110
110
|
skus = skusArray
|
|
@@ -119,16 +119,16 @@ public class ExpoIapModule: Module {
|
|
|
119
119
|
}
|
|
120
120
|
skus = tempSkus
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
logDebug("fetchProducts parsed - skus: \(skus), type: \(typeString)")
|
|
124
124
|
logDebug("SKUs count: \(skus.count)")
|
|
125
|
-
|
|
125
|
+
|
|
126
126
|
// Validate SKUs
|
|
127
127
|
guard !skus.isEmpty else {
|
|
128
128
|
logDebug("ERROR: Empty SKUs array!")
|
|
129
129
|
throw OpenIapError.emptySkuList()
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
// Convert string to OpenIapRequestProductType enum
|
|
133
133
|
let productType: OpenIapRequestProductType = {
|
|
134
134
|
switch typeString {
|
|
@@ -140,9 +140,9 @@ public class ExpoIapModule: Module {
|
|
|
140
140
|
return .all
|
|
141
141
|
}
|
|
142
142
|
}()
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
logDebug("Converted type to OpenIapRequestProductType: \(productType)")
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
// Build OpenIapProductRequest and fetch via OpenIapModule
|
|
147
147
|
let request = OpenIapProductRequest(skus: skus, type: productType)
|
|
148
148
|
let products = try await OpenIapModule.shared.fetchProducts(request)
|
|
@@ -160,18 +160,20 @@ public class ExpoIapModule: Module {
|
|
|
160
160
|
// Ensure non-optional values for Expo bridge
|
|
161
161
|
return OpenIapSerialization.products(products).compactingValues()
|
|
162
162
|
}
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
// MARK: - Purchase Operations
|
|
165
|
-
|
|
165
|
+
|
|
166
166
|
AsyncFunction("requestPurchase") { (params: [String: Any]) async throws in
|
|
167
167
|
// Extract and validate required fields
|
|
168
168
|
guard let sku = params["sku"] as? String, !sku.isEmpty else {
|
|
169
|
-
throw OpenIapError.make(
|
|
169
|
+
throw OpenIapError.make(
|
|
170
|
+
code: OpenIapError.PurchaseError, message: "Missing required 'sku'")
|
|
170
171
|
}
|
|
171
172
|
try await ensureConnection()
|
|
172
173
|
|
|
173
174
|
// Optional fields
|
|
174
|
-
let andFinish =
|
|
175
|
+
let andFinish =
|
|
176
|
+
(params["andDangerouslyFinishTransactionAutomatically"] as? Bool) ?? false
|
|
175
177
|
let appAccountToken = params["appAccountToken"] as? String
|
|
176
178
|
let quantity: Int? = {
|
|
177
179
|
if let q = params["quantity"] as? Int { return q }
|
|
@@ -188,7 +190,9 @@ public class ExpoIapModule: Module {
|
|
|
188
190
|
let nonce = (offer["nonce"] as? String) ?? ""
|
|
189
191
|
let signature = (offer["signature"] as? String) ?? ""
|
|
190
192
|
let timestamp = (offer["timestamp"] as? String) ?? ""
|
|
191
|
-
if !identifier.isEmpty && !keyIdentifier.isEmpty && !nonce.isEmpty
|
|
193
|
+
if !identifier.isEmpty && !keyIdentifier.isEmpty && !nonce.isEmpty
|
|
194
|
+
&& !signature.isEmpty && !timestamp.isEmpty
|
|
195
|
+
{
|
|
192
196
|
discountOffer = OpenIapDiscountOffer(
|
|
193
197
|
identifier: identifier,
|
|
194
198
|
keyIdentifier: keyIdentifier,
|
|
@@ -201,9 +205,10 @@ public class ExpoIapModule: Module {
|
|
|
201
205
|
|
|
202
206
|
let tokenForLog = appAccountToken ?? "nil"
|
|
203
207
|
let qtyForLog = quantity ?? -1
|
|
204
|
-
logDebug(
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
logDebug(
|
|
209
|
+
"requestPurchase parsed - sku: \(sku), andFinish: \(andFinish), appAccountToken: \(tokenForLog), quantity: \(qtyForLog), hasOffer: \(discountOffer != nil)"
|
|
210
|
+
)
|
|
211
|
+
|
|
207
212
|
// Build purchase request props using OpenIapRequestPurchaseProps
|
|
208
213
|
let requestProps = OpenIapRequestPurchaseProps(
|
|
209
214
|
sku: sku,
|
|
@@ -212,7 +217,7 @@ public class ExpoIapModule: Module {
|
|
|
212
217
|
quantity: quantity,
|
|
213
218
|
withOffer: discountOffer
|
|
214
219
|
)
|
|
215
|
-
|
|
220
|
+
|
|
216
221
|
do {
|
|
217
222
|
_ = try await OpenIapModule.shared.requestPurchase(requestProps)
|
|
218
223
|
logDebug("Purchase request completed successfully")
|
|
@@ -221,23 +226,26 @@ public class ExpoIapModule: Module {
|
|
|
221
226
|
if let openIapError = error as? OpenIapError {
|
|
222
227
|
throw openIapError
|
|
223
228
|
}
|
|
224
|
-
throw OpenIapError.make(
|
|
229
|
+
throw OpenIapError.make(
|
|
230
|
+
code: OpenIapError.PurchaseError, message: error.localizedDescription)
|
|
225
231
|
}
|
|
226
232
|
}
|
|
227
|
-
|
|
233
|
+
|
|
228
234
|
AsyncFunction("finishTransaction") { (transactionId: String) async throws -> Bool in
|
|
229
235
|
try await ensureConnection()
|
|
230
236
|
logDebug("finishTransaction called with id: \(transactionId)")
|
|
231
|
-
let result = try await OpenIapModule.shared.finishTransaction(
|
|
237
|
+
let result = try await OpenIapModule.shared.finishTransaction(
|
|
238
|
+
transactionIdentifier: transactionId)
|
|
232
239
|
return result
|
|
233
240
|
}
|
|
234
|
-
|
|
241
|
+
|
|
235
242
|
// MARK: - Purchase History
|
|
236
|
-
|
|
237
|
-
AsyncFunction("getAvailablePurchases") {
|
|
243
|
+
|
|
244
|
+
AsyncFunction("getAvailablePurchases") {
|
|
245
|
+
(options: [String: Any?]?) async throws -> [[String: Any]] in
|
|
238
246
|
try await ensureConnection()
|
|
239
247
|
logDebug("getAvailablePurchases called")
|
|
240
|
-
|
|
248
|
+
|
|
241
249
|
// Build options and get purchases directly from OpenIapModule
|
|
242
250
|
let purchaseOptions: OpenIapGetAvailablePurchasesProps? = options.map {
|
|
243
251
|
OpenIapGetAvailablePurchasesProps(
|
|
@@ -248,12 +256,14 @@ public class ExpoIapModule: Module {
|
|
|
248
256
|
let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
|
|
249
257
|
return OpenIapSerialization.purchases(purchases).compactingValues()
|
|
250
258
|
}
|
|
251
|
-
|
|
259
|
+
|
|
252
260
|
// Legacy function for backward compatibility
|
|
253
|
-
AsyncFunction("getAvailableItems") {
|
|
261
|
+
AsyncFunction("getAvailableItems") {
|
|
262
|
+
(alsoPublishToEventListener: Bool, onlyIncludeActiveItems: Bool) async throws
|
|
263
|
+
-> [[String: Any]] in
|
|
254
264
|
try await ensureConnection()
|
|
255
265
|
logDebug("getAvailableItems called (legacy)")
|
|
256
|
-
|
|
266
|
+
|
|
257
267
|
let purchaseOptions = OpenIapGetAvailablePurchasesProps(
|
|
258
268
|
alsoPublishToEventListenerIOS: alsoPublishToEventListener,
|
|
259
269
|
onlyIncludeActiveItemsIOS: onlyIncludeActiveItems
|
|
@@ -261,44 +271,44 @@ public class ExpoIapModule: Module {
|
|
|
261
271
|
let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
|
|
262
272
|
return OpenIapSerialization.purchases(purchases).compactingValues()
|
|
263
273
|
}
|
|
264
|
-
|
|
274
|
+
|
|
265
275
|
AsyncFunction("getPendingTransactionsIOS") { () async throws -> [[String: Any]] in
|
|
266
276
|
try await ensureConnection()
|
|
267
277
|
logDebug("getPendingTransactionsIOS called")
|
|
268
|
-
|
|
278
|
+
|
|
269
279
|
let pendingTransactions = try await OpenIapModule.shared.getPendingTransactionsIOS()
|
|
270
280
|
return OpenIapSerialization.purchases(pendingTransactions).compactingValues()
|
|
271
281
|
}
|
|
272
|
-
|
|
282
|
+
|
|
273
283
|
AsyncFunction("clearTransactionIOS") { () async throws -> Bool in
|
|
274
284
|
try await ensureConnection()
|
|
275
285
|
logDebug("clearTransactionIOS called")
|
|
276
286
|
try await OpenIapModule.shared.clearTransactionIOS()
|
|
277
287
|
return true
|
|
278
288
|
}
|
|
279
|
-
|
|
289
|
+
|
|
280
290
|
// MARK: - Receipt & Validation
|
|
281
|
-
|
|
291
|
+
|
|
282
292
|
AsyncFunction("getReceiptIOS") { () async throws -> String in
|
|
283
293
|
try await ensureConnection()
|
|
284
294
|
logDebug("getReceiptIOS called")
|
|
285
295
|
return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
286
296
|
}
|
|
287
|
-
|
|
297
|
+
|
|
288
298
|
// Backward-compatible alias expected by JS layer/tests
|
|
289
299
|
AsyncFunction("getReceiptDataIOS") { () async throws -> String in
|
|
290
300
|
try await ensureConnection()
|
|
291
301
|
logDebug("getReceiptDataIOS called (alias of getReceiptIOS)")
|
|
292
302
|
return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
293
303
|
}
|
|
294
|
-
|
|
304
|
+
|
|
295
305
|
AsyncFunction("requestReceiptRefreshIOS") { () async throws -> String in
|
|
296
306
|
try await ensureConnection()
|
|
297
307
|
logDebug("requestReceiptRefreshIOS called")
|
|
298
308
|
// Receipt refresh is handled automatically by StoreKit 2
|
|
299
309
|
return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
300
310
|
}
|
|
301
|
-
|
|
311
|
+
|
|
302
312
|
AsyncFunction("validateReceiptIOS") { (sku: String) async throws -> [String: Any] in
|
|
303
313
|
try await ensureConnection()
|
|
304
314
|
logDebug("validateReceiptIOS called for sku: \(sku)")
|
|
@@ -312,53 +322,55 @@ public class ExpoIapModule: Module {
|
|
|
312
322
|
"jwsRepresentation": result.jwsRepresentation,
|
|
313
323
|
// Populate unified purchaseToken for iOS as alias of JWS
|
|
314
324
|
"purchaseToken": result.jwsRepresentation,
|
|
315
|
-
"latestTransaction": result.latestTransaction.map {
|
|
325
|
+
"latestTransaction": result.latestTransaction.map {
|
|
326
|
+
OpenIapSerialization.purchase($0).compactingValues()
|
|
327
|
+
},
|
|
316
328
|
]
|
|
317
329
|
return dict.compactingValues()
|
|
318
330
|
} catch {
|
|
319
|
-
throw OpenIapError.make(code: OpenIapError.
|
|
331
|
+
throw OpenIapError.make(code: OpenIapError.ReceiptFailed)
|
|
320
332
|
}
|
|
321
333
|
}
|
|
322
|
-
|
|
334
|
+
|
|
323
335
|
// MARK: - iOS Specific Features
|
|
324
|
-
|
|
336
|
+
|
|
325
337
|
AsyncFunction("presentCodeRedemptionSheetIOS") { () async throws -> Bool in
|
|
326
338
|
try await ensureConnection()
|
|
327
339
|
logDebug("presentCodeRedemptionSheetIOS called")
|
|
328
340
|
let _ = try await OpenIapModule.shared.presentCodeRedemptionSheetIOS()
|
|
329
341
|
return true
|
|
330
342
|
}
|
|
331
|
-
|
|
343
|
+
|
|
332
344
|
AsyncFunction("showManageSubscriptionsIOS") { () async throws -> [[String: Any]] in
|
|
333
345
|
try await ensureConnection()
|
|
334
346
|
logDebug("showManageSubscriptionsIOS called")
|
|
335
|
-
// OpenIAP
|
|
347
|
+
// OpenIAP returns already-serialized dictionaries here.
|
|
336
348
|
let purchases = try await OpenIapModule.shared.showManageSubscriptionsIOS()
|
|
337
349
|
return purchases.compactingValues()
|
|
338
350
|
}
|
|
339
|
-
|
|
351
|
+
|
|
340
352
|
AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws in
|
|
341
353
|
logDebug("deepLinkToSubscriptionsIOS called")
|
|
342
354
|
// Open App Store subscriptions page directly
|
|
343
355
|
if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
|
|
344
356
|
#if canImport(UIKit)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
357
|
+
await MainActor.run {
|
|
358
|
+
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
359
|
+
}
|
|
348
360
|
#endif
|
|
349
361
|
}
|
|
350
362
|
}
|
|
351
|
-
|
|
363
|
+
|
|
352
364
|
AsyncFunction("beginRefundRequestIOS") { (sku: String) async throws -> String? in
|
|
353
365
|
try await ensureConnection()
|
|
354
366
|
logDebug("beginRefundRequestIOS called for sku: \(sku)")
|
|
355
367
|
return try await OpenIapModule.shared.beginRefundRequestIOS(sku: sku)
|
|
356
368
|
}
|
|
357
|
-
|
|
369
|
+
|
|
358
370
|
AsyncFunction("getPromotedProductIOS") { () async throws -> [String: Any]? in
|
|
359
371
|
try await ensureConnection()
|
|
360
372
|
logDebug("getPromotedProductIOS called")
|
|
361
|
-
|
|
373
|
+
|
|
362
374
|
if let promoted = try await OpenIapModule.shared.getPromotedProductIOS() {
|
|
363
375
|
// Fetch full product info by SKU to conform to OpenIapProduct
|
|
364
376
|
let request = OpenIapProductRequest(skus: [promoted.productIdentifier], type: .all)
|
|
@@ -373,37 +385,37 @@ public class ExpoIapModule: Module {
|
|
|
373
385
|
logDebug("getStorefrontIOS called")
|
|
374
386
|
return try await OpenIapModule.shared.getStorefrontIOS()
|
|
375
387
|
}
|
|
376
|
-
|
|
388
|
+
|
|
377
389
|
AsyncFunction("syncIOS") { () async throws -> Bool in
|
|
378
390
|
try await ensureConnection()
|
|
379
391
|
logDebug("syncIOS called")
|
|
380
392
|
return try await OpenIapModule.shared.syncIOS()
|
|
381
393
|
}
|
|
382
|
-
|
|
394
|
+
|
|
383
395
|
// MARK: - Additional iOS Methods
|
|
384
|
-
|
|
396
|
+
|
|
385
397
|
AsyncFunction("isTransactionVerifiedIOS") { (sku: String) async throws -> Bool in
|
|
386
398
|
try await ensureConnection()
|
|
387
399
|
logDebug("isTransactionVerifiedIOS called for sku: \(sku)")
|
|
388
400
|
return await OpenIapModule.shared.isTransactionVerifiedIOS(sku: sku)
|
|
389
401
|
}
|
|
390
|
-
|
|
402
|
+
|
|
391
403
|
AsyncFunction("getTransactionJwsIOS") { (sku: String) async throws -> String? in
|
|
392
404
|
try await ensureConnection()
|
|
393
405
|
logDebug("getTransactionJwsIOS called for sku: \(sku)")
|
|
394
406
|
return try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku)
|
|
395
407
|
}
|
|
396
|
-
|
|
408
|
+
|
|
397
409
|
AsyncFunction("isEligibleForIntroOfferIOS") { (groupID: String) async throws -> Bool in
|
|
398
410
|
try await ensureConnection()
|
|
399
411
|
logDebug("isEligibleForIntroOfferIOS called for groupID: \(groupID)")
|
|
400
412
|
return await OpenIapModule.shared.isEligibleForIntroOfferIOS(groupID: groupID)
|
|
401
413
|
}
|
|
402
|
-
|
|
414
|
+
|
|
403
415
|
AsyncFunction("subscriptionStatusIOS") { (sku: String) async throws -> [[String: Any]]? in
|
|
404
416
|
try await ensureConnection()
|
|
405
417
|
logDebug("subscriptionStatusIOS called for sku: \(sku)")
|
|
406
|
-
|
|
418
|
+
|
|
407
419
|
if let statuses = try await OpenIapModule.shared.subscriptionStatusIOS(sku: sku) {
|
|
408
420
|
// Align output with SubscriptionStatusIOS in TS:
|
|
409
421
|
// { state: SubscriptionState; renewalInfo?: { jsonRepresentation?: string; willAutoRenew: boolean; autoRenewPreference?: string } }
|
|
@@ -416,7 +428,7 @@ public class ExpoIapModule: Module {
|
|
|
416
428
|
// autoRenewStatus is a Bool from OpenIAP types
|
|
417
429
|
let renewalInfo: [String: Any?] = [
|
|
418
430
|
"willAutoRenew": info.autoRenewStatus,
|
|
419
|
-
"autoRenewPreference": info.autoRenewPreference
|
|
431
|
+
"autoRenewPreference": info.autoRenewPreference,
|
|
420
432
|
]
|
|
421
433
|
dict["renewalInfo"] = renewalInfo
|
|
422
434
|
}
|
|
@@ -426,20 +438,21 @@ public class ExpoIapModule: Module {
|
|
|
426
438
|
}
|
|
427
439
|
return nil
|
|
428
440
|
}
|
|
429
|
-
|
|
441
|
+
|
|
430
442
|
AsyncFunction("currentEntitlementIOS") { (sku: String) async throws -> [String: Any]? in
|
|
431
443
|
try await ensureConnection()
|
|
432
444
|
logDebug("currentEntitlementIOS called for sku: \(sku)")
|
|
433
445
|
do {
|
|
434
|
-
if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku)
|
|
446
|
+
if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku)
|
|
447
|
+
{
|
|
435
448
|
return OpenIapSerialization.purchase(entitlement).compactingValues()
|
|
436
449
|
}
|
|
437
450
|
return nil
|
|
438
451
|
} catch {
|
|
439
|
-
throw OpenIapError.make(code: OpenIapError.
|
|
452
|
+
throw OpenIapError.make(code: OpenIapError.SkuNotFound, productId: sku)
|
|
440
453
|
}
|
|
441
454
|
}
|
|
442
|
-
|
|
455
|
+
|
|
443
456
|
AsyncFunction("latestTransactionIOS") { (sku: String) async throws -> [String: Any]? in
|
|
444
457
|
try await ensureConnection()
|
|
445
458
|
logDebug("latestTransactionIOS called for sku: \(sku)")
|
|
@@ -449,17 +462,17 @@ public class ExpoIapModule: Module {
|
|
|
449
462
|
}
|
|
450
463
|
return nil
|
|
451
464
|
} catch {
|
|
452
|
-
throw OpenIapError.make(code: OpenIapError.
|
|
465
|
+
throw OpenIapError.make(code: OpenIapError.SkuNotFound, productId: sku)
|
|
453
466
|
}
|
|
454
467
|
}
|
|
455
468
|
}
|
|
456
|
-
|
|
469
|
+
|
|
457
470
|
// MARK: - Listeners Setup
|
|
458
|
-
|
|
471
|
+
|
|
459
472
|
@MainActor
|
|
460
473
|
private func setupStore() {
|
|
461
474
|
logDebug("Setting up OpenIapModule event listeners")
|
|
462
|
-
|
|
475
|
+
|
|
463
476
|
purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] purchase in
|
|
464
477
|
Task { @MainActor in
|
|
465
478
|
guard let self else { return }
|
|
@@ -468,7 +481,7 @@ public class ExpoIapModule: Module {
|
|
|
468
481
|
self.sendEvent(OpenIapEvent.PurchaseUpdated, purchaseData)
|
|
469
482
|
}
|
|
470
483
|
}
|
|
471
|
-
|
|
484
|
+
|
|
472
485
|
purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] event in
|
|
473
486
|
Task { @MainActor in
|
|
474
487
|
guard let self else { return }
|
|
@@ -476,13 +489,14 @@ public class ExpoIapModule: Module {
|
|
|
476
489
|
let errorData: [String: Any?] = [
|
|
477
490
|
"code": event.code,
|
|
478
491
|
"message": event.message,
|
|
479
|
-
"productId": event.productId
|
|
492
|
+
"productId": event.productId,
|
|
480
493
|
]
|
|
481
494
|
self.sendEvent(OpenIapEvent.PurchaseError, errorData)
|
|
482
495
|
}
|
|
483
496
|
}
|
|
484
|
-
|
|
485
|
-
promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS {
|
|
497
|
+
|
|
498
|
+
promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS {
|
|
499
|
+
[weak self] productId in
|
|
486
500
|
Task { @MainActor in
|
|
487
501
|
guard let self else { return }
|
|
488
502
|
logDebug("📱 Promoted product callback - sending event for: \(productId)")
|
|
@@ -490,7 +504,7 @@ public class ExpoIapModule: Module {
|
|
|
490
504
|
}
|
|
491
505
|
}
|
|
492
506
|
}
|
|
493
|
-
|
|
507
|
+
|
|
494
508
|
@MainActor
|
|
495
509
|
private func cleanupStore() async {
|
|
496
510
|
logDebug("Cleaning up listeners and ending connection")
|
|
@@ -499,13 +513,13 @@ public class ExpoIapModule: Module {
|
|
|
499
513
|
removeListener(&promotedProductSub)
|
|
500
514
|
_ = try? await OpenIapModule.shared.endConnection()
|
|
501
515
|
}
|
|
502
|
-
|
|
516
|
+
|
|
503
517
|
// MARK: - Private Helper Methods
|
|
504
|
-
|
|
518
|
+
|
|
505
519
|
private func ensureConnection() throws {
|
|
506
520
|
guard isInitialized else {
|
|
507
521
|
throw OpenIapError.make(
|
|
508
|
-
code: OpenIapError.
|
|
522
|
+
code: OpenIapError.InitConnection,
|
|
509
523
|
message: "Connection not initialized. Call initConnection() first."
|
|
510
524
|
)
|
|
511
525
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.6",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"docs:build": "cd docs && bun run build",
|
|
28
28
|
"docs:serve": "cd docs && bun run serve",
|
|
29
29
|
"docs:install": "cd docs && bun install",
|
|
30
|
+
"generate:types": "node scripts/update-types.mjs --tag 1.0.2",
|
|
30
31
|
"generate:icon": "npx sharp-cli resize 32 32 -i docs/static/img/icon.png -o docs/static/img/favicon-32x32.png && npx sharp-cli resize 16 16 -i docs/static/img/icon.png -o docs/static/img/favicon-16x16.png && npx sharp-cli resize 180 180 -i docs/static/img/icon.png -o docs/static/img/apple-touch-icon.png && npx sharp-cli resize 192 192 -i docs/static/img/icon.png -o docs/static/img/android-chrome-192x192.png && npx sharp-cli resize 512 512 -i docs/static/img/icon.png -o docs/static/img/android-chrome-512x512.png && npx sharp-cli resize 150 150 -i docs/static/img/icon.png -o docs/static/img/mstile-150x150.png && npx sharp-cli resize 1200 630 -i docs/static/img/icon.png -o docs/static/img/og-image.png && npx sharp-cli resize 1200 600 -i docs/static/img/icon.png -o docs/static/img/twitter-card.png && npx sharp-cli resize 16 16 -i docs/static/img/icon.png -o docs/static/img/favicon.png && cp docs/static/img/favicon-16x16.png docs/static/img/favicon.ico"
|
|
31
32
|
},
|
|
32
33
|
"keywords": [
|
package/plugin/build/withIAP.js
CHANGED
|
@@ -69,8 +69,7 @@ const modifyAppBuildGradle = (gradle, language) => {
|
|
|
69
69
|
const impl = (ga, v) => language === 'kotlin'
|
|
70
70
|
? ` implementation("${ga}:${v}")`
|
|
71
71
|
: ` implementation "${ga}:${v}"`;
|
|
72
|
-
|
|
73
|
-
const openiapDep = impl('io.github.hyochan.openiap:openiap-google', '1.1.0');
|
|
72
|
+
const openiapDep = impl('io.github.hyochan.openiap:openiap-google', '1.1.10');
|
|
74
73
|
// Remove any existing openiap-google lines (any version, groovy/kotlin, implementation/api)
|
|
75
74
|
const openiapAnyLine = /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)?\s*$/gm;
|
|
76
75
|
const hadExisting = openiapAnyLine.test(modified);
|
|
@@ -78,12 +77,12 @@ const modifyAppBuildGradle = (gradle, language) => {
|
|
|
78
77
|
modified = modified.replace(openiapAnyLine, '').replace(/\n{3,}/g, '\n\n');
|
|
79
78
|
}
|
|
80
79
|
// Ensure the desired dependency line is present
|
|
81
|
-
if (!new RegExp(String.raw `io\.github\.hyochan\.openiap:openiap-google:1\.1\.
|
|
80
|
+
if (!new RegExp(String.raw `io\.github\.hyochan\.openiap:openiap-google:1\.1\.10`).test(modified)) {
|
|
82
81
|
// Insert just after the opening `dependencies {` line
|
|
83
82
|
modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 1);
|
|
84
83
|
logOnce(hadExisting
|
|
85
|
-
? '🛠️ expo-iap: Replaced OpenIAP dependency with 1.1.
|
|
86
|
-
: '🛠️ expo-iap: Added OpenIAP dependency (1.1.
|
|
84
|
+
? '🛠️ expo-iap: Replaced OpenIAP dependency with 1.1.10'
|
|
85
|
+
: '🛠️ expo-iap: Added OpenIAP dependency (1.1.10) to build.gradle');
|
|
87
86
|
}
|
|
88
87
|
return modified;
|
|
89
88
|
};
|
package/plugin/src/withIAP.ts
CHANGED
|
@@ -54,8 +54,7 @@ const modifyAppBuildGradle = (
|
|
|
54
54
|
language === 'kotlin'
|
|
55
55
|
? ` implementation("${ga}:${v}")`
|
|
56
56
|
: ` implementation "${ga}:${v}"`;
|
|
57
|
-
|
|
58
|
-
const openiapDep = impl('io.github.hyochan.openiap:openiap-google', '1.1.0');
|
|
57
|
+
const openiapDep = impl('io.github.hyochan.openiap:openiap-google', '1.1.10');
|
|
59
58
|
|
|
60
59
|
// Remove any existing openiap-google lines (any version, groovy/kotlin, implementation/api)
|
|
61
60
|
const openiapAnyLine =
|
|
@@ -68,15 +67,15 @@ const modifyAppBuildGradle = (
|
|
|
68
67
|
// Ensure the desired dependency line is present
|
|
69
68
|
if (
|
|
70
69
|
!new RegExp(
|
|
71
|
-
String.raw`io\.github\.hyochan\.openiap:openiap-google:1\.1\.
|
|
70
|
+
String.raw`io\.github\.hyochan\.openiap:openiap-google:1\.1\.10`,
|
|
72
71
|
).test(modified)
|
|
73
72
|
) {
|
|
74
73
|
// Insert just after the opening `dependencies {` line
|
|
75
74
|
modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 1);
|
|
76
75
|
logOnce(
|
|
77
76
|
hadExisting
|
|
78
|
-
? '🛠️ expo-iap: Replaced OpenIAP dependency with 1.1.
|
|
79
|
-
: '🛠️ expo-iap: Added OpenIAP dependency (1.1.
|
|
77
|
+
? '🛠️ expo-iap: Replaced OpenIAP dependency with 1.1.10'
|
|
78
|
+
: '🛠️ expo-iap: Added OpenIAP dependency (1.1.10) to build.gradle',
|
|
80
79
|
);
|
|
81
80
|
}
|
|
82
81
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {mkdtempSync, readFileSync, writeFileSync, rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import {execFileSync} from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TAG = '1.0.1';
|
|
8
|
+
const PROJECT_ROOT = process.cwd();
|
|
9
|
+
|
|
10
|
+
function parseArgs() {
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
let tag = DEFAULT_TAG;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg === '--tag' && typeof args[i + 1] === 'string') {
|
|
17
|
+
tag = args[i + 1];
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {tag};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getReleaseUrl(tag) {
|
|
26
|
+
return `https://github.com/hyodotdev/openiap-gql/releases/download/${tag}/openiap-typescript.zip`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function main() {
|
|
30
|
+
const {tag} = parseArgs();
|
|
31
|
+
const releaseUrl = getReleaseUrl(tag);
|
|
32
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'openiap-types-'));
|
|
33
|
+
const zipPath = join(tempDir, 'openiap-typescript.zip');
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
console.log(`Downloading OpenIAP types (tag: ${tag}) from ${releaseUrl}`);
|
|
37
|
+
execFileSync('curl', ['-L', '-o', zipPath, releaseUrl], {
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log('Extracting types.ts from archive');
|
|
42
|
+
execFileSync('unzip', ['-o', zipPath, 'types.ts', '-d', tempDir], {
|
|
43
|
+
stdio: 'inherit',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const extractedPath = join(tempDir, 'types.ts');
|
|
47
|
+
let contents = readFileSync(extractedPath, 'utf8');
|
|
48
|
+
contents = contents.replace(
|
|
49
|
+
/Run `[^`]+` after updating any \*\.graphql schema file\./,
|
|
50
|
+
'Run `bun run generate:types` after updating any *.graphql schema file.',
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const destination = join(PROJECT_ROOT, 'src', 'types.ts');
|
|
54
|
+
writeFileSync(destination, contents);
|
|
55
|
+
console.log('Updated src/types.ts');
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(tempDir, {recursive: true, force: true});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main();
|