react-native-iap 10.0.1 → 10.0.2

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.
Files changed (2) hide show
  1. package/ios/RNIapIos.swift +848 -848
  2. package/package.json +1 -1
@@ -4,967 +4,967 @@ import StoreKit
4
4
  typealias RNIapIosPromise = (RCTPromiseResolveBlock, RCTPromiseRejectBlock)
5
5
 
6
6
  public func debugMessage(_ object: Any...) {
7
- #if DEBUG
8
- for item in object {
9
- print("[react-native-iap] \(item)")
10
- }
11
- #endif
7
+ #if DEBUG
8
+ for item in object {
9
+ print("[react-native-iap] \(item)")
10
+ }
11
+ #endif
12
12
  }
13
13
 
14
14
  // Based on https://stackoverflow.com/a/40135192/570612
15
15
  extension Date {
16
- var millisecondsSince1970: Int64 {
17
- return Int64((self.timeIntervalSince1970 * 1000.0).rounded())
18
- }
16
+ var millisecondsSince1970: Int64 {
17
+ return Int64((self.timeIntervalSince1970 * 1000.0).rounded())
18
+ }
19
19
 
20
- var millisecondsSince1970String: String {
21
- return String((self.timeIntervalSince1970 * 1000.0).rounded())
22
- }
20
+ var millisecondsSince1970String: String {
21
+ return String((self.timeIntervalSince1970 * 1000.0).rounded())
22
+ }
23
23
 
24
- init(milliseconds: Int64) {
25
- self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
26
- }
24
+ init(milliseconds: Int64) {
25
+ self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
26
+ }
27
27
  }
28
28
 
29
29
  extension SKProductsRequest {
30
- var key: String {
31
- return String(self.hashValue)
32
- }
30
+ var key: String {
31
+ return String(self.hashValue)
32
+ }
33
33
  }
34
34
 
35
35
  @objc(RNIapIos)
