react-native-iap 10.0.1 → 10.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,967 +4,992 @@ 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
+ }
473
111
 
474
- validProducts.append(aProd)
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
+ }
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))
165
+ }
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
+ }
572
196
  }
573
- }
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
247
 
585
- func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
586
- debugMessage("PaymentQueueRestoreCompletedTransactionsFinished")
587
- var items = [[String: Any?]]()
588
-
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")
631
357
 
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
- ]
358
+ lockQueue.sync {
359
+ validProducts.removeAll()
360
+ }
646
361
 
647
- guard let code = code else {
648
- return descriptions[0]
362
+ resolve(nil)
649
363
  }
650
364
 
651
- if code > descriptions.count - 1 || code < 0 { // Fix crash app without internet connection
652
- 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)
653
371
  }
654
372
 
655
- return descriptions[code]
656
- }
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
+ }
383
+ }
657
384
 
658
- func getProductObject(_ product: SKProduct) -> [String: Any?] {
659
- let formatter = NumberFormatter()
660
- formatter.numberStyle = .currency
661
- formatter.locale = product.priceLocale
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
+ }
662
398
 
663
- let localizedPrice = formatter.string(from: product.price)
664
- var introductoryPrice = localizedPrice
665
- var introductoryPriceAsAmountIOS = "\(product.price)"
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
+ }
666
435
 
667
- var introductoryPricePaymentMode = ""
668
- var introductoryPriceNumberOfPeriods = ""
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
+ }
669
451
 
670
- var introductoryPriceSubscriptionPeriod = ""
452
+ // StoreKitDelegate
453
+ func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
454
+ for prod in response.products {
455
+ add(prod)
456
+ }
671
457
 
672
- var currencyCode: String? = ""
673
- var countryCode: String? = ""
674
- var periodNumberIOS = "0"
675
- var periodUnitIOS = ""
676
- var itemType = "iap"
458
+ var items: [[String: Any?]] = [[:]]
459
+ let lockQueue = DispatchQueue(label: "validProducts")
677
460
 
678
- if #available(iOS 11.2, tvOS 11.2, *) {
679
- let numOfUnits = UInt(product.subscriptionPeriod?.numberOfUnits ?? 0)
680
- let unit = product.subscriptionPeriod?.unit
461
+ lockQueue.sync {
462
+ for product in validProducts {
463
+ items.append(getProductObject(product))
464
+ }
465
+ }
681
466
 
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
- }
467
+ resolvePromises(forKey: request.key, value: items)
468
+ }
691
469
 
692
- periodNumberIOS = String(format: "%lu", numOfUnits)
693
- if numOfUnits != 0 {
694
- itemType = "subs"
695
- }
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")
696
474
 
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
475
+ lockQueue.sync {
476
+ debugMessage("Add new object: \(aProd.productIdentifier)")
477
+ var delTar = -1
712
478
 
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
479
+ for k in 0..<validProducts.count {
480
+ let cur = validProducts[k]
481
+ if cur.productIdentifier == aProd.productIdentifier {
482
+ delTar = k
483
+ }
484
+ }
720
485
 
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
486
+ if delTar >= 0 {
487
+ validProducts.remove(at: delTar)
488
+ }
489
+
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
+ var nsError = transaction.error as? NSError
564
+ // From https://developer.apple.com/forums/thread/674081
565
+ if let underlyingError = nsError?.userInfo["NSUnderlyingError"] as? NSError,
566
+ underlyingError.code == 3038 {
567
+ // General conditions have changed, don't display an error for the interrupted transaction
568
+ nsError = underlyingError
569
+ }
570
+
571
+ if hasListeners {
572
+ let code = nsError?.code
573
+ let responseCode = String(code ?? 0)
574
+ let err = [
575
+ "responseCode": responseCode,
576
+ "debugMessage": transaction.error?.localizedDescription,
577
+ "code": standardErrorCode(code),
578
+ "message": transaction.error?.localizedDescription,
579
+ "productId": transaction.payment.productIdentifier
580
+ ]
581
+
582
+ sendEvent(withName: "purchase-error", body: err)
583
+ }
584
+
585
+ rejectPromises(
586
+ forKey: transaction.payment.productIdentifier,
587
+ code: standardErrorCode(nsError?.code),
588
+ message: nsError?.localizedDescription,
589
+ error: nsError)
590
+ })
591
+
592
+ break
593
+ }
594
+ }
595
+ }
812
596
 
