react-native-iap 10.0.0 → 10.0.3

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