36
36
  class RNIapIos: RCTEventEmitter, SKRequestDelegate, SKPaymentTransactionObserver, SKProductsRequestDelegate {
37
- private var promisesByKey: [String: [RNIapIosPromise]]
38
- private var myQueue: DispatchQueue
39
- private var hasListeners = false
40
- private var pendingTransactionWithAutoFinish = false
41
- private var receiptBlock: ((Data?, Error?) -> Void)? // Block to handle request the receipt async from delegate
42
- private var validProducts: [SKProduct]
43
- private var promotedPayment: SKPayment?
44
- private var promotedProduct: SKProduct?
45
- private var productsRequest: SKProductsRequest?
46
- private var countPendingTransaction: Int = 0
47
- private var hasTransactionObserver = false
48
-
49
- override init() {
50
- promisesByKey = [String: [RNIapIosPromise]]()
51
- pendingTransactionWithAutoFinish = false
52
- myQueue = DispatchQueue(label: "reject")
53
- validProducts = [SKProduct]()
54
- super.init()
55
- addTransactionObserver()
56
- }
57
-
58
- deinit {
59
- removeTransactionObserver()
60
- }
61
-
62
- override class func requiresMainQueueSetup() -> Bool {
63
- return true
64
- }
65
-
66
- func addTransactionObserver() {
67
- if !hasTransactionObserver {
68
- hasTransactionObserver = true
69
- SKPaymentQueue.default().add(self)
70
- }
71
- }
72
-
73
- func removeTransactionObserver() {
74
- if hasTransactionObserver {
75
- hasTransactionObserver = false
76
- SKPaymentQueue.default().remove(self)
77
- }
78
- }
79
-
80
- func flushUnheardEvents() {
81
- paymentQueue(SKPaymentQueue.default(), updatedTransactions: SKPaymentQueue.default().transactions)
82
- }
83
-
84
- override func startObserving() {
85
- hasListeners = true
86
- flushUnheardEvents()
87
- }
88
-
89
- override func stopObserving() {
90
- hasListeners = false
91
- }
92
-
93
- override func addListener(_ eventName: String?) {
94
- super.addListener(eventName)
95
-
96
- if (eventName == "iap-promoted-product") && promotedPayment != nil {
97
- sendEvent(withName: "iap-promoted-product", body: promotedPayment?.productIdentifier)
98
- }
99
- }
100
-
101
- func addPromise(forKey key: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
102
- var promises: [RNIapIosPromise]? = promisesByKey[key]
103
-
104
- if promises == nil {
105
- promises = []
106
- }
107
-
108
- promises?.append((resolve, reject))
109
- promisesByKey[key] = promises
110
- }
111
-
112
- func resolvePromises(forKey key: String?, value: Any?) {
113
- let promises: [RNIapIosPromise]? = promisesByKey[key ?? ""]
114
-
115
- if let promises = promises {
116
- for tuple in promises {
117
- let resolveBlck = tuple.0
118
- resolveBlck(value)
119
- }
120
- promisesByKey[key ?? ""] = nil
121
- }
122
- }
123
-
124
- func rejectPromises(forKey key: String, code: String?, message: String?, error: Error?) {
125
- let promises = promisesByKey[key]
126
-
127
- if let promises = promises {
128
- for tuple in promises {
129
- let reject = tuple.1
130
- reject(code, message, error)
131
- }
132
- promisesByKey[key] = nil
133
- }
134
- }
135
-
136
- func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
137
- promotedProduct = product
138
- promotedPayment = payment
139
-
140
- if hasListeners {
141
- sendEvent(withName: "iap-promoted-product", body: product.productIdentifier)
142
- }
143
- return false
144
- }
145
-
146
- override func supportedEvents() -> [String]? {
147
- return ["iap-promoted-product", "purchase-updated", "purchase-error"]
148
- }
149
-
150
- @objc public func initConnection(
151
- _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
152
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
153
- ) {
154
- addTransactionObserver()
155
- let canMakePayments = SKPaymentQueue.canMakePayments()
156
- resolve(NSNumber(value: canMakePayments))
157
- }
158
- @objc public func endConnection(
159
- _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
160
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
161
- ) {
162
- removeTransactionObserver()
163
- resolve(nil)
164
- }
165
- @objc public func getItems(
166
- _ skus: [String],
167
- resolve: @escaping RCTPromiseResolveBlock = { _ in },
168
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
169
- ) {
170
- let productIdentifiers = Set<AnyHashable>(skus)
171
- if let productIdentifiers = productIdentifiers as? Set<String> {
172
- productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
173
- if let productsRequest = productsRequest {
174
- productsRequest.delegate = self
175
- let key: String = productsRequest.key
176
- addPromise(forKey: key, resolve: resolve, reject: reject)
177
- productsRequest.start()
178
- }
179
- }
180
- }
181
- @objc public func getAvailableItems(
182
- _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
183
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
184
- ) {
185
- addPromise(forKey: "availableItems", resolve: resolve, reject: reject)
186
- SKPaymentQueue.default().restoreCompletedTransactions()
187
- }
188
-
189
- @objc public func buyProduct(
190
- _ sku: String,
191
- andDangerouslyFinishTransactionAutomatically: Bool,
192
- applicationUsername: String?,
193
- resolve: @escaping RCTPromiseResolveBlock = { _ in },
194
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
195
- ) {
196
- pendingTransactionWithAutoFinish = andDangerouslyFinishTransactionAutomatically
197
- var product: SKProduct?
198
- let lockQueue = DispatchQueue(label: "validProducts")
199
- lockQueue.sync {
200
- for p in validProducts {
201
- if sku == p.productIdentifier {
202
- product = p
203
- break
204
- }
205
- }
206
- }
207
- if let prod = product {
208
- addPromise(forKey: prod.productIdentifier, resolve: resolve, reject: reject)
209
-
210
- let payment = SKMutablePayment(product: prod)
211
-
212
- if let applicationUsername = applicationUsername {
213
- payment.applicationUsername = applicationUsername
214
- }
215
- SKPaymentQueue.default().add(payment)
216
- } else {
217
- if hasListeners {
218
- let err = [
219
- "debugMessage": "Invalid product ID.",
220
- "code": "E_DEVELOPER_ERROR",
221
- "message": "Invalid product ID.",
222
- "productId": sku
223
- ]
37
+ private var promisesByKey: [String: [RNIapIosPromise]]
38
+ private var myQueue: DispatchQueue
39
+ private var hasListeners = false
40
+ private var pendingTransactionWithAutoFinish = false
41
+ private var receiptBlock: ((Data?, Error?) -> Void)? // Block to handle request the receipt async from delegate
42
+ private var validProducts: [SKProduct]
43
+ private var promotedPayment: SKPayment?
44
+ private var promotedProduct: SKProduct?
45
+ private var productsRequest: SKProductsRequest?
46
+ private var countPendingTransaction: Int = 0
47
+ private var hasTransactionObserver = false
48
+
49
+ override init() {
50
+ promisesByKey = [String: [RNIapIosPromise]]()
51
+ pendingTransactionWithAutoFinish = false
52
+ myQueue = DispatchQueue(label: "reject")
53
+ validProducts = [SKProduct]()
54
+ super.init()
55
+ addTransactionObserver()
56
+ }
57
+
58
+ deinit {
59
+ removeTransactionObserver()
60
+ }
61
+
62
+ override class func requiresMainQueueSetup() -> Bool {
63
+ return true
64
+ }
65
+
66
+ func addTransactionObserver() {
67
+ if !hasTransactionObserver {
68
+ hasTransactionObserver = true
69
+ SKPaymentQueue.default().add(self)
70
+ }
71
+ }
224
72
 
225
- sendEvent(withName: "purchase-error", body: err)
226
- }
227
-
228
- reject("E_DEVELOPER_ERROR", "Invalid product ID.", nil)
229
- }
230
- }
231
-
232
- @objc public func buyProductWithOffer(
233
- _ sku: String,
234
- forUser usernameHash: String,
235
- withOffer discountOffer: [String: String],
236
- resolve: @escaping RCTPromiseResolveBlock = { _ in },
237
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
238
- ) {
239
- var product: SKProduct?
240
- let lockQueue = DispatchQueue(label: "validProducts")
241
- lockQueue.sync {
242
- for p in validProducts {
243
- if sku == p.productIdentifier {
244
- product = p
245
- break
246
- }
247
- }
248
- }
249
-
250
- if let prod = product {
251
- addPromise(forKey: prod.productIdentifier, resolve: resolve, reject: reject)
252
-
253
- let payment = SKMutablePayment(product: prod)
254
-
255
- if #available(iOS 12.2, tvOS 12.2, *) {
256
- let discount = SKPaymentDiscount(
257
- identifier: discountOffer["identifier"]!,
258
- keyIdentifier: discountOffer["keyIdentifier"]!,
259
- nonce: UUID(uuidString: discountOffer["nonce"]!)!,
260
- signature: discountOffer["signature"]!,
261
- timestamp: NSNumber(value: Int(discountOffer["timestamp"]!)!))
262
- payment.paymentDiscount = discount
263
- }
264
- payment.applicationUsername = usernameHash
265
- SKPaymentQueue.default().add(payment)
266
- } else {
267
- if hasListeners {
268
- let err = [
269
- "debugMessage": "Invalid product ID.",
270
- "message": "Invalid product ID.",
271
- "code": "E_DEVELOPER_ERROR",
272
- "productId": sku
273
- ]
274
- sendEvent(withName: "purchase-error", body: err)
275
- }
276
- reject("E_DEVELOPER_ERROR", "Invalid product ID.", nil)
277
- }
278
- }
279
-
280
- @objc public func buyProductWithQuantityIOS(
281
- _ sku: String,
282
- quantity: Int,
283
- resolve: @escaping RCTPromiseResolveBlock = { _ in },
284
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
285
- ) {
286
- debugMessage("buyProductWithQuantityIOS")
287
- var product: SKProduct?
288
- let lockQueue = DispatchQueue(label: "validProducts")
289
- lockQueue.sync {
290
- for p in validProducts {
291
- if sku == p.productIdentifier {
292
- product = p
293
- break
294
- }
295
- }
296
- }
297
- if let prod = product {
298
- let payment = SKMutablePayment(product: prod)
299
- payment.quantity = quantity
300
- SKPaymentQueue.default().add(payment)
301
- } else {
302
- if hasListeners {
303
- let err = [
304
- "debugMessage": "Invalid product ID.",
305
- "message": "Invalid product ID.",
306
- "code": "E_DEVELOPER_ERROR",
307
- "productId": sku
308
- ]
309
- sendEvent(withName: "purchase-error", body: err)
310
- }
311
- reject("E_DEVELOPER_ERROR", "Invalid product ID.", nil)
312
- }
313
- }
314
-
315
- @objc public func clearTransaction(
316
- _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
317
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
318
- ) {
319
- let pendingTrans = SKPaymentQueue.default().transactions
320
- countPendingTransaction = pendingTrans.count
321
-
322
- debugMessage("clear remaining Transactions (\(countPendingTransaction)). Call this before make a new transaction")
323
-
324
- if countPendingTransaction > 0 {
325
- addPromise(forKey: "cleaningTransactions", resolve: resolve, reject: reject)
326
- for transaction in pendingTrans {
327
- SKPaymentQueue.default().finishTransaction(transaction)
328
- }
329
- } else {
330
- resolve(nil)
331
- }
332
- }
333
-
334
- @objc public func clearProducts(
335
- _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
336
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
337
- ) {
338
- debugMessage("clear valid products")
339
-
340
- let lockQueue = DispatchQueue(label: "validProducts")
341
-
342
- lockQueue.sync {
343
- validProducts.removeAll()
344
- }
345
-
346
- resolve(nil)
347
- }
348
-
349
- @objc public func promotedProduct(
350
- _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
351
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
352
- ) {
353
- debugMessage("get promoted product")
354
- resolve((promotedProduct != nil) ? getProductObject(promotedProduct!) : nil)
355
- }
356
-
357
- @objc public func buyPromotedProduct(
358
- _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
359
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
360
- ) {
361
- if let promoPayment = promotedPayment {
362
- debugMessage("buy promoted product")
363
- SKPaymentQueue.default().add(promoPayment)
364
- } else {
365
- reject("E_DEVELOPER_ERROR", "Invalid product ID.", nil)
366
- }
367
- }
368
-
369
- @objc public func requestReceipt(
370
- _ refresh: Bool,
371
- resolve: @escaping RCTPromiseResolveBlock = { _ in },
372
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
373
- ) {
374
- requestReceiptData(withBlock: refresh) { [self] receiptData, error in
375
- if error == nil {
376
- resolve(receiptData?.base64EncodedString(options: []))
377
- } else {
378
- reject(standardErrorCode(9), "Invalid receipt", nil)
379
- }
380
- }
381
- }
382
-
383
- @objc public func finishTransaction(
384
- _ transactionIdentifier: String,
385
- resolve: @escaping RCTPromiseResolveBlock = { _ in },
386
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
387
- ) {
388
- finishTransaction(withIdentifier: transactionIdentifier)
389
- resolve(nil)
390
- }
391
-
392
- @objc public func getPendingTransactions (
393
- _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
394
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
395
- ) {
396
- requestReceiptData(withBlock: false) { receiptData, _ in
397
- var output: [AnyHashable] = []
398
-
399
- if let receipt = receiptData {
400
- let transactions = SKPaymentQueue.default().transactions
401
-
402
- for item in transactions {
403
- let timestamp = item.transactionDate?.millisecondsSince1970 == nil ? nil : String(item.transactionDate!.millisecondsSince1970)
404
- let purchase = [
405
- "transactionDate": timestamp,
406
- "transactionId": item.transactionIdentifier,
407
- "productId": item.payment.productIdentifier,
408
- "quantity": "\(item.payment.quantity)",
409
- "transactionReceipt": receipt.base64EncodedString(options: [])
410
- ]
411
-
412
- output.append(purchase)
413
- }
414
- }
415
-
416
- resolve(output)
417
- }
418
- }
419
-
420
- @objc public func presentCodeRedemptionSheet(
421
- _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
422
- reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
423
- ) {
424
- #if !os(tvOS)
425
- if #available(iOS 14.0, tvOS 14.0, *) {
426
- SKPaymentQueue.default().presentCodeRedemptionSheet()
427
- resolve(nil)
428
- } else {
429
- reject(standardErrorCode(2), "This method only available above iOS 14", nil)
430
- }
431
- #else
432
- reject(standardErrorCode(2), "This method is not available on tvOS", nil)
433
- #endif
434
- }
73
+ func removeTransactionObserver() {
74
+ if hasTransactionObserver {
75
+ hasTransactionObserver = false
76
+ SKPaymentQueue.default().remove(self)
77
+ }
78
+ }
435
79
 
