react-native-iap 8.2.0 → 8.2.1

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.
Binary file
@@ -7,7 +7,7 @@ import com.facebook.react.bridge.Promise
7
7
  class PlayUtils {
8
8
  fun rejectPromiseWithBillingError(promise: Promise, responseCode: Int) {
9
9
  val errorData = getBillingResponseData(responseCode)
10
- promise.reject(errorData[0], errorData[1])
10
+ promise.safeReject(errorData[0], errorData[1])
11
11
  }
12
12
 
13
13
  fun getBillingResponseData(responseCode: Int): Array<String?> {
@@ -0,0 +1,33 @@
1
+ package com.dooboolab.RNIap
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.ObjectAlreadyConsumedException
5
+ import com.facebook.react.bridge.Promise
6
+
7
+ /**
8
+ * Extension functions used to simplify promise handling since we don't
9
+ * want to crash in the case of it being resolved/rejected more than once
10
+ */
11
+
12
+ fun Promise.safeResolve(value: Any) {
13
+ try {
14
+ this.resolve(value)
15
+ } catch (oce: ObjectAlreadyConsumedException) {
16
+ Log.d(RNIapModule.TAG, "Already consumed ${oce.message}")
17
+ }
18
+ }
19
+
20
+ fun Promise.safeReject(message: String) = this.safeReject(message, null, null)
21
+
22
+ fun Promise.safeReject(code: String?, message: String?) = this.safeReject(code, message, null)
23
+
24
+ fun Promise.safeReject(code: String?, throwable: Throwable?) =
25
+ this.safeReject(code, null, throwable)
26
+
27
+ fun Promise.safeReject(code: String?, message: String?, throwable: Throwable?) {
28
+ try {
29
+ this.reject(code, message, throwable)
30
+ } catch (oce: ObjectAlreadyConsumedException) {
31
+ Log.d(RNIapModule.TAG, "Already consumed ${oce.message}")
32
+ }
33
+ }
@@ -15,7 +15,6 @@ import com.android.billingclient.api.SkuDetails
15
15
  import com.android.billingclient.api.SkuDetailsParams
16
16
  import com.facebook.react.bridge.Arguments
17
17
  import com.facebook.react.bridge.LifecycleEventListener
18
- import com.facebook.react.bridge.ObjectAlreadyConsumedException
19
18
  import com.facebook.react.bridge.Promise
20
19
  import com.facebook.react.bridge.PromiseImpl
21
20
  import com.facebook.react.bridge.ReactApplicationContext
@@ -46,7 +45,7 @@ class RNIapModule(
46
45
  return "RNIapModule"
47
46
  }
48
47
 
49
- private fun ensureConnection(promise: Promise, callback: () -> Unit) {
48
+ internal fun ensureConnection(promise: Promise, callback: () -> Unit) {
50
49
  if (billingClient.isReady) {
51
50
  callback()
52
51
  return
@@ -61,7 +60,7 @@ class RNIapModule(
61
60
  },
62
61
  {
63
62
  if (it.size > 1 && it[0] is String && it[1] is String) {
64
- promise.reject(
63
+ promise.safeReject(
65
64
  it[0] as String, it[1] as String
66
65
  )
67
66
  } else {
@@ -80,14 +79,14 @@ class RNIapModule(
80
79
  TAG,
81
80
  "Already initialized, you should only call initConnection() once when your app starts"
82
81
  )
83
- promise.resolve(true)
82
+ promise.safeResolve(true)
84
83
  return
85
84
  }
86
85
  if (googleApiAvailability.isGooglePlayServicesAvailable(reactContext)
87
86
  != ConnectionResult.SUCCESS
88
87
  ) {
89
88
  Log.i(TAG, "Google Play Services are not available on this device")
90
- promise.reject(DoobooUtils.E_NOT_PREPARED, "Google Play Services are not available on this device")
89
+ promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Google Play Services are not available on this device")
91
90
  return
92
91
  }
93
92
 
@@ -97,15 +96,12 @@ class RNIapModule(
97
96
  object : BillingClientStateListener {
98
97
  override fun onBillingSetupFinished(billingResult: BillingResult) {
99
98
  val responseCode = billingResult.responseCode
100
- try {
101
- if (responseCode == BillingClient.BillingResponseCode.OK) {
102
- promise.resolve(true)
103
- } else {
104
- PlayUtils.instance
105
- .rejectPromiseWithBillingError(promise, responseCode)
106
- }
107
- } catch (oce: ObjectAlreadyConsumedException) {
108
- Log.e(TAG, oce.message!!)
99
+
100
+ if (responseCode == BillingClient.BillingResponseCode.OK) {
101
+ promise.safeResolve(true)
102
+ } else {
103
+ PlayUtils.instance
104
+ .rejectPromiseWithBillingError(promise, responseCode)
109
105
  }
110
106
  }
111
107
 
@@ -118,7 +114,7 @@ class RNIapModule(
118
114
  @ReactMethod
119
115
  fun endConnection(promise: Promise) {
120
116
  billingClient.endConnection()
121
- promise.resolve(true)
117
+ promise.safeResolve(true)
122
118
  }
123
119
 
124
120
  private fun consumeItems(
@@ -143,11 +139,8 @@ class RNIapModule(
143
139
  )
144
140
  return@ConsumeResponseListener
145
141
  }
146
- try {
147
- promise.resolve(true)
148
- } catch (oce: ObjectAlreadyConsumedException) {
149
- promise.reject(oce.message)
150
- }
142
+
143
+ promise.safeResolve(true)
151
144
  }
152
145
  billingClient.consumeAsync(consumeParams, listener)
153
146
  }
@@ -164,7 +157,7 @@ class RNIapModule(
164
157
  ) { _: BillingResult?, list: List<Purchase>? ->
165
158
  if (list == null) {
166
159
  // No purchases found
167
- promise.resolve(false)
160
+ promise.safeResolve(false)
168
161
  return@queryPurchasesAsync
169
162
  }
170
163
  // we only want to try to consume PENDING items, in order to force cache-refresh
@@ -172,7 +165,7 @@ class RNIapModule(
172
165
  val pendingPurchases = list.filter { it.purchaseState == Purchase.PurchaseState.PENDING }
173
166
 
174
167
  if (pendingPurchases.isEmpty()) {
175
- promise.resolve(false)
168
+ promise.safeResolve(false)
176
169
  return@queryPurchasesAsync
177
170
  }
178
171
  consumeItems(
@@ -262,11 +255,7 @@ class RNIapModule(
262
255
  item.putString("originalPrice", originalPrice)
263
256
  items.pushMap(item)
264
257
  }
265
- try {
266
- promise.resolve(items)
267
- } catch (oce: ObjectAlreadyConsumedException) {
268
- Log.e(TAG, oce.message!!)
269
- }
258
+ promise.safeResolve(items)
270
259
  }
271
260
  }
272
261
  }
@@ -309,11 +298,7 @@ class RNIapModule(
309
298
  items.pushMap(item)
310
299
  }
311
300
  }
312
- try {
313
- promise.resolve(items)
314
- } catch (oce: ObjectAlreadyConsumedException) {
315
- Log.e(TAG, oce.message!!)
316
- }
301
+ promise.safeResolve(items)
317
302
  }
318
303
  }
319
304
  }
@@ -345,11 +330,7 @@ class RNIapModule(
345
330
  item.putString("developerPayload", purchase.developerPayload)
346
331
  items.pushMap(item)
347
332
  }
348
- try {
349
- promise.resolve(items)
350
- } catch (oce: ObjectAlreadyConsumedException) {
351
- Log.e(TAG, oce.message!!)
352
- }
333
+ promise.safeResolve(items)
353
334
  }
354
335
  }
355
336
  }
@@ -366,7 +347,7 @@ class RNIapModule(
366
347
  ) {
367
348
  val activity = currentActivity
368
349
  if (activity == null) {
369
- promise.reject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
350
+ promise.safeReject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
370
351
  return
371
352
  }
372
353
  ensureConnection(
@@ -386,7 +367,7 @@ class RNIapModule(
386
367
  error.putString("message", debugMessage)
387
368
  error.putString("productId", sku)
388
369
  sendEvent(reactContext, "purchase-error", error)
389
- promise.reject(PROMISE_BUY_ITEM, debugMessage)
370
+ promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
390
371
  return@ensureConnection
391
372
  }
392
373
  builder.setSkuDetails(selectedSku)
@@ -419,7 +400,7 @@ class RNIapModule(
419
400
  error.putString("message", debugMessage)
420
401
  error.putString("productId", sku)
421
402
  sendEvent(reactContext, "purchase-error", error)
422
- promise.reject(PROMISE_BUY_ITEM, debugMessage)
403
+ promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
423
404
  return@ensureConnection
424
405
  }
425
406
  } else if (prorationMode
@@ -481,18 +462,14 @@ class RNIapModule(
481
462
  PlayUtils.instance
482
463
  .rejectPromiseWithBillingError(promise, billingResult.responseCode)
483
464
  }
484
- try {
485
- val map = Arguments.createMap()
486
- map.putInt("responseCode", billingResult.responseCode)
487
- map.putString("debugMessage", billingResult.debugMessage)
488
- val errorData: Array<String?> = PlayUtils.instance
489
- .getBillingResponseData(billingResult.responseCode)
490
- map.putString("code", errorData[0])
491
- map.putString("message", errorData[1])
492
- promise.resolve(map)
493
- } catch (oce: ObjectAlreadyConsumedException) {
494
- Log.e(TAG, oce.message!!)
495
- }
465
+ val map = Arguments.createMap()
466
+ map.putInt("responseCode", billingResult.responseCode)
467
+ map.putString("debugMessage", billingResult.debugMessage)
468
+ val errorData: Array<String?> = PlayUtils.instance
469
+ .getBillingResponseData(billingResult.responseCode)
470
+ map.putString("code", errorData[0])
471
+ map.putString("message", errorData[1])
472
+ promise.safeResolve(map)
496
473
  }
497
474
  }
498
475
  }
@@ -514,18 +491,15 @@ class RNIapModule(
514
491
  PlayUtils.instance
515
492
  .rejectPromiseWithBillingError(promise, billingResult.responseCode)
516
493
  }
517
- try {
518
- val map = Arguments.createMap()
519
- map.putInt("responseCode", billingResult.responseCode)
520
- map.putString("debugMessage", billingResult.debugMessage)
521
- val errorData: Array<String?> = PlayUtils.instance
522
- .getBillingResponseData(billingResult.responseCode)
523
- map.putString("code", errorData[0])
524
- map.putString("message", errorData[1])
525
- promise.resolve(map)
526
- } catch (oce: ObjectAlreadyConsumedException) {
527
- promise.reject(oce.message)
528
- }
494
+
495
+ val map = Arguments.createMap()
496
+ map.putInt("responseCode", billingResult.responseCode)
497
+ map.putString("debugMessage", billingResult.debugMessage)
498
+ val errorData: Array<String?> = PlayUtils.instance
499
+ .getBillingResponseData(billingResult.responseCode)
500
+ map.putString("code", errorData[0])
501
+ map.putString("message", errorData[1])
502
+ promise.safeResolve(map)
529
503
  }