813
- case .payUpFront:
814
- paymendMode = "PAYUPFRONT"
815
- numberOfPeriods = NSNumber(value: discount.subscriptionPeriod.numberOfUnits ).stringValue
816
- break
597
+ func finishTransaction(withIdentifier transactionIdentifier: String?) {
598
+ let queue = SKPaymentQueue.default()
817
599
 
818
- default:
819
- paymendMode = ""
820
- numberOfPeriods = "0"
821
- break
600
+ for transaction in queue.transactions {
601
+ if transaction.transactionIdentifier == transactionIdentifier {
602
+ SKPaymentQueue.default().finishTransaction(transaction)
603
+ }
822
604
  }
605
+ }
823
606
 
824
- switch discount.subscriptionPeriod.unit {
825
- case .day:
826
- subscriptionPeriods = "DAY"
607
+ func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
608
+ debugMessage("PaymentQueueRestoreCompletedTransactionsFinished")
609
+ var items = [[String: Any?]]()
827
610
 
828
- case .week:
829
- subscriptionPeriods = "WEEK"
611
+ for transaction in queue.transactions {
612
+ if transaction.transactionState == .restored || transaction.transactionState == .purchased {
613
+ getPurchaseData(transaction) { restored in
614
+ if let restored = restored {
615
+ items.append(restored)
616
+ }
830
617
 
831
- case .month:
832
- subscriptionPeriods = "MONTH"
618
+ SKPaymentQueue.default().finishTransaction(transaction)
619
+ }
620
+ }
621
+ }
833
622
 
834
- case .year:
835
- subscriptionPeriods = "YEAR"
623
+ resolvePromises(forKey: "availableItems", value: items)
624
+ }
836
625
 
837
- default:
838
- subscriptionPeriods = ""
839
- }
626
+ func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
627
+ myQueue.sync(execute: { [self] in
628
+ rejectPromises(
629
+ forKey: "availableItems",
630
+ code: standardErrorCode((error as NSError).code),
631
+ message: error.localizedDescription,
632
+ error: error)
633
+ })
840
634
 
841
- let discountIdentifier = discount.identifier
842
- switch discount.type {
843
- case SKProductDiscount.Type.introductory:
844
- discountType = "INTRODUCTORY"
845
- break
635
+ debugMessage("restoreCompletedTransactionsFailedWithError")
636
+ }
637
+
638
+ func purchaseProcess(_ transaction: SKPaymentTransaction) {
639
+ if pendingTransactionWithAutoFinish {
640
+ SKPaymentQueue.default().finishTransaction(transaction)
641
+ pendingTransactionWithAutoFinish = false
642
+ }
846
643
 
847
- case SKProductDiscount.Type.subscription:
848
- discountType = "SUBSCRIPTION"
849
- break
644
+ getPurchaseData(transaction) { [self] purchase in
645
+ resolvePromises(forKey: transaction.payment.productIdentifier, value: purchase)
850
646
 
851
- default:
852
- discountType = ""
853
- break
647
+ // additionally send event
648
+ if hasListeners {
649
+ sendEvent(withName: "purchase-updated", body: purchase)
650
+ }
854
651
  }
652
+ }
855
653
 
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
654
+ func standardErrorCode(_ code: Int?) -> String? {
655
+ let descriptions = [
656
+ "E_UNKNOWN",
657
+ "E_SERVICE_ERROR",
658
+ "E_USER_CANCELLED",
659
+ "E_USER_ERROR",
660
+ "E_USER_ERROR",
661
+ "E_ITEM_UNAVAILABLE",
662
+ "E_REMOTE_ERROR",
663
+ "E_NETWORK_ERROR",
664
+ "E_SERVICE_ERROR",
665
+ "E_RECEIPT_FAILED",
666
+ "E_RECEIPT_FINISHED_FAILED"
864
667
  ]
865
668
 
866
- mappedDiscounts.append(discountObj)
867
- }
669
+ guard let code = code else {
670
+ return descriptions[0]
671
+ }
672
+ if code == 3038 { // Purchase interrupted so user can accept terms and conditions
673
+ return "E_INTERRUPTED"
674
+ }
868
675
 
