@capgo/native-purchases 7.18.0-alpha.0 → 7.18.0

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.
@@ -1,87 +1,61 @@
1
- //
2
- // TransactionHelpers.swift
3
- // CapgoNativePurchases
4
- //
5
- // Created by Martin DONADIEU
6
- //
7
-
8
1
  import Foundation
9
2
  import StoreKit
10
3
 
11
- @available(iOS 15.0, *)
12
4
  internal class TransactionHelpers {
13
5
 
14
- static func buildTransactionResponse(from transaction: Transaction, jwsRepresentation: String? = nil, alwaysIncludeWillCancel: Bool = false) async -> [String: Any] {
6
+ static func buildTransactionResponse(
7
+ from transaction: Transaction,
8
+ jwsRepresentation: String? = nil,
9
+ alwaysIncludeWillCancel: Bool = false
10
+ ) async -> [String: Any] {
15
11
  var response: [String: Any] = ["transactionId": String(transaction.id)]
16
12
 
17
- // Always include willCancel key with NSNull() default if requested (for transaction listener)
18
13
  if alwaysIncludeWillCancel {
19
14
  response["willCancel"] = NSNull()
20
15
  }
21
16
 
22
- // Get receipt data (may not exist in Xcode/sandbox testing)
17
+ addReceiptAndJws(to: &response, jws: jwsRepresentation)
18
+ addTransactionDetails(to: &response, transaction: transaction)
19
+
20
+ if transaction.productType == .autoRenewable {
21
+ addSubscriptionInfo(to: &response, transaction: transaction)
22
+ await addRenewalInfo(to: &response, transaction: transaction)
23
+ }
24
+
25
+ return response
26
+ }
27
+
28
+ private static func addReceiptAndJws(to response: inout [String: Any], jws: String?) {
23
29
  if let receiptBase64 = getReceiptData() {
24
30
  response["receipt"] = receiptBase64
25
31
  }
26
-
27
- // Add StoreKit 2 JWS representation (always available when passed from VerificationResult)
28
- if let jws = jwsRepresentation {
32
+ if let jws = jws {
29
33
  response["jwsRepresentation"] = jws
30
34
  }
35
+ }
31
36
 
32
- // Add detailed transaction information
37
+ private static func addTransactionDetails(to response: inout [String: Any], transaction: Transaction) {
33
38
  response["productIdentifier"] = transaction.productID
34
39
  response["purchaseDate"] = ISO8601DateFormatter().string(from: transaction.purchaseDate)
35
40
  response["productType"] = transaction.productType == .autoRenewable ? "subs" : "inapp"
36
41
  response["isUpgraded"] = transaction.isUpgraded
42
+ response["ownershipType"] = transaction.ownershipType.descriptionString
37
43
 
38
44
  if let revocationDate = transaction.revocationDate {
39
45
  response["revocationDate"] = ISO8601DateFormatter().string(from: revocationDate)
40
46
  }
41
-
42
47
  if let revocationReason = transaction.revocationReason {
43
48
  response["revocationReason"] = revocationReason.descriptionString
44
49
  }
45
-
46
50
  if #available(iOS 17.0, *) {
47
51
  response["transactionReason"] = transaction.reason.descriptionString
48
52
  }
49
-
50
- // Add ownership type (purchased or familyShared)
51
- switch transaction.ownershipType {
52
- case .purchased:
53
- response["ownershipType"] = "purchased"
54
- case .familyShared:
55
- response["ownershipType"] = "familyShared"
56
- default:
57
- response["ownershipType"] = "purchased"
58
- }
59
-
60
- // Add environment (Sandbox, Production, or Xcode) - iOS 16.0+
61
53
  if #available(iOS 16.0, *) {
62
- switch transaction.environment {
63
- case .sandbox:
64
- response["environment"] = "Sandbox"
65
- case .production:
66
- response["environment"] = "Production"
67
- case .xcode:
68
- response["environment"] = "Xcode"
69
- default:
70
- response["environment"] = "Production"
71
- }
54
+ response["environment"] = transaction.environment.descriptionString
72
55
  }
73
-
74
56
  if let token = transaction.appAccountToken {
75
57
  response["appAccountToken"] = token.uuidString
76
58
  }
77
-
78
- // Add subscription-specific information
79
- if transaction.productType == .autoRenewable {
80
- addSubscriptionInfo(to: &response, transaction: transaction)
81
- await addRenewalInfo(to: &response, transaction: transaction)
82
- }
83
-
84
- return response
85
59
  }
86
60
 
87
61
  static func getReceiptData() -> String? {
@@ -94,7 +68,9 @@ internal class TransactionHelpers {
94
68
  }
95
69
 
96
70
  static func addSubscriptionInfo(to response: inout [String: Any], transaction: Transaction) {
97
- response["originalPurchaseDate"] = ISO8601DateFormatter().string(from: transaction.originalPurchaseDate)
71
+ response["originalPurchaseDate"] = ISO8601DateFormatter().string(
72
+ from: transaction.originalPurchaseDate
73
+ )
98
74
  if let expirationDate = transaction.expirationDate {
99
75
  response["expirationDate"] = ISO8601DateFormatter().string(from: expirationDate)
100
76
  response["isActive"] = expirationDate > Date()
@@ -102,8 +78,7 @@ internal class TransactionHelpers {
102
78
  }
103
79
 
104
80
  static func addRenewalInfo(to response: inout [String: Any], transaction: Transaction) async {
105
- let subscriptionStatus = await transaction.subscriptionStatus
106
- guard let subscriptionStatus = subscriptionStatus else {
81
+ guard let subscriptionStatus = await transaction.subscriptionStatus else {
107
82
  response["willCancel"] = NSNull()
108
83
  return
109
84
  }
@@ -111,11 +86,9 @@ internal class TransactionHelpers {
111
86
  response["subscriptionState"] = subscriptionStatus.state.descriptionString
112
87
 
113
88
  if subscriptionStatus.state == .subscribed {
114
- let renewalInfo = subscriptionStatus.renewalInfo
115
- switch renewalInfo {
116
- case .verified(let value):
89
+ if case .verified(let value) = subscriptionStatus.renewalInfo {
117
90
  response["willCancel"] = !value.willAutoRenew
118
- case .unverified:
91
+ } else {
119
92
  response["willCancel"] = NSNull()
120
93
  }
121
94
  } else {
@@ -123,78 +96,49 @@ internal class TransactionHelpers {
123
96
  }
124
97
  }
125
98
 
126
- static func shouldFilterTransaction(_ transaction: Transaction, filter: String?) -> Bool {
127
- guard let filter = filter else { return false }
128
- let transactionAccountToken = transaction.appAccountToken?.uuidString
129
- return transactionAccountToken != filter
130
- }
131
-
132
99
  static func collectAllPurchases(appAccountTokenFilter: String?) async -> [[String: Any]] {
133
100
  var allPurchases: [[String: Any]] = []
101
+ var seenIds = Set<String>()
134
102
 
135
- // Get all current entitlements (active subscriptions)
136
- await collectCurrentEntitlements(appAccountTokenFilter: appAccountTokenFilter, into: &allPurchases)
137
-
138
- // Also get all transactions (including non-consumables and expired subscriptions)
139
- await collectAllTransactions(appAccountTokenFilter: appAccountTokenFilter, into: &allPurchases)
140
-
141
- return allPurchases
142
- }
143
-
144
- static func collectCurrentEntitlements(appAccountTokenFilter: String?, into allPurchases: inout [[String: Any]]) async {
145
103
  for await result in Transaction.currentEntitlements {
146
- guard case .verified(let transaction) = result else {
147
- if case .unverified(_, let error) = result {
148
- print("Skipping unverified entitlement: \(error.localizedDescription)")
149
- }
150
- continue
151
- }
152
-
153
- if shouldFilterTransaction(transaction, filter: appAccountTokenFilter) {
154
- continue
155
- }
104
+ guard case .verified(let transaction) = result else { continue }
105
+ if let filter = appAccountTokenFilter,
106
+ transaction.appAccountToken?.uuidString != filter { continue }
156
107
 
157
- let purchaseData = await buildTransactionResponse(from: transaction, jwsRepresentation: result.jwsRepresentation)
158
- allPurchases.append(purchaseData)
108
+ let idStr = String(transaction.id)
109
+ seenIds.insert(idStr)
110
+ let data = await buildTransactionResponse(
111
+ from: transaction,
112
+ jwsRepresentation: result.jwsRepresentation
113
+ )
114
+ allPurchases.append(data)
159
115
  }
160
- }
161
116
 
162
- static func collectAllTransactions(appAccountTokenFilter: String?, into allPurchases: inout [[String: Any]]) async {
163
117
  for await result in Transaction.all {
164
- guard case .verified(let transaction) = result else {
165
- if case .unverified(_, let error) = result {
166
- print("Skipping unverified transaction: \(error.localizedDescription)")
167
- }
168
- continue
169
- }
118
+ guard case .verified(let transaction) = result else { continue }
119
+ if let filter = appAccountTokenFilter,
120
+ transaction.appAccountToken?.uuidString != filter { continue }
170
121
 
171
- if shouldFilterTransaction(transaction, filter: appAccountTokenFilter) {
172
- continue
173
- }
122
+ let idStr = String(transaction.id)
123
+ if seenIds.contains(idStr) { continue }
174
124
 
175
- let transactionIdString = String(transaction.id)
176
- let alreadyExists = allPurchases.contains { purchase in
177
- (purchase["transactionId"] as? String) == transactionIdString
178
- }
179
-
180
- if !alreadyExists {
181
- let purchaseData = await buildTransactionResponse(from: transaction, jwsRepresentation: result.jwsRepresentation)
182
- allPurchases.append(purchaseData)
183
- }
125
+ let data = await buildTransactionResponse(
126
+ from: transaction,
127
+ jwsRepresentation: result.jwsRepresentation
128
+ )
129
+ allPurchases.append(data)
184
130
  }
131
+
132
+ return allPurchases
185
133
  }
186
134
  }
187
135
 
188
- @available(iOS 15.0, *)
189
136
  private extension Transaction.RevocationReason {
190
137
  var descriptionString: String {
191
138
  switch self {
192
- case .developerIssue:
193
- return "developerIssue"
194
- case .other:
195
- return "other"
196
- default:
197
- return "unknown"
139
+ case .developerIssue: return "developerIssue"
140
+ case .other: return "other"
141
+ default: return "unknown"
198
142
  }
199
143
  }
200
144
  }
@@ -203,32 +147,44 @@ private extension Transaction.RevocationReason {
203
147
  private extension Transaction.Reason {
204
148
  var descriptionString: String {
205
149
  switch self {
206
- case .purchase:
207
- return "purchase"
208
- case .renewal:
209
- return "renewal"
210
- default:
211
- return "unknown"
150
+ case .purchase: return "purchase"
151
+ case .renewal: return "renewal"
152
+ default: return "unknown"
153
+ }
154
+ }
155
+ }
156
+
157
+ private extension Transaction.OwnershipType {
158
+ var descriptionString: String {
159
+ switch self {
160
+ case .purchased: return "purchased"
161
+ case .familyShared: return "familyShared"
162
+ default: return "purchased"
163
+ }
164
+ }
165
+ }
166
+
167
+ @available(iOS 16.0, *)
168
+ private extension AppStore.Environment {
169
+ var descriptionString: String {
170
+ switch self {
171
+ case .sandbox: return "Sandbox"
172
+ case .production: return "Production"
173
+ case .xcode: return "Xcode"
174
+ default: return "Production"
212
175
  }
213
176
  }
214
177
  }
215
178
 
216
- @available(iOS 15.0, *)
217
179
  private extension Product.SubscriptionInfo.RenewalState {
218
180
  var descriptionString: String {
219
181
  switch self {
220
- case .subscribed:
221
- return "subscribed"
222
- case .expired:
223
- return "expired"
224
- case .revoked:
225
- return "revoked"
226
- case .inGracePeriod:
227
- return "inGracePeriod"
228
- case .inBillingRetryPeriod:
229
- return "inBillingRetryPeriod"
230
- default:
231
- return "unknown"
182
+ case .subscribed: return "subscribed"
183
+ case .expired: return "expired"
184
+ case .revoked: return "revoked"
185
+ case .inGracePeriod: return "inGracePeriod"
186
+ case .inBillingRetryPeriod: return "inBillingRetryPeriod"
187
+ default: return "unknown"
232
188
  }
233
189
  }
234
190
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-purchases",
3
- "version": "7.18.0-alpha.0",
3
+ "version": "7.18.0",
4
4
  "description": "In-app Subscriptions Made Easy",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",