530
504
  }
531
505
  }
@@ -612,7 +586,7 @@ class RNIapModule(
612
586
  onPurchasesUpdated(billingResult, unacknowledgedPurchases)
613
587
  }
614
588
  }
615
- promise.resolve(true)
589
+ promise.safeResolve(true)
616
590
  }
617
591
  }
618
592
 
@@ -3,6 +3,8 @@ package com.dooboolab.RNIap
3
3
  import com.android.billingclient.api.BillingClient
4
4
  import com.android.billingclient.api.BillingClientStateListener
5
5
  import com.android.billingclient.api.BillingResult
6
+ import com.android.billingclient.api.ConsumeResponseListener
7
+ import com.android.billingclient.api.Purchase
6
8
  import com.android.billingclient.api.PurchasesResponseListener
7
9
  import com.facebook.react.bridge.Promise
8
10
  import com.facebook.react.bridge.ReactApplicationContext
@@ -14,6 +16,7 @@ import io.mockk.impl.annotations.MockK
14
16
  import io.mockk.mockk
15
17
  import io.mockk.slot
16
18
  import io.mockk.verify
19
+ import org.junit.Assert.assertTrue
17
20
  import org.junit.Before
18
21
  import org.junit.Test
19
22
 
@@ -55,7 +58,7 @@ class RNIapModuleTest {
55
58
  val promise = mockk<Promise>(relaxed = true)
56
59
 
57
60
  module.initConnection(promise)
58
- verify { promise.reject(any(), any<String>()) }
61
+ verify { promise.safeReject(any(), any<String>()) }
59
62
  verify(exactly = 0) { promise.resolve(any()) }
60
63
  }
