@capgo/native-purchases 7.9.4 → 7.12.5

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.
@@ -17,14 +17,15 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
17
17
  CAPPluginMethod(name: "getProducts", returnType: CAPPluginReturnPromise),
18
18
  CAPPluginMethod(name: "getProduct", returnType: CAPPluginReturnPromise),
19
19
  CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
20
- CAPPluginMethod(name: "getPurchases", returnType: CAPPluginReturnPromise)
20
+ CAPPluginMethod(name: "getPurchases", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "manageSubscriptions", returnType: CAPPluginReturnPromise)
21
22
  ]
22
23
 
23
- private let PLUGIN_VERSION: String = "7.9.4"
24
+ private let pluginVersion: String = "7.12.5"
24
25
  private var transactionUpdatesTask: Task<Void, Never>?
25
26
 
26
27
  @objc func getPluginVersion(_ call: CAPPluginCall) {
27
- call.resolve(["version": self.PLUGIN_VERSION])
28
+ call.resolve(["version": self.pluginVersion])
28
29
  }
29
30
 
30
31
  override public func load() {
@@ -61,42 +62,7 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
61
62
  }
62
63
 
63
64
  // Build payload similar to purchase response
64
- var payload: [String: Any] = ["transactionId": String(transaction.id)]
65
-
66
- // Always include willCancel key with NSNull() default
67
- payload["willCancel"] = NSNull()
68
-
69
- if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
70
- FileManager.default.fileExists(atPath: appStoreReceiptURL.path),
71
- let receiptData = try? Data(contentsOf: appStoreReceiptURL) {
72
- payload["receipt"] = receiptData.base64EncodedString()
73
- }
74
-
75
- payload["productIdentifier"] = transaction.productID
76
- payload["purchaseDate"] = dateFormatter.string(from: transaction.purchaseDate)
77
- payload["productType"] = transaction.productType == .autoRenewable ? "subs" : "inapp"
78
-
79
- if transaction.productType == .autoRenewable {
80
- payload["originalPurchaseDate"] = dateFormatter.string(from: transaction.originalPurchaseDate)
81
- if let expirationDate = transaction.expirationDate {
82
- payload["expirationDate"] = dateFormatter.string(from: expirationDate)
83
- payload["isActive"] = expirationDate > Date()
84
- }
85
- }
86
-
87
- let subscriptionStatus = await transaction.subscriptionStatus
88
- if let subscriptionStatus = subscriptionStatus {
89
- if subscriptionStatus.state == .subscribed {
90
- let renewalInfo = subscriptionStatus.renewalInfo
91
- switch renewalInfo {
92
- case .verified(let value):
93
- payload["willCancel"] = !value.willAutoRenew
94
- case .unverified:
95
- // willCancel remains NSNull() for unverified renewalInfo
96
- break
97
- }
98
- }
99
- }
65
+ let payload = await TransactionHelpers.buildTransactionResponse(from: transaction, alwaysIncludeWillCancel: true)
100
66
 
101
67
  // Finish the transaction to avoid blocking future purchases
102
68
  await transaction.finish()
@@ -112,6 +78,8 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
112
78
  transactionUpdatesTask = task
113
79
  }
114
80
 