436
- // StoreKitDelegate
437
- func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
438
- for prod in response.products {
439
- add(prod)
80
+ func flushUnheardEvents() {
81
+ paymentQueue(SKPaymentQueue.default(), updatedTransactions: SKPaymentQueue.default().transactions)
440
82
  }
441
83
 
442
- var items: [[String: Any?]] = [[:]]
443
- let lockQueue = DispatchQueue(label: "validProducts")
84
+ override func startObserving() {
85
+ hasListeners = true
86
+ flushUnheardEvents()
87
+ }
444
88
 
445
- lockQueue.sync {
446
- for product in validProducts {
447
- items.append(getProductObject(product))
448
- }
89
+ override func stopObserving() {
90
+ hasListeners = false
449
91
  }
450
92
 
451
- resolvePromises(forKey: request.key, value: items)
452
- }
93
+ override func addListener(_ eventName: String?) {
94
+ super.addListener(eventName)
453
95
 
454
- // Add to valid products from Apple server response. Allowing getProducts, getSubscriptions call several times.
455
- // Doesn't allow duplication. Replace new product.
456
- func add(_ aProd: SKProduct) {
457
- let lockQueue = DispatchQueue(label: "validProducts")
96
+ if (eventName == "iap-promoted-product") && promotedPayment != nil {
97
+ sendEvent(withName: "iap-promoted-product", body: promotedPayment?.productIdentifier)
98
+ }
99
+ }
458
100
 
459
- lockQueue.sync {
460
- debugMessage("Add new object: \(aProd.productIdentifier)")
461
- var delTar = -1
101
+ func addPromise(forKey key: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
102
+ var promises: [RNIapIosPromise]? = promisesByKey[key]
462
103
 
463
- for k in 0..<validProducts.count {
464
- let cur = validProducts[k]
465
- if cur.productIdentifier == aProd.productIdentifier {
466
- delTar = k
104
+ if promises == nil {
105
+ promises = []
467
106
  }
468
- }
469
-
470
- if delTar >= 0 {
471
- validProducts.remove(at: delTar)
472
- }
473
107
 
474
- validProducts.append(aProd)
108
+ promises?.append((resolve, reject))
109
+ promisesByKey[key] = promises
475
110
  }
476
- }
477
111
 
478
- func request(_ request: SKRequest, didFailWithError error: Error) {
479
- let nsError = error as NSError
112
+ func resolvePromises(forKey key: String?, value: Any?) {
113
+ let promises: [RNIapIosPromise]? = promisesByKey[key ?? ""]
480
114
 
481
- if request is SKReceiptRefreshRequest {
482
- if let unwrappedReceiptBlock = receiptBlock {
483
- let standardError = NSError(domain: nsError.domain, code: 9, userInfo: nsError.userInfo)
484
- unwrappedReceiptBlock(nil, standardError)
485
- receiptBlock = nil
486
- return
487
- } else {
488
- if let key: String = productsRequest?.key {
489
- myQueue.sync(execute: { [self] in
490
- rejectPromises(
491
- forKey: key,
492
- code: standardErrorCode(nsError.code),
493
- message: error.localizedDescription,
494
- error: error)}
495
- )
115
+ if let promises = promises {
116
+ for tuple in promises {
117
+ let resolveBlck = tuple.0
118
+ resolveBlck(value)
119
+ }
120
+ promisesByKey[key ?? ""] = nil
496
121
  }
497
- }
498
122
  }
499
- }
500
123
 
501
- func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
502
- for transaction in transactions {
503
- switch transaction.transactionState {
504
- case .purchasing:
505
- debugMessage("Purchase Started")
506
- break
124
+ func rejectPromises(forKey key: String, code: String?, message: String?, error: Error?) {
125
+ let promises = promisesByKey[key]
507
126
 
508
- case .purchased:
509
- debugMessage("Purchase Successful")
510
- purchaseProcess(transaction)
511
- break
512
-
513
- case .restored:
514
- debugMessage("Restored")
515
- SKPaymentQueue.default().finishTransaction(transaction)
516
- break
127
+ if let promises = promises {
128
+ for tuple in promises {
129
+ let reject = tuple.1
130
+ reject(code, message, error)
131
+ }
132
+ promisesByKey[key] = nil
133
+ }
134
+ }
517
135
 
518
- case .deferred:
519
- debugMessage("Deferred (awaiting approval via parental controls, etc.)")
136
+ func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
137
+ promotedProduct = product
138
+ promotedPayment = payment
520
139
 
521
- myQueue.sync(execute: { [self] in
522
- if hasListeners {
523
- let err = [
524
- "debugMessage": "The payment was deferred (awaiting approval via parental controls for instance)",
525
- "code": "E_DEFERRED_PAYMENT",
526
- "message": "The payment was deferred (awaiting approval via parental controls for instance)",
527
- "productId": transaction.payment.productIdentifier,
528
- "quantity": "\(transaction.payment.quantity)"
529
- ]
530
-
531
- sendEvent(withName: "purchase-error", body: err)
532
- }
533
-
534
- rejectPromises(
535
- forKey: transaction.payment.productIdentifier,
536
- code: "E_DEFERRED_PAYMENT",
537
- message: "The payment was deferred (awaiting approval via parental controls for instance)",
538
- error: nil)
539
- })
140
+ if hasListeners {
141
+ sendEvent(withName: "iap-promoted-product", body: product.productIdentifier)
142
+ }
143
+ return false
144
+ }
145
+
146
+ override func supportedEvents() -> [String]? {
147
+ return ["iap-promoted-product", "purchase-updated", "purchase-error"]
148
+ }
149
+
150
+ @objc public func initConnection(
151
+ _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
152
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
153
+ ) {
154
+ addTransactionObserver()
155
+ let canMakePayments = SKPaymentQueue.canMakePayments()
156
+ resolve(NSNumber(value: canMakePayments))
157
+ }
158
+ @objc public func endConnection(
159
+ _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
160
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
161
+ ) {
162
+ removeTransactionObserver()
163
+ resolve(nil)
164
+ }
165
+ @objc public func getItems(
166
+ _ skus: [String],
167
+ resolve: @escaping RCTPromiseResolveBlock = { _ in },
168
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
169
+ ) {
170
+ let productIdentifiers = Set<AnyHashable>(skus)
171
+ if let productIdentifiers = productIdentifiers as? Set<String> {
172
+ productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
173
+ if let productsRequest = productsRequest {
174
+ productsRequest.delegate = self
175
+ let key: String = productsRequest.key
176
+ addPromise(forKey: key, resolve: resolve, reject: reject)
177
+ productsRequest.start()
178
+ }
179
+ }
180
+ }
181
+ @objc public func getAvailableItems(
182
+ _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
183
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
184
+ ) {
185
+ addPromise(forKey: "availableItems", resolve: resolve, reject: reject)
186
+ SKPaymentQueue.default().restoreCompletedTransactions()
187
+ }
188
+
189
+ @objc public func buyProduct(
190
+ _ sku: String,
191
+ andDangerouslyFinishTransactionAutomatically: Bool,
192
+ applicationUsername: String?,
193
+ resolve: @escaping RCTPromiseResolveBlock = { _ in },
194
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
195
+ ) {
196
+ pendingTransactionWithAutoFinish = andDangerouslyFinishTransactionAutomatically
197
+ var product: SKProduct?
198
+ let lockQueue = DispatchQueue(label: "validProducts")
199
+ lockQueue.sync {
200
+ for p in validProducts {
201
+ if sku == p.productIdentifier {
202
+ product = p
203
+ break
204
+ }
205
+ }
206
+ }
207
+ if let prod = product {
208
+ addPromise(forKey: prod.productIdentifier, resolve: resolve, reject: reject)
540
209
 
541
- case .failed:
542
- debugMessage("Purchase Failed")
210
+ let payment = SKMutablePayment(product: prod)
543
211
 
544
- SKPaymentQueue.default().finishTransaction(transaction)
212
+ if let applicationUsername = applicationUsername {
213
+ payment.applicationUsername = applicationUsername
214
+ }
215
+ SKPaymentQueue.default().add(payment)
216
+ } else {
217
+ if hasListeners {
218
+ let err = [
219
+ "debugMessage": "Invalid product ID.",
220
+ "code": "E_DEVELOPER_ERROR",
221
+ "message": "Invalid product ID.",
222
+ "productId": sku
223
+ ]
224
+
225
+ sendEvent(withName: "purchase-error", body: err)
226
+ }
227
+
228
+ reject("E_DEVELOPER_ERROR", "Invalid product ID.", nil)
229
+ }
230
+ }
545
231
 
546
- myQueue.sync(execute: { [self] in
547
- let nsError = transaction.error as NSError?
548
-
549
- if hasListeners {
550
- let code = nsError?.code
551
- let responseCode = String(code ?? 0)
552
- let err = [
553
- "responseCode": responseCode,
554
- "debugMessage": transaction.error?.localizedDescription,
555
- "code": standardErrorCode(code),
556
- "message": transaction.error?.localizedDescription,
557
- "productId": transaction.payment.productIdentifier
558
- ]
559
-
560
- sendEvent(withName: "purchase-error", body: err)
561
- }
562
-
563
- rejectPromises(
564
- forKey: transaction.payment.productIdentifier,
565
- code: standardErrorCode(nsError?.code),
566
- message: nsError?.localizedDescription,
567
- error: nsError)
568
- })
232
+ @objc public func buyProductWithOffer(
233
+ _ sku: String,
234
+ forUser usernameHash: String,
235
+ withOffer discountOffer: [String: String],
236
+ resolve: @escaping RCTPromiseResolveBlock = { _ in },
237
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
238
+ ) {
239
+ var product: SKProduct?
240
+ let lockQueue = DispatchQueue(label: "validProducts")
241
+ lockQueue.sync {
242
+ for p in validProducts {
243
+ if sku == p.productIdentifier {
244
+ product = p
245
+ break
246
+ }
247
+ }
248
+ }
569
249
 
570
- break
571
- }
250
+ if let prod = product {
251
+ addPromise(forKey: prod.productIdentifier, resolve: resolve, reject: reject)
252
+
253
+ let payment = SKMutablePayment(product: prod)
254
+
255
+ if #available(iOS 12.2, tvOS 12.2, *) {
256
+ let discount = SKPaymentDiscount(
257
+ identifier: discountOffer["identifier"]!,
258
+ keyIdentifier: discountOffer["keyIdentifier"]!,
259
+ nonce: UUID(uuidString: discountOffer["nonce"]!)!,
260
+ signature: discountOffer["signature"]!,
261
+ timestamp: NSNumber(value: Int(discountOffer["timestamp"]!)!))
262
+ payment.paymentDiscount = discount
263
+ }
264
+ payment.applicationUsername = usernameHash
265
+ SKPaymentQueue.default().add(payment)
266
+ } else {
267
+ if hasListeners {
268
+ let err = [
269
+ "debugMessage": "Invalid product ID.",
270
+ "message": "Invalid product ID.",
271
+ "code": "E_DEVELOPER_ERROR",
272
+ "productId": sku
273
+ ]
274
+ sendEvent(withName: "purchase-error", body: err)
275
+ }
276
+ reject("E_DEVELOPER_ERROR", "Invalid product ID.", nil)
277
+ }
572
278
  }
573
- }
574
-
575
- func finishTransaction(withIdentifier transactionIdentifier: String?) {
576
- let queue = SKPaymentQueue.default()
577
279
 
578
- for transaction in queue.transactions {
579
- if transaction.transactionIdentifier == transactionIdentifier {
580
- SKPaymentQueue.default().finishTransaction(transaction)
581
- }
280
+ @objc public func buyProductWithQuantityIOS(
281
+ _ sku: String,
282
+ quantity: Int,
283
+ resolve: @escaping RCTPromiseResolveBlock = { _ in },
284
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
285
+ ) {
286
+ debugMessage("buyProductWithQuantityIOS")
287
+ var product: SKProduct?
288
+ let lockQueue = DispatchQueue(label: "validProducts")
289
+ lockQueue.sync {
290
+ for p in validProducts {
291
+ if sku == p.productIdentifier {
292
+ product = p
293
+ break
294
+ }
295
+ }
296
+ }
297
+ if let prod = product {
298
+ let payment = SKMutablePayment(product: prod)
299
+ payment.quantity = quantity
300
+ SKPaymentQueue.default().add(payment)
301
+ } else {
302
+ if hasListeners {
303
+ let err = [
304
+ "debugMessage": "Invalid product ID.",
305
+ "message": "Invalid product ID.",
306
+ "code": "E_DEVELOPER_ERROR",
307
+ "productId": sku
308
+ ]
309
+ sendEvent(withName: "purchase-error", body: err)
310
+ }
311
+ reject("E_DEVELOPER_ERROR", "Invalid product ID.", nil)
312
+ }
582
313
  }
583
- }
584
314
 
585
- func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
586
- debugMessage("PaymentQueueRestoreCompletedTransactionsFinished")
587
- var items = [[String: Any?]]()
315
+ @objc public func clearTransaction(
316
+ _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
317
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
318
+ ) {
319
+ let pendingTrans = SKPaymentQueue.default().transactions
320
+ countPendingTransaction = pendingTrans.count
588
321
 
589
- for transaction in queue.transactions {
590
- if transaction.transactionState == .restored || transaction.transactionState == .purchased {
591
- getPurchaseData(transaction) { restored in
592
- if let restored = restored {
593
- items.append(restored)
594
- }
322
+ debugMessage("clear remaining Transactions (\(countPendingTransaction)). Call this before make a new transaction")
595
323
 
596
- SKPaymentQueue.default().finishTransaction(transaction)
324
+ if countPendingTransaction > 0 {
325
+ addPromise(forKey: "cleaningTransactions", resolve: resolve, reject: reject)
326
+ for transaction in pendingTrans {
327
+ SKPaymentQueue.default().finishTransaction(transaction)
328
+ }
329
+ } else {
330
+ resolve(nil)
597
331
  }
598
- }
599
332
  }
600
333
 
601
- resolvePromises(forKey: "availableItems", value: items)
602
- }
334
+ @objc public func clearProducts(
335
+ _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
336
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
337
+ ) {
338
+ debugMessage("clear valid products")
603
339
 
604
- func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
605
- myQueue.sync(execute: { [self] in
606
- rejectPromises(
607
- forKey: "availableItems",
608
- code: standardErrorCode((error as NSError).code),
609
- message: error.localizedDescription,
610
- error: error)
611
- })
340
+ let lockQueue = DispatchQueue(label: "validProducts")
612
341
 
613
- debugMessage("restoreCompletedTransactionsFailedWithError")
614
- }
342
+ lockQueue.sync {
343
+ validProducts.removeAll()
344
+ }
615
345
 
616
- func purchaseProcess(_ transaction: SKPaymentTransaction) {
617
- if pendingTransactionWithAutoFinish {
618
- SKPaymentQueue.default().finishTransaction(transaction)
619
- pendingTransactionWithAutoFinish = false
346
+ resolve(nil)
620
347
  }
621
348
 
622
- getPurchaseData(transaction) { [self] purchase in
623
- resolvePromises(forKey: transaction.payment.productIdentifier, value: purchase)
624
-
625
- // additionally send event
626
- if hasListeners {
627
- sendEvent(withName: "purchase-updated", body: purchase)
628
- }
349
+ @objc public func promotedProduct(
350
+ _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
351
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
352
+ ) {
353
+ debugMessage("get promoted product")
354
+ resolve((promotedProduct != nil) ? getProductObject(promotedProduct!) : nil)
629
355
  }
630
- }
631
356
 
632
- func standardErrorCode(_ code: Int?) -> String? {
633
- let descriptions = [
634
- "E_UNKNOWN",
635
- "E_SERVICE_ERROR",
636
- "E_USER_CANCELLED",
637
- "E_USER_ERROR",
638
- "E_USER_ERROR",
639
- "E_ITEM_UNAVAILABLE",
640
- "E_REMOTE_ERROR",
641
- "E_NETWORK_ERROR",
642
- "E_SERVICE_ERROR",
643
- "E_RECEIPT_FAILED",
644
- "E_RECEIPT_FINISHED_FAILED"
645
- ]
357
+ @objc public func buyPromotedProduct(
358
+ _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
359
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
360
+ ) {
361
+ if let promoPayment = promotedPayment {
362
+ debugMessage("buy promoted product")
363
+ SKPaymentQueue.default().add(promoPayment)
364
+ } else {
365
+ reject("E_DEVELOPER_ERROR", "Invalid product ID.", nil)
366
+ }
367
+ }
646
368
 
647
- guard let code = code else {
648
- return descriptions[0]
369
+ @objc public func requestReceipt(
370
+ _ refresh: Bool,
371
+ resolve: @escaping RCTPromiseResolveBlock = { _ in },
372
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
373
+ ) {
374
+ requestReceiptData(withBlock: refresh) { [self] receiptData, error in
375
+ if error == nil {
376
+ resolve(receiptData?.base64EncodedString(options: []))
377
+ } else {
378
+ reject(standardErrorCode(9), "Invalid receipt", nil)
379
+ }
380
+ }
649
381
  }
650
382
 
651
- if code > descriptions.count - 1 || code < 0 { // Fix crash app without internet connection
652
- return descriptions[0]
383
+ @objc public func finishTransaction(
384
+ _ transactionIdentifier: String,
385
+ resolve: @escaping RCTPromiseResolveBlock = { _ in },
386
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
387
+ ) {
388
+ finishTransaction(withIdentifier: transactionIdentifier)
389
+ resolve(nil)
390
+ }
391
+
392
+ @objc public func getPendingTransactions (
393
+ _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
394
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
395
+ ) {
396
+ requestReceiptData(withBlock: false) { receiptData, _ in
397
+ var output: [AnyHashable] = []
398
+
399
+ if let receipt = receiptData {
400
+ let transactions = SKPaymentQueue.default().transactions
401
+
402
+ for item in transactions {
403
+ let timestamp = item.transactionDate?.millisecondsSince1970 == nil ? nil : String(item.transactionDate!.millisecondsSince1970)
404
+ let purchase = [
405
+ "transactionDate": timestamp,
406
+ "transactionId": item.transactionIdentifier,
407
+ "productId": item.payment.productIdentifier,
408
+ "quantity": "\(item.payment.quantity)",
409
+ "transactionReceipt": receipt.base64EncodedString(options: [])
410
+ ]
411
+
412
+ output.append(purchase)
413
+ }
414
+ }
415
+
416
+ resolve(output)
417
+ }
653
418
  }
654
419
 
655
- return descriptions[code]
656
- }
420
+ @objc public func presentCodeRedemptionSheet(
421
+ _ resolve: @escaping RCTPromiseResolveBlock = { _ in },
422
+ reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
423
+ ) {
424
+ #if !os(tvOS)
425
+ if #available(iOS 14.0, tvOS 14.0, *) {
426
+ SKPaymentQueue.default().presentCodeRedemptionSheet()
427
+ resolve(nil)
428
+ } else {
429
+ reject(standardErrorCode(2), "This method only available above iOS 14", nil)
430
+ }
431
+ #else
432
+ reject(standardErrorCode(2), "This method is not available on tvOS", nil)
433
+ #endif
434
+ }
657
435
 
658
- func getProductObject(_ product: SKProduct) -> [String: Any?] {
659
- let formatter = NumberFormatter()
660
- formatter.numberStyle = .currency
661
- formatter.locale = product.priceLocale
436
+ // StoreKitDelegate
437
+ func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
438
+ for prod in response.products {
439
+ add(prod)
440
+ }
662
441
 
663
- let localizedPrice = formatter.string(from: product.price)
664
- var introductoryPrice = localizedPrice
665
- var introductoryPriceAsAmountIOS = "\(product.price)"
442
+ var items: [[String: Any?]] = [[:]]
443
+ let lockQueue = DispatchQueue(label: "validProducts")
666
444
 
667
- var introductoryPricePaymentMode = ""
668
- var introductoryPriceNumberOfPeriods = ""
445
+ lockQueue.sync {
446
+ for product in validProducts {
447
+ items.append(getProductObject(product))
448
+ }
449
+ }
669
450
 
670
- var introductoryPriceSubscriptionPeriod = ""
451
+ resolvePromises(forKey: request.key, value: items)
452
+ }
671
453
 
672
- var currencyCode: String? = ""
673
- var countryCode: String? = ""
674
- var periodNumberIOS = "0"
675
- var periodUnitIOS = ""
676
- var itemType = "iap"
454
+ // Add to valid products from Apple server response. Allowing getProducts, getSubscriptions call several times.
455
+ // Doesn't allow duplication. Replace new product.
456
+ func add(_ aProd: SKProduct) {
457
+ let lockQueue = DispatchQueue(label: "validProducts")
677
458
 
678
- if #available(iOS 11.2, tvOS 11.2, *) {
679
- let numOfUnits = UInt(product.subscriptionPeriod?.numberOfUnits ?? 0)
680
- let unit = product.subscriptionPeriod?.unit
459
+ lockQueue.sync {
460
+ debugMessage("Add new object: \(aProd.productIdentifier)")
461
+ var delTar = -1
681
462
 
682
- if unit == .year {
683
- periodUnitIOS = "YEAR"
684
- } else if unit == .month {
685
- periodUnitIOS = "MONTH"
686
- } else if unit == .week {
687
- periodUnitIOS = "WEEK"
688
- } else if unit == .day {
689
- periodUnitIOS = "DAY"
690
- }
463
+ for k in 0..<validProducts.count {
464
+ let cur = validProducts[k]
465
+ if cur.productIdentifier == aProd.productIdentifier {
466
+ delTar = k
467
+ }
468
+ }
691
469
 
692
- periodNumberIOS = String(format: "%lu", numOfUnits)
693
- if numOfUnits != 0 {
694
- itemType = "subs"
695
- }
470
+ if delTar >= 0 {
471
+ validProducts.remove(at: delTar)
472
+ }
696
473
 
697
- // subscriptionPeriod = product.subscriptionPeriod ? [product.subscriptionPeriod stringValue] : @"";
698
- // introductoryPrice = product.introductoryPrice != nil ? [NSString stringWithFormat:@"%@", product.introductoryPrice] : @"";
699
- if product.introductoryPrice != nil {
700
- formatter.locale = product.introductoryPrice?.priceLocale
701
-
702
- if let price = product.introductoryPrice?.price {
703
- introductoryPrice = formatter.string(from: price)
704
- }
705
-
706
- introductoryPriceAsAmountIOS = product.introductoryPrice?.price.stringValue ?? ""
707
-
708
- switch product.introductoryPrice?.paymentMode {
709
- case .freeTrial:
710
- introductoryPricePaymentMode = "FREETRIAL"
711
- introductoryPriceNumberOfPeriods = NSNumber(value: product.introductoryPrice?.subscriptionPeriod.numberOfUnits ?? 0).stringValue
474
+ validProducts.append(aProd)
475
+ }
476
+ }
712
477
 
713
- case .payAsYouGo:
714
- introductoryPricePaymentMode = "PAYASYOUGO"
715
- introductoryPriceNumberOfPeriods = NSNumber(value: product.introductoryPrice?.numberOfPeriods ?? 0).stringValue
716
-
717
- case .payUpFront:
718
- introductoryPricePaymentMode = "PAYUPFRONT"
719
- introductoryPriceNumberOfPeriods = NSNumber(value: product.introductoryPrice?.subscriptionPeriod.numberOfUnits ?? 0).stringValue
478
+ func request(_ request: SKRequest, didFailWithError error: Error) {
479
+ let nsError = error as NSError
480
+
481
+ if request is SKReceiptRefreshRequest {
482
+ if let unwrappedReceiptBlock = receiptBlock {
483
+ let standardError = NSError(domain: nsError.domain, code: 9, userInfo: nsError.userInfo)
484
+ unwrappedReceiptBlock(nil, standardError)
485
+ receiptBlock = nil
486
+ return
487
+ } else {
488
+ if let key: String = productsRequest?.key {
489
+ myQueue.sync(execute: { [self] in
490
+ rejectPromises(
491
+ forKey: key,
492
+ code: standardErrorCode(nsError.code),
493
+ message: error.localizedDescription,
494
+ error: error)}
495
+ )
496
+ }
497
+ }
498
+ }
499
+ }
720
500
 
721
- default:
722
- introductoryPricePaymentMode = ""
723
- introductoryPriceNumberOfPeriods = "0"
724
- }
725
-
726
- if product.introductoryPrice?.subscriptionPeriod.unit == .day {
727
- introductoryPriceSubscriptionPeriod = "DAY"
728
- } else if product.introductoryPrice?.subscriptionPeriod.unit == .week {
729
- introductoryPriceSubscriptionPeriod = "WEEK"
730
- } else if product.introductoryPrice?.subscriptionPeriod.unit == .month {
731
- introductoryPriceSubscriptionPeriod = "MONTH"
732
- } else if product.introductoryPrice?.subscriptionPeriod.unit == .year {
733
- introductoryPriceSubscriptionPeriod = "YEAR"
734
- } else {
735
- introductoryPriceSubscriptionPeriod = ""
736
- }
737
- } else {
738
- introductoryPrice = ""
739
- introductoryPriceAsAmountIOS = ""
740
- introductoryPricePaymentMode = ""
741
- introductoryPriceNumberOfPeriods = ""
742
- introductoryPriceSubscriptionPeriod = ""
743
- }
744
- }
745
-
746
- if #available(iOS 10.0, tvOS 10.0, *) {
747
- currencyCode = product.priceLocale.currencyCode
748
- }
749
-
750
- if #available(iOS 13.0, tvOS 13.0, *) {
751
- countryCode = SKPaymentQueue.default().storefront?.countryCode
752
- } else if #available(iOS 10.0, tvOS 10.0, *) {
753
- countryCode = product.priceLocale.regionCode
754
- }
755
-
756
- var discounts: [[String: String?]]?
757
-
758
- if #available(iOS 12.2, tvOS 12.2, *) {
759
- discounts = getDiscountData(product)
760
- }
761
-
762
- let obj: [String: Any?] = [
763
- "productId": product.productIdentifier,
764
- "price": "\(product.price)",
765
- "currency": currencyCode,
766
- "countryCode": countryCode ?? "",
767
- "type": itemType,
768
- "title": product.localizedTitle != "" ? product.localizedTitle : "",
769
- "description": product.localizedDescription != "" ? product.localizedDescription : "",
770
- "localizedPrice": localizedPrice,
771
- "subscriptionPeriodNumberIOS": periodNumberIOS,
772
- "subscriptionPeriodUnitIOS": periodUnitIOS,
773
- "introductoryPrice": introductoryPrice,
774
- "introductoryPriceAsAmountIOS": introductoryPriceAsAmountIOS,
775
- "introductoryPricePaymentModeIOS": introductoryPricePaymentMode,
776
- "introductoryPriceNumberOfPeriodsIOS": introductoryPriceNumberOfPeriods,
777
- "introductoryPriceSubscriptionPeriodIOS": introductoryPriceSubscriptionPeriod,
778
- "discounts": discounts
779
- ]
780
-
781
- return obj
782
- }
783
-
784
- func getDiscountData(_ product: SKProduct) -> [[String: String?]]? {
785
- if #available(iOS 12.2, tvOS 12.2, *) {
786
- var mappedDiscounts: [[String: String?]] = []
787
- var localizedPrice: String?
788
- var paymendMode: String?
789
- var subscriptionPeriods: String?
790
- var discountType: String?
791
-
792
- for discount in product.discounts {
793
- let formatter = NumberFormatter()
794
- formatter.numberStyle = .currency
795
- let priceLocale: Locale? = discount.priceLocale
796
- if let pLocale = priceLocale {
797
- formatter.locale = pLocale
501
+ func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
502
+ for transaction in transactions {
503
+ switch transaction.transactionState {
504
+ case .purchasing:
505
+ debugMessage("Purchase Started")
506
+ break
507
+
508
+ case .purchased:
509
+ debugMessage("Purchase Successful")
510
+ purchaseProcess(transaction)
511
+ break
512
+
513
+ case .restored:
514
+ debugMessage("Restored")
515
+ SKPaymentQueue.default().finishTransaction(transaction)
516
+ break
517
+
518
+ case .deferred:
519
+ debugMessage("Deferred (awaiting approval via parental controls, etc.)")
520
+
521
+ myQueue.sync(execute: { [self] in
522
+ if hasListeners {
523
+ let err = [
524
+ "debugMessage": "The payment was deferred (awaiting approval via parental controls for instance)",
525
+ "code": "E_DEFERRED_PAYMENT",
526
+ "message": "The payment was deferred (awaiting approval via parental controls for instance)",
527
+ "productId": transaction.payment.productIdentifier,
528
+ "quantity": "\(transaction.payment.quantity)"
529
+ ]
530
+
531
+ sendEvent(withName: "purchase-error", body: err)
532
+ }
533
+
534
+ rejectPromises(
535
+ forKey: transaction.payment.productIdentifier,
536
+ code: "E_DEFERRED_PAYMENT",
537
+ message: "The payment was deferred (awaiting approval via parental controls for instance)",
538
+ error: nil)
539
+ })
540
+
541
+ case .failed:
542
+ debugMessage("Purchase Failed")
543
+
544
+ SKPaymentQueue.default().finishTransaction(transaction)
545
+
546
+ myQueue.sync(execute: { [self] in
547
+ let nsError = transaction.error as NSError?
548
+
549
+ if hasListeners {
550
+ let code = nsError?.code
551
+ let responseCode = String(code ?? 0)
552
+ let err = [
553
+ "responseCode": responseCode,
554
+ "debugMessage": transaction.error?.localizedDescription,
555
+ "code": standardErrorCode(code),
556
+ "message": transaction.error?.localizedDescription,
557
+ "productId": transaction.payment.productIdentifier
558
+ ]
559
+
560
+ sendEvent(withName: "purchase-error", body: err)
561
+ }
562
+
563
+ rejectPromises(
564
+ forKey: transaction.payment.productIdentifier,
565
+ code: standardErrorCode(nsError?.code),
566
+ message: nsError?.localizedDescription,
567
+ error: nsError)
568
+ })
569
+
570
+ break
571
+ }
798
572
  }
799
- localizedPrice = formatter.string(from: discount.price)
800
- var numberOfPeriods: String?
573
+ }
574
+
575
+ func finishTransaction(withIdentifier transactionIdentifier: String?) {
576
+ let queue = SKPaymentQueue.default()
801
577
 
802
- switch discount.paymentMode {
803
- case .freeTrial:
804
- paymendMode = "FREETRIAL"
805
- numberOfPeriods = NSNumber(value: discount.subscriptionPeriod.numberOfUnits ).stringValue
806
- break
578
+ for transaction in queue.transactions {
579
+ if transaction.transactionIdentifier == transactionIdentifier {
580
+ SKPaymentQueue.default().finishTransaction(transaction)
581
+ }
582
+ }
583
+ }
807
584
 
808
- case .payAsYouGo:
809
- paymendMode = "PAYASYOUGO"
810
- numberOfPeriods = NSNumber(value: discount.numberOfPeriods).stringValue
811
- break
585
+ func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
586
+ debugMessage("PaymentQueueRestoreCompletedTransactionsFinished")
587
+ var items = [[String: Any?]]()
812
588
 
813
- case .payUpFront:
814
- paymendMode = "PAYUPFRONT"
815
- numberOfPeriods = NSNumber(value: discount.subscriptionPeriod.numberOfUnits ).stringValue
816
- break
589
+ for transaction in queue.transactions {
590
+ if transaction.transactionState == .restored || transaction.transactionState == .purchased {
591
+ getPurchaseData(transaction) { restored in
592
+ if let restored = restored {
593
+ items.append(restored)
594
+ }
817
595
 
818
- default:
819
- paymendMode = ""
820
- numberOfPeriods = "0"
821
- break
596
+ SKPaymentQueue.default().finishTransaction(transaction)
597
+ }
598
+ }
822
599
  }
823
600
 
824
- switch discount.subscriptionPeriod.unit {
825
- case .day:
826
- subscriptionPeriods = "DAY"
827
-
828
- case .week:
829
- subscriptionPeriods = "WEEK"
601
+ resolvePromises(forKey: "availableItems", value: items)
602
+ }
830
603
 
831
- case .month:
832
- subscriptionPeriods = "MONTH"
604
+ func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
605
+ myQueue.sync(execute: { [self] in
606
+ rejectPromises(
607
+ forKey: "availableItems",
608
+ code: standardErrorCode((error as NSError).code),
609
+ message: error.localizedDescription,
610
+ error: error)
611
+ })
833
612
 
834
- case .year:
835
- subscriptionPeriods = "YEAR"
613
+ debugMessage("restoreCompletedTransactionsFailedWithError")
614
+ }
836
615
 
837
- default:
838
- subscriptionPeriods = ""
616
+ func purchaseProcess(_ transaction: SKPaymentTransaction) {
617
+ if pendingTransactionWithAutoFinish {
618
+ SKPaymentQueue.default().finishTransaction(transaction)
619
+ pendingTransactionWithAutoFinish = false
839
620
  }
840
621
 
841
- let discountIdentifier = discount.identifier
842
- switch discount.type {
843
- case SKProductDiscount.Type.introductory:
844
- discountType = "INTRODUCTORY"
845
- break
846
-
847
- case SKProductDiscount.Type.subscription:
848
- discountType = "SUBSCRIPTION"
849
- break
622
+ getPurchaseData(transaction) { [self] purchase in
623
+ resolvePromises(forKey: transaction.payment.productIdentifier, value: purchase)
850
624
 
851
- default:
852
- discountType = ""
853
- break
625
+ // additionally send event
626
+ if hasListeners {
627
+ sendEvent(withName: "purchase-updated", body: purchase)
628
+ }
854
629
  }
630
+ }
855
631
 
856
- let discountObj = [
857
- "identifier": discountIdentifier,
858
- "type": discountType,
859
- "numberOfPeriods": numberOfPeriods,
860
- "price": "\(discount.price)",
861
- "localizedPrice": localizedPrice,
862
- "paymentMode": paymendMode,
863
- "subscriptionPeriod": subscriptionPeriods
632
+ func standardErrorCode(_ code: Int?) -> String? {
633
+ let descriptions = [
634
+ "E_UNKNOWN",
635
+ "E_SERVICE_ERROR",
636
+ "E_USER_CANCELLED",
637
+ "E_USER_ERROR",
638
+ "E_USER_ERROR",
639
+ "E_ITEM_UNAVAILABLE",
640
+ "E_REMOTE_ERROR",
641
+ "E_NETWORK_ERROR",
642
+ "E_SERVICE_ERROR",
643
+ "E_RECEIPT_FAILED",
644
+ "E_RECEIPT_FINISHED_FAILED"
864
645
  ]
865
646
 
866
- mappedDiscounts.append(discountObj)
867
- }
647
+ guard let code = code else {
648
+ return descriptions[0]
649
+ }
650
+
651
+ if code > descriptions.count - 1 || code < 0 { // Fix crash app without internet connection
652
+ return descriptions[0]
653
+ }
868
654
 
869
- return mappedDiscounts
655
+ return descriptions[code]
870
656
  }
871
657
 
872
- return nil
873
- }
658
+ func getProductObject(_ product: SKProduct) -> [String: Any?] {
659
+ let formatter = NumberFormatter()
660
+ formatter.numberStyle = .currency
661
+ formatter.locale = product.priceLocale
662
+
663
+ let localizedPrice = formatter.string(from: product.price)
664
+ var introductoryPrice = localizedPrice
665
+ var introductoryPriceAsAmountIOS = "\(product.price)"
666
+
667
+ var introductoryPricePaymentMode = ""
668
+ var introductoryPriceNumberOfPeriods = ""
669
+
670
+ var introductoryPriceSubscriptionPeriod = ""
671
+
672
+ var currencyCode: String? = ""
673
+ var countryCode: String? = ""
674
+ var periodNumberIOS = "0"
675
+ var periodUnitIOS = ""
676
+ var itemType = "iap"
677
+
678
+ if #available(iOS 11.2, tvOS 11.2, *) {
679
+ let numOfUnits = UInt(product.subscriptionPeriod?.numberOfUnits ?? 0)
680
+ let unit = product.subscriptionPeriod?.unit
681
+
682
+ if unit == .year {
683
+ periodUnitIOS = "YEAR"
684
+ } else if unit == .month {
685
+ periodUnitIOS = "MONTH"
686
+ } else if unit == .week {
687
+ periodUnitIOS = "WEEK"
688
+ } else if unit == .day {
689
+ periodUnitIOS = "DAY"
690
+ }
691
+
692
+ periodNumberIOS = String(format: "%lu", numOfUnits)
693
+ if numOfUnits != 0 {
694
+ itemType = "subs"
695
+ }
696
+
697
+ // subscriptionPeriod = product.subscriptionPeriod ? [product.subscriptionPeriod stringValue] : @"";
698
+ // introductoryPrice = product.introductoryPrice != nil ? [NSString stringWithFormat:@"%@", product.introductoryPrice] : @"";
699
+ if product.introductoryPrice != nil {
700
+ formatter.locale = product.introductoryPrice?.priceLocale
701
+
702
+ if let price = product.introductoryPrice?.price {
703
+ introductoryPrice = formatter.string(from: price)
704
+ }
705
+
706
+ introductoryPriceAsAmountIOS = product.introductoryPrice?.price.stringValue ?? ""
707
+
708
+ switch product.introductoryPrice?.paymentMode {
709
+ case .freeTrial:
710
+ introductoryPricePaymentMode = "FREETRIAL"
711
+ introductoryPriceNumberOfPeriods = NSNumber(value: product.introductoryPrice?.subscriptionPeriod.numberOfUnits ?? 0).stringValue
712
+
713
+ case .payAsYouGo:
714
+ introductoryPricePaymentMode = "PAYASYOUGO"
715
+ introductoryPriceNumberOfPeriods = NSNumber(value: product.introductoryPrice?.numberOfPeriods ?? 0).stringValue
716
+
717
+ case .payUpFront:
718
+ introductoryPricePaymentMode = "PAYUPFRONT"
719
+ introductoryPriceNumberOfPeriods = NSNumber(value: product.introductoryPrice?.subscriptionPeriod.numberOfUnits ?? 0).stringValue
720
+
721
+ default:
722
+ introductoryPricePaymentMode = ""
723
+ introductoryPriceNumberOfPeriods = "0"
724
+ }
725
+
726
+ if product.introductoryPrice?.subscriptionPeriod.unit == .day {
727
+ introductoryPriceSubscriptionPeriod = "DAY"
728
+ } else if product.introductoryPrice?.subscriptionPeriod.unit == .week {
729
+ introductoryPriceSubscriptionPeriod = "WEEK"
730
+ } else if product.introductoryPrice?.subscriptionPeriod.unit == .month {
731
+ introductoryPriceSubscriptionPeriod = "MONTH"
732
+ } else if product.introductoryPrice?.subscriptionPeriod.unit == .year {
733
+ introductoryPriceSubscriptionPeriod = "YEAR"
734
+ } else {
735
+ introductoryPriceSubscriptionPeriod = ""
736
+ }
737
+ } else {
738
+ introductoryPrice = ""
739
+ introductoryPriceAsAmountIOS = ""
740
+ introductoryPricePaymentMode = ""
741
+ introductoryPriceNumberOfPeriods = ""
742
+ introductoryPriceSubscriptionPeriod = ""
743
+ }
744
+ }
745
+
746
+ if #available(iOS 10.0, tvOS 10.0, *) {
747
+ currencyCode = product.priceLocale.currencyCode
748
+ }
749
+
750
+ if #available(iOS 13.0, tvOS 13.0, *) {
751
+ countryCode = SKPaymentQueue.default().storefront?.countryCode
752
+ } else if #available(iOS 10.0, tvOS 10.0, *) {
753
+ countryCode = product.priceLocale.regionCode
754
+ }
755
+
756
+ var discounts: [[String: String?]]?
874
757
 