869
- return mappedDiscounts
676
+ if code > descriptions.count - 1 || code < 0 { // Fix crash app without internet connection
677
+ return descriptions[0]
678
+ }
679
+
680
+ return descriptions[code]
870
681
  }
871
682
 
872
- return nil
873
- }
683
+ func getProductObject(_ product: SKProduct) -> [String: Any?] {
684
+ let formatter = NumberFormatter()
685
+ formatter.numberStyle = .currency
686
+ formatter.locale = product.priceLocale
687
+
688
+ let localizedPrice = formatter.string(from: product.price)
689
+ var introductoryPrice = localizedPrice
690
+ var introductoryPriceAsAmountIOS = "\(product.price)"
691
+
692
+ var introductoryPricePaymentMode = ""
693
+ var introductoryPriceNumberOfPeriods = ""
694
+
695
+ var introductoryPriceSubscriptionPeriod = ""
696
+
697
+ var currencyCode: String? = ""
698
+ var countryCode: String? = ""
699
+ var periodNumberIOS = "0"
700
+ var periodUnitIOS = ""
701
+ var itemType = "iap"
702
+
703
+ if #available(iOS 11.2, tvOS 11.2, *) {
704
+ let numOfUnits = UInt(product.subscriptionPeriod?.numberOfUnits ?? 0)
705
+ let unit = product.subscriptionPeriod?.unit
706
+
707
+ if unit == .year {
708
+ periodUnitIOS = "YEAR"
709
+ } else if unit == .month {
710
+ periodUnitIOS = "MONTH"
711
+ } else if unit == .week {
712
+ periodUnitIOS = "WEEK"
713
+ } else if unit == .day {
714
+ periodUnitIOS = "DAY"
715
+ }
716
+
717
+ periodNumberIOS = String(format: "%lu", numOfUnits)
718
+ if numOfUnits != 0 {
719
+ itemType = "subs"
720
+ }
721
+
722
+ // subscriptionPeriod = product.subscriptionPeriod ? [product.subscriptionPeriod stringValue] : @"";
723
+ // introductoryPrice = product.introductoryPrice != nil ? [NSString stringWithFormat:@"%@", product.introductoryPrice] : @"";
724
+ if product.introductoryPrice != nil {
725
+ formatter.locale = product.introductoryPrice?.priceLocale
726
+
727
+ if let price = product.introductoryPrice?.price {
728
+ introductoryPrice = formatter.string(from: price)
729
+ }
730
+
731
+ introductoryPriceAsAmountIOS = product.introductoryPrice?.price.stringValue ?? ""
732
+
733
+ switch product.introductoryPrice?.paymentMode {
734
+ case .freeTrial:
735
+ introductoryPricePaymentMode = "FREETRIAL"
736
+ introductoryPriceNumberOfPeriods = NSNumber(value: product.introductoryPrice?.subscriptionPeriod.numberOfUnits ?? 0).stringValue
737
+
738
+ case .payAsYouGo:
739
+ introductoryPricePaymentMode = "PAYASYOUGO"
740
+ introductoryPriceNumberOfPeriods = NSNumber(value: product.introductoryPrice?.numberOfPeriods ?? 0).stringValue
741
+
742
+ case .payUpFront:
743
+ introductoryPricePaymentMode = "PAYUPFRONT"
744
+ introductoryPriceNumberOfPeriods = NSNumber(value: product.introductoryPrice?.subscriptionPeriod.numberOfUnits ?? 0).stringValue
745
+
746
+ default:
747
+ introductoryPricePaymentMode = ""
748
+ introductoryPriceNumberOfPeriods = "0"
749
+ }
750
+
751
+ if product.introductoryPrice?.subscriptionPeriod.unit == .day {
752
+ introductoryPriceSubscriptionPeriod = "DAY"
753
+ } else if product.introductoryPrice?.subscriptionPeriod.unit == .week {
754
+ introductoryPriceSubscriptionPeriod = "WEEK"
755
+ } else if product.introductoryPrice?.subscriptionPeriod.unit == .month {
756
+ introductoryPriceSubscriptionPeriod = "MONTH"
757
+ } else if product.introductoryPrice?.subscriptionPeriod.unit == .year {
758
+ introductoryPriceSubscriptionPeriod = "YEAR"
759
+ } else {
760
+ introductoryPriceSubscriptionPeriod = ""
761
+ }
762
+ } else {
763
+ introductoryPrice = ""
764
+ introductoryPriceAsAmountIOS = ""
765
+ introductoryPricePaymentMode = ""
766
+ introductoryPriceNumberOfPeriods = ""
767
+ introductoryPriceSubscriptionPeriod = ""
768
+ }
769
+ }
770
+
771
+ if #available(iOS 10.0, tvOS 10.0, *) {
772
+ currencyCode = product.priceLocale.currencyCode
773
+ }
774
+
775
+ if #available(iOS 13.0, tvOS 13.0, *) {
776
+ countryCode = SKPaymentQueue.default().storefront?.countryCode
777
+ } else if #available(iOS 10.0, tvOS 10.0, *) {
778
+ countryCode = product.priceLocale.regionCode
779
+ }
780
+
781
+ var discounts: [[String: String?]]?
874
782
 
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: [])
783
+ if #available(iOS 12.2, tvOS 12.2, *) {
784
+ discounts = getDiscountData(product)
785
+ }
786
+
787
+ let obj: [String: Any?] = [
788
+ "productId": product.productIdentifier,
789
+ "price": "\(product.price)",
790
+ "currency": currencyCode,
791
+ "countryCode": countryCode ?? "",
792
+ "type": itemType,
793
+ "title": product.localizedTitle != "" ? product.localizedTitle : "",
794
+ "description": product.localizedDescription != "" ? product.localizedDescription : "",
795
+ "localizedPrice": localizedPrice,
796
+ "subscriptionPeriodNumberIOS": periodNumberIOS,
797
+ "subscriptionPeriodUnitIOS": periodUnitIOS,
798
+ "introductoryPrice": introductoryPrice,
799
+ "introductoryPriceAsAmountIOS": introductoryPriceAsAmountIOS,
800
+ "introductoryPricePaymentModeIOS": introductoryPricePaymentMode,
801
+ "introductoryPriceNumberOfPeriodsIOS": introductoryPriceNumberOfPeriods,
802
+ "introductoryPriceSubscriptionPeriodIOS": introductoryPriceSubscriptionPeriod,
803
+ "discounts": discounts
885
804
  ]