61
64
 
@@ -85,7 +88,7 @@ class RNIapModuleTest {
85
88
  val promise = mockk<Promise>(relaxed = true)
86
89
 
87
90
  module.initConnection(promise)
88
- verify { promise.reject(any(), any<String>()) }
91
+ verify { promise.safeReject(any(), any<String>()) }
89
92
  verify(exactly = 0) { promise.resolve(any()) }
90
93
  }
91
94
 
@@ -114,6 +117,51 @@ class RNIapModuleTest {
114
117
  verify { promise.resolve(false) } // empty list
115
118
  }
116
119
 
120
+ @Test
121
+ fun `flushFailedPurchasesCachedAsPending resolves to true if pending purchases`() {
122
+ every { billingClient.isReady } returns true
123
+ val promise = mockk<Promise>(relaxed = true)
124
+ val listener = slot<PurchasesResponseListener>()
125
+ every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
126
+ listener.captured.onQueryPurchasesResponse(
127
+ BillingResult.newBuilder().build(),
128
+ listOf(
129
+ // 4 = Pending
130
+ mockk<Purchase> {
131
+ every { purchaseState } returns 2
132
+ every { purchaseToken } returns "token"
133
+ },
134
+ Purchase("", "1")
135
+ )
136
+ )
137
+ }
138
+ val consumeListener = slot<ConsumeResponseListener>()
139
+ every { billingClient.consumeAsync(any(), capture(consumeListener)) } answers {
140
+ consumeListener.captured.onConsumeResponse(BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED).build(), "")
141
+ }
142
+
143
+ module.flushFailedPurchasesCachedAsPending(promise)
144
+
145
+ verify(exactly = 0) { promise.reject(any(), any<String>()) }
146
+ verify { promise.resolve(true) } // at least one pending transactions
147
+ }
148
+
149
+ @Test
150
+ fun `ensureConnection should attempt to reconnect, if not in ready state`() {
151
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
152
+ val promise = mockk<Promise>(relaxed = true)
153
+ var isCallbackCalled = false
154
+ val callback = {
155
+ isCallbackCalled = true
156
+ promise.resolve(true)
157
+ }
158
+
159
+ every { billingClient.isReady } returns false andThen true
160
+ module.ensureConnection(promise, callback)
161
+ verify { promise.resolve(true) } // at least one pending transactions
162
+ assertTrue("Should call callback", isCallbackCalled)
163
+ }
164
+
117
165
  @Test
118
166
  fun getItemsByType() {
119
167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iap",
3
- "version": "8.2.0",
3
+ "version": "8.2.1",
4
4
  "packageManager": "yarn@3.2.0",
5
5
  "description": "React Native In App Purchase Module.",
6
6
  "main": "index.js",