875
- func getPurchaseData(_ transaction: SKPaymentTransaction, withBlock block: @escaping (_ transactionDict: [String: Any]?) -> Void) {
876
- requestReceiptData(withBlock: false) { receiptData, _ in
877
- if receiptData == nil {
878
- block(nil)
879
- } else {
880
- var purchase = [
881
- "transactionDate": transaction.transactionDate?.millisecondsSince1970String,
882
- "transactionId": transaction.transactionIdentifier,
883
- "productId": transaction.payment.productIdentifier,
884
- "transactionReceipt": receiptData?.base64EncodedString(options: [])
758
+ if #available(iOS 12.2, tvOS 12.2, *) {
759
+ discounts = getDiscountData(product)
760
+ }
761
+
762
+ let obj: [String: Any?] = [
763
+ "productId": product.productIdentifier,
764
+ "price": "\(product.price)",
765
+ "currency": currencyCode,
766
+ "countryCode": countryCode ?? "",
767
+ "type": itemType,
768
+ "title": product.localizedTitle != "" ? product.localizedTitle : "",
769
+ "description": product.localizedDescription != "" ? product.localizedDescription : "",
770
+ "localizedPrice": localizedPrice,
771
+ "subscriptionPeriodNumberIOS": periodNumberIOS,
772
+ "subscriptionPeriodUnitIOS": periodUnitIOS,
773
+ "introductoryPrice": introductoryPrice,
774
+ "introductoryPriceAsAmountIOS": introductoryPriceAsAmountIOS,
775
+ "introductoryPricePaymentModeIOS": introductoryPricePaymentMode,
776
+ "introductoryPriceNumberOfPeriodsIOS": introductoryPriceNumberOfPeriods,
777
+ "introductoryPriceSubscriptionPeriodIOS": introductoryPriceSubscriptionPeriod,
778
+ "discounts": discounts
885
779
  ]
886
780
 
887
- // originalTransaction is available for restore purchase and purchase of cancelled/expired subscriptions
888
- if let originalTransaction = transaction.original {
889
- purchase["originalTransactionDateIOS"] = originalTransaction.transactionDate?.millisecondsSince1970String
890
- purchase["originalTransactionIdentifierIOS"] = originalTransaction.transactionIdentifier
781
+ return obj
782
+ }
783
+
784
+ func getDiscountData(_ product: SKProduct) -> [[String: String?]]? {
785
+ if #available(iOS 12.2, tvOS 12.2, *) {
786
+ var mappedDiscounts: [[String: String?]] = []
787
+ var localizedPrice: String?
788
+ var paymendMode: String?
789
+ var subscriptionPeriods: String?
790
+ var discountType: String?
791
+
792
+ for discount in product.discounts {
793
+ let formatter = NumberFormatter()
794
+ formatter.numberStyle = .currency
795
+ let priceLocale: Locale? = discount.priceLocale
796
+ if let pLocale = priceLocale {
797
+ formatter.locale = pLocale
798
+ }
799
+ localizedPrice = formatter.string(from: discount.price)
800
+ var numberOfPeriods: String?
801
+
802
+ switch discount.paymentMode {
803
+ case .freeTrial:
804
+ paymendMode = "FREETRIAL"
805
+ numberOfPeriods = NSNumber(value: discount.subscriptionPeriod.numberOfUnits ).stringValue
806
+ break
807
+
808
+ case .payAsYouGo:
809
+ paymendMode = "PAYASYOUGO"
810
+ numberOfPeriods = NSNumber(value: discount.numberOfPeriods).stringValue
811
+ break
812
+
813
+ case .payUpFront:
814
+ paymendMode = "PAYUPFRONT"
815
+ numberOfPeriods = NSNumber(value: discount.subscriptionPeriod.numberOfUnits ).stringValue
816
+ break
817
+
818
+ default:
819
+ paymendMode = ""
820
+ numberOfPeriods = "0"
821
+ break
822
+ }
823
+
824
+ switch discount.subscriptionPeriod.unit {
825
+ case .day:
826
+ subscriptionPeriods = "DAY"
827
+
828
+ case .week:
829
+ subscriptionPeriods = "WEEK"
830
+
831
+ case .month:
832
+ subscriptionPeriods = "MONTH"
833
+
834
+ case .year:
835
+ subscriptionPeriods = "YEAR"
836
+
837
+ default:
838
+ subscriptionPeriods = ""
839
+ }
840
+
841
+ let discountIdentifier = discount.identifier
842
+ switch discount.type {
843
+ case SKProductDiscount.Type.introductory:
844
+ discountType = "INTRODUCTORY"
845
+ break
846
+
847
+ case SKProductDiscount.Type.subscription:
848
+ discountType = "SUBSCRIPTION"
849
+ break
850
+
851
+ default:
852
+ discountType = ""
853
+ break
854
+ }
855
+
856
+ let discountObj = [
857
+ "identifier": discountIdentifier,
858
+ "type": discountType,
859
+ "numberOfPeriods": numberOfPeriods,
860
+ "price": "\(discount.price)",
861
+ "localizedPrice": localizedPrice,
862
+ "paymentMode": paymendMode,
863
+ "subscriptionPeriod": subscriptionPeriods
864
+ ]
865
+
866
+ mappedDiscounts.append(discountObj)
867
+ }
868
+
869
+ return mappedDiscounts
891
870
  }
892
871
 
893
- block(purchase as [String: Any])
894
- }
872
+ return nil
873
+ }
874
+
875
+ func getPurchaseData(_ transaction: SKPaymentTransaction, withBlock block: @escaping (_ transactionDict: [String: Any]?) -> Void) {
876
+ requestReceiptData(withBlock: false) { receiptData, _ in
877
+ if receiptData == nil {
878
+ block(nil)
879
+ } else {
880
+ var purchase = [
881
+ "transactionDate": transaction.transactionDate?.millisecondsSince1970String,
882
+ "transactionId": transaction.transactionIdentifier,
883
+ "productId": transaction.payment.productIdentifier,
884
+ "transactionReceipt": receiptData?.base64EncodedString(options: [])
885
+ ]
886
+
887
+ // originalTransaction is available for restore purchase and purchase of cancelled/expired subscriptions
888
+ if let originalTransaction = transaction.original {
889
+ purchase["originalTransactionDateIOS"] = originalTransaction.transactionDate?.millisecondsSince1970String
890
+ purchase["originalTransactionIdentifierIOS"] = originalTransaction.transactionIdentifier
891
+ }
892
+
893
+ block(purchase as [String: Any])
894
+ }
895
+ }
895
896
  }
896
- }
897
897
 