81
+ // MARK: - Plugin Methods
82
+
115
83
  @objc func isBillingSupported(_ call: CAPPluginCall) {
116
84
  if #available(iOS 15, *) {
117
85
  call.resolve([
@@ -130,6 +98,7 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
130
98
  let productIdentifier = call.getString("productIdentifier", "")
131
99
  let quantity = call.getInt("quantity", 1)
132
100
  let appAccountToken = call.getString("appAccountToken")
101
+
133
102
  if productIdentifier.isEmpty {
134
103
  call.reject("productIdentifier is Empty, give an id")
135
104
  return
@@ -142,90 +111,12 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
142
111
  call.reject("Cannot find product for id \(productIdentifier)")
143
112
  return
144
113
  }
145
- var purchaseOptions = Set<Product.PurchaseOption>()
146
- purchaseOptions.insert(Product.PurchaseOption.quantity(quantity))
147
-
148
- // Add appAccountToken if provided
149
- if let accountToken = appAccountToken, !accountToken.isEmpty {
150
- if let tokenData = UUID(uuidString: accountToken) {
151
- purchaseOptions.insert(Product.PurchaseOption.appAccountToken(tokenData))
152
- }
153
- }
154
114
 
115
+ let purchaseOptions = buildPurchaseOptions(quantity: quantity, appAccountToken: appAccountToken)
155
116
  let result = try await product.purchase(options: purchaseOptions)
156
117
  print("purchaseProduct result \(result)")
157
- switch result {
158
- case let .success(.verified(transaction)):
159
- // Successful purchase
160
- var response: [String: Any] = ["transactionId": transaction.id]
161
-
162
- // Get receipt data
163
- if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
164
- FileManager.default.fileExists(atPath: appStoreReceiptURL.path),
165
- let receiptData = try? Data(contentsOf: appStoreReceiptURL) {
166
- let receiptBase64 = receiptData.base64EncodedString()
167
- response["receipt"] = receiptBase64
168
- }
169
118
 
170
- // Add detailed transaction information
171
- response["productIdentifier"] = transaction.productID
172
- response["purchaseDate"] = ISO8601DateFormatter().string(from: transaction.purchaseDate)
173
- response["productType"] = transaction.productType == .autoRenewable ? "subs" : "inapp"
174
- if let token = transaction.appAccountToken {
175
- let tokenString = token.uuidString
176
- response["appAccountToken"] = tokenString
177
- }
178
-
179
- // Add subscription-specific information
180
- if transaction.productType == .autoRenewable {
181
- response["originalPurchaseDate"] = ISO8601DateFormatter().string(from: transaction.originalPurchaseDate)
182
- if let expirationDate = transaction.expirationDate {
183
- response["expirationDate"] = ISO8601DateFormatter().string(from: expirationDate)
184
- let isActive = expirationDate > Date()
185
- response["isActive"] = isActive
186
- }
187
- }
188
-
189
- let subscriptionStatus = await transaction.subscriptionStatus
190
- if let subscriptionStatus = subscriptionStatus {
191
- // You can use 'state' here if needed
192
- let state = subscriptionStatus.state
193
- if state == .subscribed {
194
- // Use Objective-C reflection to access advancedCommerceInfo
195
- let renewalInfo = subscriptionStatus.renewalInfo
196
-
197
- switch renewalInfo {
198
- case .verified(let value):
199
- // if #available(iOS 18.4, *) {
200
- // // This should work but may need runtime access
201
- // let advancedInfo = value.advancedCommerceInfo
202
- // print("Advanced commerce info: \(advancedInfo)")
203
- // }
204
- // print("[InAppPurchase] Subscription renewalInfo verified.")
205
- response["willCancel"] = !value.willAutoRenew
206
- case .unverified:
207
- print("[InAppPurchase] Subscription renewalInfo not verified.")
208
- response["willCancel"] = NSNull()
209
- }
210
- }
211
- }
212
-
213
- await transaction.finish()
214
- call.resolve(response)
215
- case let .success(.unverified(_, error)):
216
- // Successful purchase but transaction/receipt can't be verified
217
- // Could be a jailbroken phone
218
- call.reject(error.localizedDescription)
219
- case .pending:
220
- // Transaction waiting on SCA (Strong Customer Authentication) or
221
- // approval from Ask to Buy
222
- call.reject("Transaction pending")
223
- case .userCancelled:
224
- // ^^^
225
- call.reject("User cancelled")
226
- @unknown default:
227
- call.reject("Unknown error")
228
- }
119
+ await handlePurchaseResult(result, call: call)
229
120
  } catch {
230
121
  print(error)
231
122
  call.reject(error.localizedDescription)
@@ -237,6 +128,36 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
237
128
  }
238
129
  }
239
130
 
131
+ @available(iOS 15.0, *)
132
+ private func buildPurchaseOptions(quantity: Int, appAccountToken: String?) -> Set<Product.PurchaseOption> {
133
+ var purchaseOptions = Set<Product.PurchaseOption>()
134
+ purchaseOptions.insert(Product.PurchaseOption.quantity(quantity))
135
+
136
+ if let accountToken = appAccountToken, !accountToken.isEmpty, let tokenData = UUID(uuidString: accountToken) {
137
+ purchaseOptions.insert(Product.PurchaseOption.appAccountToken(tokenData))
138
+ }
139
+
140
+ return purchaseOptions
141
+ }
142
+
143
+ @available(iOS 15.0, *)
144
+ private func handlePurchaseResult(_ result: Product.PurchaseResult, call: CAPPluginCall) async {
145
+ switch result {
146
+ case let .success(.verified(transaction)):
147
+ let response = await TransactionHelpers.buildTransactionResponse(from: transaction)
148
+ await transaction.finish()
149
+ call.resolve(response)
150
+ case let .success(.unverified(_, error)):
151
+ call.reject(error.localizedDescription)
152
+ case .pending:
153
+ call.reject("Transaction pending")
154
+ case .userCancelled:
155
+ call.reject("User cancelled")
156
+ @unknown default:
157
+ call.reject("Unknown error")
158
+ }
159
+ }
160
+
240
161
  @objc func restorePurchases(_ call: CAPPluginCall) {
241
162
  if #available(iOS 15.0, *) {
242
163
  print("restorePurchases")
@@ -324,151 +245,7 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
324
245
  DispatchQueue.global().async {
325
246
  Task {
326
247
  do {
327
- var allPurchases: [[String: Any]] = []
328
-
329
- // Get all current entitlements (active subscriptions)
330
- for await result in Transaction.currentEntitlements {
331
- if case .verified(let transaction) = result {
332
- let transactionAccountToken = transaction.appAccountToken?.uuidString
333
- if let filter = appAccountTokenFilter {
334
- guard let token = transactionAccountToken, token == filter else {
335
- continue
336
- }
337
- }
338
- var purchaseData: [String: Any] = ["transactionId": String(transaction.id)]
339
-
340
- // Get receipt data
341
- if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
342
- FileManager.default.fileExists(atPath: appStoreReceiptURL.path),
343
- let receiptData = try? Data(contentsOf: appStoreReceiptURL) {
344
- let receiptBase64 = receiptData.base64EncodedString()
345
- purchaseData["receipt"] = receiptBase64
346
- }
347
-
348
- // Add detailed transaction information
349
- purchaseData["productIdentifier"] = transaction.productID
350
- purchaseData["purchaseDate"] = ISO8601DateFormatter().string(from: transaction.purchaseDate)
351
- purchaseData["productType"] = transaction.productType == .autoRenewable ? "subs" : "inapp"
352
- if let token = transactionAccountToken {
353
- purchaseData["appAccountToken"] = token
354
- }
355
-
356
- // Add subscription-specific information
357
- if transaction.productType == .autoRenewable {
358
- purchaseData["originalPurchaseDate"] = ISO8601DateFormatter().string(from: transaction.originalPurchaseDate)
359
- if let expirationDate = transaction.expirationDate {
360
- purchaseData["expirationDate"] = ISO8601DateFormatter().string(from: expirationDate)
361
- let isActive = expirationDate > Date()
362
- purchaseData["isActive"] = isActive
363
- }
364
- }
365
-
366
- let subscriptionStatus = await transaction.subscriptionStatus
367
- if let subscriptionStatus = subscriptionStatus {
368
- // You can use 'state' here if needed
369
- let state = subscriptionStatus.state
370
- if state == .subscribed {
371
- // Use Objective-C reflection to access advancedCommerceInfo
372
- let renewalInfo = subscriptionStatus.renewalInfo
373
-
374
- switch renewalInfo {
375
- case .verified(let value):
376
- // if #available(iOS 18.4, *) {
377
- // // This should work but may need runtime access
378
- // let advancedInfo = value.advancedCommerceInfo
379
- // print("Advanced commerce info: \(advancedInfo)")
380
- // }
381
- // print("[InAppPurchase] Subscription renewalInfo verified.")
382
- purchaseData["willCancel"] = !value.willAutoRenew
383
- case .unverified:
384
- print("[InAppPurchase] Subscription renewalInfo not verified.")
385
- purchaseData["willCancel"] = NSNull()
386
- }
387
- }
388
- }
389
-
390
- allPurchases.append(purchaseData)
391
- }
392
- }
393
-
394
- // Also get all transactions (including non-consumables and expired subscriptions)
395
- for await result in Transaction.all {
396
- if case .verified(let transaction) = result {
397
- let transactionIdString = String(transaction.id)
398
- let transactionAccountToken = transaction.appAccountToken?.uuidString
399
-
400
- if let filter = appAccountTokenFilter {
401
- guard let token = transactionAccountToken, token == filter else {
402
- continue
403
- }
404
- }
405
-
406
- // Check if we already have this transaction
407
- let alreadyExists = allPurchases.contains { purchase in
408
- if let existingId = purchase["transactionId"] as? String {
409
- return existingId == transactionIdString
410
- }
411
- return false
412
- }
413
-
414
- if !alreadyExists {
415
- var purchaseData: [String: Any] = ["transactionId": transactionIdString]
416
-
417
- // Get receipt data
418
- if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
419
- FileManager.default.fileExists(atPath: appStoreReceiptURL.path),
420
- let receiptData = try? Data(contentsOf: appStoreReceiptURL) {
421
- let receiptBase64 = receiptData.base64EncodedString()
422
- purchaseData["receipt"] = receiptBase64
423
- }
424
-
425
- // Add detailed transaction information
426
- purchaseData["productIdentifier"] = transaction.productID
427
- purchaseData["purchaseDate"] = ISO8601DateFormatter().string(from: transaction.purchaseDate)
428
- purchaseData["productType"] = transaction.productType == .autoRenewable ? "subs" : "inapp"
429
- if let token = transactionAccountToken {
430
- purchaseData["appAccountToken"] = token
431
- }
432
-
433
- // Add subscription-specific information
434
- if transaction.productType == .autoRenewable {
435
- purchaseData["originalPurchaseDate"] = ISO8601DateFormatter().string(from: transaction.originalPurchaseDate)
436
- if let expirationDate = transaction.expirationDate {
437
- purchaseData["expirationDate"] = ISO8601DateFormatter().string(from: expirationDate)
438
- let isActive = expirationDate > Date()
439
- purchaseData["isActive"] = isActive
440
- }
441
- }
442
-
443
- let subscriptionStatus = await transaction.subscriptionStatus
444
- if let subscriptionStatus = subscriptionStatus {
445
- // You can use 'state' here if needed
446
- let state = subscriptionStatus.state
447
- if state == .subscribed {
448
- // Use Objective-C reflection to access advancedCommerceInfo
449
- let renewalInfo = subscriptionStatus.renewalInfo
450
-
451
- switch renewalInfo {
452
- case .verified(let value):
453
- // if #available(iOS 18.4, *) {
454
- // // This should work but may need runtime access
455
- // let advancedInfo = value.advancedCommerceInfo
456
- // print("Advanced commerce info: \(advancedInfo)")
457
- // }
458
- // print("[InAppPurchase] Subscription renewalInfo verified.")
459
- purchaseData["willCancel"] = !value.willAutoRenew
460
- case .unverified:
461
- print("[InAppPurchase] Subscription renewalInfo not verified.")
462
- purchaseData["willCancel"] = NSNull()
463
- }
464
- }
465
- }
466
-
467
- allPurchases.append(purchaseData)
468
- }
469
- }
470
- }
471
-
248
+ let allPurchases = await TransactionHelpers.collectAllPurchases(appAccountTokenFilter: appAccountTokenFilter)
472
249
  call.resolve(["purchases": allPurchases])
473
250
  } catch {
474
251
  print("getPurchases error: \(error)")
@@ -482,4 +259,28 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
482
259
  }
483
260
  }
484
261
 
262
+ @objc func manageSubscriptions(_ call: CAPPluginCall) {
263
+ if #available(iOS 15.0, *) {
264
+ print("manageSubscriptions")
265
+ Task { @MainActor in
266
+ do {
267
+ // Get the current window scene
268
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
269
+ call.reject("Unable to get window scene")
270
+ return
271
+ }
272
+ // Open the App Store subscription management page
273
+ try await AppStore.showManageSubscriptions(in: windowScene)
274
+ call.resolve()
275
+ } catch {
276
+ print("manageSubscriptions error: \(error)")
277
+ call.reject(error.localizedDescription)
278
+ }
279
+ }
280
+ } else {
281
+ print("Not implemented under iOS 15")
282
+ call.reject("Not implemented under iOS 15")
283
+ }
284
+ }
285
+
485
286
  }