886
805
 
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
806
+ return obj
807
+ }
808
+
809
+ func getDiscountData(_ product: SKProduct) -> [[String: String?]]? {
810
+ if #available(iOS 12.2, tvOS 12.2, *) {
811
+ var mappedDiscounts: [[String: String?]] = []
812
+ var localizedPrice: String?
813
+ var paymendMode: String?
814
+ var subscriptionPeriods: String?
815
+ var discountType: String?
816
+
817
+ for discount in product.discounts {
818
+ let formatter = NumberFormatter()
819
+ formatter.numberStyle = .currency
820
+ let priceLocale: Locale? = discount.priceLocale
821
+ if let pLocale = priceLocale {
822
+ formatter.locale = pLocale
823
+ }
824
+ localizedPrice = formatter.string(from: discount.price)
825
+ var numberOfPeriods: String?
826
+
827
+ switch discount.paymentMode {
828
+ case .freeTrial:
829
+ paymendMode = "FREETRIAL"
830
+ numberOfPeriods = NSNumber(value: discount.subscriptionPeriod.numberOfUnits ).stringValue
831
+ break
832
+
833
+ case .payAsYouGo:
834
+ paymendMode = "PAYASYOUGO"
835
+ numberOfPeriods = NSNumber(value: discount.numberOfPeriods).stringValue
836
+ break
837
+
838
+ case .payUpFront:
839
+ paymendMode = "PAYUPFRONT"
840
+ numberOfPeriods = NSNumber(value: discount.subscriptionPeriod.numberOfUnits ).stringValue
841
+ break
842
+
843
+ default:
844
+ paymendMode = ""
845
+ numberOfPeriods = "0"
846
+ break
847
+ }
848
+
849
+ switch discount.subscriptionPeriod.unit {
850
+ case .day:
851
+ subscriptionPeriods = "DAY"
852
+
853
+ case .week:
854
+ subscriptionPeriods = "WEEK"
855
+
856
+ case .month:
857
+ subscriptionPeriods = "MONTH"
858
+
859
+ case .year:
860
+ subscriptionPeriods = "YEAR"
861
+
862
+ default:
863
+ subscriptionPeriods = ""
864
+ }
865
+
866
+ let discountIdentifier = discount.identifier
867
+ switch discount.type {
868
+ case SKProductDiscount.Type.introductory:
869
+ discountType = "INTRODUCTORY"
870
+ break
871
+
872
+ case SKProductDiscount.Type.subscription:
873
+ discountType = "SUBSCRIPTION"
874
+ break
875
+
876
+ default:
877
+ discountType = ""
878
+ break
879
+ }
880
+
881
+ let discountObj = [
882
+ "identifier": discountIdentifier,
883
+ "type": discountType,
884
+ "numberOfPeriods": numberOfPeriods,
885
+ "price": "\(discount.price)",
886
+ "localizedPrice": localizedPrice,
887
+ "paymentMode": paymendMode,
888
+ "subscriptionPeriod": subscriptionPeriods
889
+ ]
890
+
891
+ mappedDiscounts.append(discountObj)
892
+ }
893
+
894
+ return mappedDiscounts
891
895
  }