898
- func requestReceiptData(withBlock forceRefresh: Bool, withBlock block: @escaping (_ data: Data?, _ error: Error?) -> Void) {
899
- debugMessage("requestReceiptDataWithBlock with force refresh: \(forceRefresh ? "YES" : "NO")")
898
+ func requestReceiptData(withBlock forceRefresh: Bool, withBlock block: @escaping (_ data: Data?, _ error: Error?) -> Void) {
899
+ debugMessage("requestReceiptDataWithBlock with force refresh: \(forceRefresh ? "YES" : "NO")")
900
900
 
901
- if forceRefresh || isReceiptPresent() == false {
902
- let refreshRequest = SKReceiptRefreshRequest()
903
- refreshRequest.delegate = self
904
- refreshRequest.start()
905
- receiptBlock = block
906
- } else {
907
- receiptBlock = nil
908
- block(receiptData(), nil)
901
+ if forceRefresh || isReceiptPresent() == false {
902
+ let refreshRequest = SKReceiptRefreshRequest()
903
+ refreshRequest.delegate = self
904
+ refreshRequest.start()
905
+ receiptBlock = block
906
+ } else {
907
+ receiptBlock = nil
908
+ block(receiptData(), nil)
909
+ }
909
910
  }
910
- }
911
911
 
912
- func isReceiptPresent() -> Bool {
913
- let receiptURL = Bundle.main.appStoreReceiptURL
914
- var canReachError: Error?
912
+ func isReceiptPresent() -> Bool {
913
+ let receiptURL = Bundle.main.appStoreReceiptURL
914
+ var canReachError: Error?
915
+
916
+ do {
917
+ try _ = receiptURL?.checkResourceIsReachable()
918
+ } catch let error {
919
+ canReachError = error
920
+ }
915
921
 
916
- do {
917
- try _ = receiptURL?.checkResourceIsReachable()
918
- } catch let error {
919
- canReachError = error
922
+ return canReachError == nil
920
923
  }
921
924
 
922
- return canReachError == nil
923
- }
925
+ func receiptData() -> Data? {
926
+ let receiptURL = Bundle.main.appStoreReceiptURL
927
+ var receiptData: Data?
924
928
 
925
- func receiptData() -> Data? {
926
- let receiptURL = Bundle.main.appStoreReceiptURL
927
- var receiptData: Data?
929
+ if let receiptURL = receiptURL {
930
+ do {
931
+ try receiptData = Data(contentsOf: receiptURL)
932
+ } catch _ {
933
+ }
934
+ }
928
935
 
929
- if let receiptURL = receiptURL {
930
- do {
931
- try receiptData = Data(contentsOf: receiptURL)
932
- } catch _ {
933
- }
936
+ return receiptData
934
937
  }
935
938
 
936
- return receiptData
937
- }
938
-
939
- func requestDidFinish(_ request: SKRequest) {
940
- if request is SKReceiptRefreshRequest {
941
- if isReceiptPresent() == true {
942
- debugMessage("Receipt refreshed success")
939
+ func requestDidFinish(_ request: SKRequest) {
940
+ if request is SKReceiptRefreshRequest {
941
+ if isReceiptPresent() == true {
942
+ debugMessage("Receipt refreshed success")
943
943
 
944
- if let receiptBlock = receiptBlock {
945
- receiptBlock(receiptData(), nil)
946
- }
947
- } else if let receiptBlock = receiptBlock {
948
- debugMessage("Finished but receipt refreshed failed")
944
+ if let receiptBlock = receiptBlock {
945
+ receiptBlock(receiptData(), nil)
946
+ }
947
+ } else if let receiptBlock = receiptBlock {
948
+ debugMessage("Finished but receipt refreshed failed")
949
949
 
950
- let error = NSError(domain: "Receipt request finished but it failed!", code: 10, userInfo: nil)
951
- receiptBlock(nil, error)
952
- }
950
+ let error = NSError(domain: "Receipt request finished but it failed!", code: 10, userInfo: nil)
951
+ receiptBlock(nil, error)
952
+ }
953
953
 
954
- receiptBlock = nil
954
+ receiptBlock = nil
955
+ }
955
956
  }
956
- }
957
957
 
958
- func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
959
- debugMessage("removedTransactions - countPendingTransactions \(countPendingTransaction)")
958
+ func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
959
+ debugMessage("removedTransactions - countPendingTransactions \(countPendingTransaction)")
960
960
 
961
- if countPendingTransaction > 0 {
962
- countPendingTransaction -= transactions.count
961
+ if countPendingTransaction > 0 {
962
+ countPendingTransaction -= transactions.count
963
963
 
964
- if countPendingTransaction <= 0 {
965
- resolvePromises(forKey: "cleaningTransactions", value: nil)
966
- countPendingTransaction = 0
967
- }
964
+ if countPendingTransaction <= 0 {
965
+ resolvePromises(forKey: "cleaningTransactions", value: nil)
966
+ countPendingTransaction = 0
967
+ }
968
+ }
968
969
  }
969
- }
970
970
  }