@@ -0,0 +1,132 @@
1
+ //
2
+ // TransactionHelpers.swift
3
+ // CapgoNativePurchases
4
+ //
5
+ // Created by Martin DONADIEU
6
+ //
7
+
8
+ import Foundation
9
+ import StoreKit
10
+
11
+ @available(iOS 15.0, *)
12
+ internal class TransactionHelpers {
13
+
14
+ static func buildTransactionResponse(from transaction: Transaction, alwaysIncludeWillCancel: Bool = false) async -> [String: Any] {
15
+ var response: [String: Any] = ["transactionId": String(transaction.id)]
16
+
17
+ // Always include willCancel key with NSNull() default if requested (for transaction listener)
18
+ if alwaysIncludeWillCancel {
19
+ response["willCancel"] = NSNull()
20
+ }
21
+
22
+ // Get receipt data
23
+ if let receiptBase64 = getReceiptData() {
24
+ response["receipt"] = receiptBase64
25
+ }
26
+
27
+ // Add detailed transaction information
28
+ response["productIdentifier"] = transaction.productID
29
+ response["purchaseDate"] = ISO8601DateFormatter().string(from: transaction.purchaseDate)
30
+ response["productType"] = transaction.productType == .autoRenewable ? "subs" : "inapp"
31
+
32
+ if let token = transaction.appAccountToken {
33
+ response["appAccountToken"] = token.uuidString
34
+ }
35
+
36
+ // Add subscription-specific information
37
+ if transaction.productType == .autoRenewable {
38
+ addSubscriptionInfo(to: &response, transaction: transaction)
39
+ await addRenewalInfo(to: &response, transaction: transaction)
40
+ }
41
+
42
+ return response
43
+ }
44
+
45
+ static func getReceiptData() -> String? {
46
+ guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
47
+ FileManager.default.fileExists(atPath: appStoreReceiptURL.path),
48
+ let receiptData = try? Data(contentsOf: appStoreReceiptURL) else {
49
+ return nil
50
+ }
51
+ return receiptData.base64EncodedString()
52
+ }
53
+
54
+ static func addSubscriptionInfo(to response: inout [String: Any], transaction: Transaction) {
55
+ response["originalPurchaseDate"] = ISO8601DateFormatter().string(from: transaction.originalPurchaseDate)
56
+ if let expirationDate = transaction.expirationDate {
57
+ response["expirationDate"] = ISO8601DateFormatter().string(from: expirationDate)
58
+ response["isActive"] = expirationDate > Date()
59
+ }
60
+ }
61
+
62
+ static func addRenewalInfo(to response: inout [String: Any], transaction: Transaction) async {
63
+ let subscriptionStatus = await transaction.subscriptionStatus
64
+ guard let subscriptionStatus = subscriptionStatus else {
65
+ response["willCancel"] = NSNull()
66
+ return
67
+ }
68
+
69
+ if subscriptionStatus.state == .subscribed {
70
+ let renewalInfo = subscriptionStatus.renewalInfo
71
+ switch renewalInfo {
72
+ case .verified(let value):
73
+ response["willCancel"] = !value.willAutoRenew
74
+ case .unverified:
75
+ response["willCancel"] = NSNull()
76
+ }
77
+ } else {
78
+ response["willCancel"] = NSNull()
79
+ }
80
+ }
81
+
82
+ static func shouldFilterTransaction(_ transaction: Transaction, filter: String?) -> Bool {
83
+ guard let filter = filter else { return false }
84
+ let transactionAccountToken = transaction.appAccountToken?.uuidString
85
+ return transactionAccountToken != filter
86
+ }
87
+
88
+ static func collectAllPurchases(appAccountTokenFilter: String?) async -> [[String: Any]] {
89
+ var allPurchases: [[String: Any]] = []
90
+
91
+ // Get all current entitlements (active subscriptions)
92
+ await collectCurrentEntitlements(appAccountTokenFilter: appAccountTokenFilter, into: &allPurchases)
93
+
94
+ // Also get all transactions (including non-consumables and expired subscriptions)
95
+ await collectAllTransactions(appAccountTokenFilter: appAccountTokenFilter, into: &allPurchases)
96
+
97
+ return allPurchases
98
+ }
99
+
100
+ static func collectCurrentEntitlements(appAccountTokenFilter: String?, into allPurchases: inout [[String: Any]]) async {
101
+ for await result in Transaction.currentEntitlements {
102
+ guard case .verified(let transaction) = result else { continue }
103
+
104
+ if shouldFilterTransaction(transaction, filter: appAccountTokenFilter) {
105
+ continue
106
+ }
107
+
108
+ let purchaseData = await buildTransactionResponse(from: transaction)
109
+ allPurchases.append(purchaseData)
110
+ }
111
+ }
112
+
113
+ static func collectAllTransactions(appAccountTokenFilter: String?, into allPurchases: inout [[String: Any]]) async {
114
+ for await result in Transaction.all {
115
+ guard case .verified(let transaction) = result else { continue }
116
+
117
+ if shouldFilterTransaction(transaction, filter: appAccountTokenFilter) {
118
+ continue
119
+ }
120
+
121
+ let transactionIdString = String(transaction.id)
122
+ let alreadyExists = allPurchases.contains { purchase in
123
+ (purchase["transactionId"] as? String) == transactionIdString
124
+ }
125
+
126
+ if !alreadyExists {
127
+ let purchaseData = await buildTransactionResponse(from: transaction)
128
+ allPurchases.append(purchaseData)
129
+ }
130
+ }
131
+ }
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-purchases",
3
- "version": "7.9.4",
3
+ "version": "7.12.5",
4
4
  "description": "In-app Subscriptions Made Easy",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",