892
896
 
893
- block(purchase as [String: Any])
894
- }
897
+ return nil
898
+ }
899
+
900
+ func getPurchaseData(_ transaction: SKPaymentTransaction, withBlock block: @escaping (_ transactionDict: [String: Any]?) -> Void) {
901
+ requestReceiptData(withBlock: false) { receiptData, _ in
902
+ if receiptData == nil {
903
+ block(nil)
904
+ } else {
905
+ var purchase = [
906
+ "transactionDate": transaction.transactionDate?.millisecondsSince1970String,
907
+ "transactionId": transaction.transactionIdentifier,
908
+ "productId": transaction.payment.productIdentifier,
909
+ "transactionReceipt": receiptData?.base64EncodedString(options: [])
910
+ ]
911
+
912
+ // originalTransaction is available for restore purchase and purchase of cancelled/expired subscriptions
913
+ if let originalTransaction = transaction.original {
914
+ purchase["originalTransactionDateIOS"] = originalTransaction.transactionDate?.millisecondsSince1970String
915
+ purchase["originalTransactionIdentifierIOS"] = originalTransaction.transactionIdentifier
916
+ }
917
+
918
+ block(purchase as [String: Any])
919
+ }
920
+ }
895
921
  }
896
- }
897
922
 
898
- func requestReceiptData(withBlock forceRefresh: Bool, withBlock block: @escaping (_ data: Data?, _ error: Error?) -> Void) {
899
- debugMessage("requestReceiptDataWithBlock with force refresh: \(forceRefresh ? "YES" : "NO")")
923
+ func requestReceiptData(withBlock forceRefresh: Bool, withBlock block: @escaping (_ data: Data?, _ error: Error?) -> Void) {
924
+ debugMessage("requestReceiptDataWithBlock with force refresh: \(forceRefresh ? "YES" : "NO")")
900
925
 
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)
926
+ if forceRefresh || isReceiptPresent() == false {
927
+ let refreshRequest = SKReceiptRefreshRequest()
928
+ refreshRequest.delegate = self
929
+ refreshRequest.start()
930
+ receiptBlock = block
931
+ } else {
932
+ receiptBlock = nil
933
+ block(receiptData(), nil)
934
+ }
909
935
  }
