@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.
- package/README.md +748 -50
- package/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java +135 -4
- package/dist/docs.json +296 -36
- package/dist/esm/definitions.d.ts +186 -19
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -0
- package/dist/esm/web.js +3 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +3 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativePurchasesPlugin/NativePurchasesPlugin.swift +65 -264
- package/ios/Sources/NativePurchasesPlugin/TransactionHelpers.swift +132 -0
- package/package.json +1 -1
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|