910
- }
911
936
 
912
- func isReceiptPresent() -> Bool {
913
- let receiptURL = Bundle.main.appStoreReceiptURL
914
- var canReachError: Error?
937
+ func isReceiptPresent() -> Bool {
938
+ let receiptURL = Bundle.main.appStoreReceiptURL
939
+ var canReachError: Error?
940
+
941
+ do {
942
+ try _ = receiptURL?.checkResourceIsReachable()
943
+ } catch let error {
944
+ canReachError = error
945
+ }
915
946
 
916
- do {
917
- try _ = receiptURL?.checkResourceIsReachable()
918
- } catch let error {
919
- canReachError = error
947
+ return canReachError == nil
920
948
  }
921
949
 
922
- return canReachError == nil
923
- }
950
+ func receiptData() -> Data? {
951
+ let receiptURL = Bundle.main.appStoreReceiptURL
952
+ var receiptData: Data?
924
953
 
925
- func receiptData() -> Data? {
926
- let receiptURL = Bundle.main.appStoreReceiptURL
927
- var receiptData: Data?
954
+ if let receiptURL = receiptURL {
955
+ do {
956
+ try receiptData = Data(contentsOf: receiptURL)
957
+ } catch _ {
958
+ }
959
+ }
928
960
 
929
- if let receiptURL = receiptURL {
930
- do {
931
- try receiptData = Data(contentsOf: receiptURL)
932
- } catch _ {
933
- }
961
+ return receiptData
934
962
  }
935
963
 
936
- return receiptData
937
- }
938
-
939
- func requestDidFinish(_ request: SKRequest) {
940
- if request is SKReceiptRefreshRequest {
941
- if isReceiptPresent() == true {
942
- debugMessage("Receipt refreshed success")
964
+ func requestDidFinish(_ request: SKRequest) {
965
+ if request is SKReceiptRefreshRequest {
966
+ if isReceiptPresent() == true {
967
+ debugMessage("Receipt refreshed success")
943
968
 
944
- if let receiptBlock = receiptBlock {
945
- receiptBlock(receiptData(), nil)
946
- }
947
- } else if let receiptBlock = receiptBlock {
948
- debugMessage("Finished but receipt refreshed failed")
969
+ if let receiptBlock = receiptBlock {
970
+ receiptBlock(receiptData(), nil)
971
+ }
972
+ } else if let receiptBlock = receiptBlock {
973
+ debugMessage("Finished but receipt refreshed failed")
949
974
 
950
- let error = NSError(domain: "Receipt request finished but it failed!", code: 10, userInfo: nil)
951
- receiptBlock(nil, error)
952
- }
975
+ let error = NSError(domain: "Receipt request finished but it failed!", code: 10, userInfo: nil)
976
+ receiptBlock(nil, error)
977
+ }
953
978
 
954
- receiptBlock = nil
979
+ receiptBlock = nil
980
+ }
955
981
  }
956
- }
957
982
 
958
- func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
959
- debugMessage("removedTransactions - countPendingTransactions \(countPendingTransaction)")
983
+ func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
984
+ debugMessage("removedTransactions - countPendingTransactions \(countPendingTransaction)")
960
985
 
961
- if countPendingTransaction > 0 {
962
- countPendingTransaction -= transactions.count
986
+ if countPendingTransaction > 0 {
987
+ countPendingTransaction -= transactions.count
963
988
 
964
- if countPendingTransaction <= 0 {
965
- resolvePromises(forKey: "cleaningTransactions", value: nil)
966
- countPendingTransaction = 0
967
- }
989
+ if countPendingTransaction <= 0 {
990
+ resolvePromises(forKey: "cleaningTransactions", value: nil)
991
+ countPendingTransaction = 0
992
+ }
993
+ }
968
994
  }
969
- }
970
995
  }