react-native-iap 8.2.1 → 8.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -66,7 +66,7 @@ repositories {
66
66
 
67
67
  dependencies {
68
68
  implementation 'com.facebook.react:react-native:+'
69
- testImplementation 'junit:junit:4.12'
69
+ testImplementation 'junit:junit:4.13.1'
70
70
  testImplementation "io.mockk:mockk:1.12.4"
71
71
  playImplementation 'com.android.billingclient:billing:4.0.0'
72
72
  def playServicesVersion = safeExtGet('playServicesVersion', DEFAULT_PLAY_SERVICES_VERSION)
@@ -2,6 +2,7 @@ package com.dooboolab.RNIap
2
2
 
3
3
  import com.amazon.device.iap.PurchasingService
4
4
  import com.amazon.device.iap.model.FulfillmentResult
5
+ import com.facebook.react.bridge.LifecycleEventListener
5
6
  import com.facebook.react.bridge.Promise
6
7
  import com.facebook.react.bridge.ReactApplicationContext
7
8
  import com.facebook.react.bridge.ReactContextBaseJavaModule
@@ -10,9 +11,8 @@ import com.facebook.react.bridge.ReadableArray
10
11
  import com.facebook.react.bridge.WritableNativeArray
11
12
  import java.util.HashSet
12
13
 
13
- class RNIapAmazonModule(reactContext: ReactApplicationContext?) :
14
+ class RNIapAmazonModule(reactContext: ReactApplicationContext) :
14
15
  ReactContextBaseJavaModule(reactContext) {
15
- val TAG = "RNIapAmazonModule"
16
16
  override fun getName(): String {
17
17
  return TAG
18
18
  }
@@ -127,5 +127,23 @@ class RNIapAmazonModule(reactContext: ReactApplicationContext?) :
127
127
  const val PROMISE_QUERY_PURCHASES = "PROMISE_QUERY_PURCHASES"
128
128
  const val PROMISE_QUERY_AVAILABLE_ITEMS = "PROMISE_QUERY_AVAILABLE_ITEMS"
129
129
  const val PROMISE_GET_USER_DATA = "PROMISE_GET_USER_DATA"
130
+
131
+ const val TAG = "RNIapAmazonModule"
132
+ }
133
+ init {
134
+ val lifecycleEventListener: LifecycleEventListener = object : LifecycleEventListener {
135
+ /**
136
+ * From https://developer.amazon.com/docs/in-app-purchasing/iap-implement-iap.html#getpurchaseupdates-responses
137
+ * We should fetch updates on resume
138
+ */
139
+ override fun onHostResume() {
140
+ PurchasingService.getUserData()
141
+ PurchasingService.getPurchaseUpdates(false)
142
+ }
143
+ override fun onHostPause() {}
144
+ override fun onHostDestroy() {
145
+ }
146
+ }
147
+ reactContext.addLifecycleEventListener(lifecycleEventListener)
130
148
  }
131
149
  }
@@ -42,7 +42,7 @@ class RNIapModule(
42
42
  private var billingClient: BillingClient = builder.setListener(this).build()
43
43
  private val skus: MutableMap<String, SkuDetails> = mutableMapOf()
44
44
  override fun getName(): String {
45
- return "RNIapModule"
45
+ return TAG
46
46
  }
47
47
 
48
48
  internal fun ensureConnection(promise: Promise, callback: () -> Unit) {
@@ -95,14 +95,9 @@ class RNIapModule(
95
95
  billingClient.startConnection(
96
96
  object : BillingClientStateListener {
97
97
  override fun onBillingSetupFinished(billingResult: BillingResult) {
98
- val responseCode = billingResult.responseCode
98
+ if (!isValidResult(billingResult, promise)) return
99
99
 
100
- if (responseCode == BillingClient.BillingResponseCode.OK) {
101
- promise.safeResolve(true)
102
- } else {
103
- PlayUtils.instance
104
- .rejectPromiseWithBillingError(promise, responseCode)
105
- }
100
+ promise.safeResolve(true)
106
101
  }
107
102
 
108
103
  override fun onBillingServiceDisconnected() {
@@ -154,7 +149,8 @@ class RNIapModule(
154
149
  ) {
155
150
  billingClient.queryPurchasesAsync(
156
151
  BillingClient.SkuType.INAPP
157
- ) { _: BillingResult?, list: List<Purchase>? ->
152
+ ) { billingResult: BillingResult, list: List<Purchase>? ->
153
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
158
154
  if (list == null) {
159
155
  // No purchases found
160
156
  promise.safeResolve(false)
@@ -178,7 +174,7 @@ class RNIapModule(
178
174
  }
179
175
 
180
176
  @ReactMethod
181
- fun getItemsByType(type: String?, skuArr: ReadableArray, promise: Promise) {
177
+ fun getItemsByType(type: String, skuArr: ReadableArray, promise: Promise) {
182
178
  ensureConnection(
183
179
  promise
184
180
  ) {
@@ -190,22 +186,18 @@ class RNIapModule(
190
186
  }
191
187
  }
192
188
  val params = SkuDetailsParams.newBuilder()
193
- params.setSkusList(skuList).setType(type!!)
189
+ params.setSkusList(skuList).setType(type)
194
190
  billingClient.querySkuDetailsAsync(
195
191
  params.build()
196
192
  ) { billingResult: BillingResult, skuDetailsList: List<SkuDetails>? ->
197
- Log.d(TAG, "responseCode: " + billingResult.responseCode)
198
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
199
- PlayUtils.instance
200
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
201
- return@querySkuDetailsAsync
202
- }
193
+ if (!isValidResult(billingResult, promise)) return@querySkuDetailsAsync
194
+
203
195
  if (skuDetailsList != null) {
204
196
  for (sku in skuDetailsList) {
205
197
  skus[sku.sku] = sku
206
198
  }
207
199
  }
208
- val items = WritableNativeArray()
200
+ val items = Arguments.createArray()
209
201
  for (skuDetails in skuDetailsList!!) {
210
202
  val item = Arguments.createMap()
211
203
  item.putString("productId", skuDetails.sku)
@@ -260,6 +252,22 @@ class RNIapModule(
260
252
  }
261
253
  }
262
254
 
255
+ /**
256
+ * Rejects promise with billing code if BillingResult is not OK
257
+ */
258
+ private fun isValidResult(
259
+ billingResult: BillingResult,
260
+ promise: Promise
261
+ ): Boolean {
262
+ Log.d(TAG, "responseCode: " + billingResult.responseCode)
263
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
264
+ PlayUtils.instance
265
+ .rejectPromiseWithBillingError(promise, billingResult.responseCode)
266
+ return false
267
+ }
268
+ return true
269
+ }
270
+
263
271
  @ReactMethod
264
272
  fun getAvailableItemsByType(type: String, promise: Promise) {
265
273
  ensureConnection(
@@ -268,7 +276,8 @@ class RNIapModule(
268
276
  val items = WritableNativeArray()
269
277
  billingClient.queryPurchasesAsync(
270
278
  if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
271
- ) { billingResult: BillingResult?, purchases: List<Purchase>? ->
279
+ ) { billingResult: BillingResult, purchases: List<Purchase>? ->
280
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
272
281
  if (purchases != null) {
273
282
  for (i in purchases.indices) {
274
283
  val purchase = purchases[i]
@@ -311,16 +320,12 @@ class RNIapModule(
311
320
  billingClient.queryPurchaseHistoryAsync(
312
321
  if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
313
322
  ) { billingResult, purchaseHistoryRecordList ->
314
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
315
- PlayUtils.instance
316
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
317
- return@queryPurchaseHistoryAsync
318
- }
323
+ if (!isValidResult(billingResult, promise)) return@queryPurchaseHistoryAsync
324
+
319
325
  Log.d(TAG, purchaseHistoryRecordList.toString())
320
326
  val items = Arguments.createArray()
321
- for (i in purchaseHistoryRecordList!!.indices) {
327
+ purchaseHistoryRecordList?.forEach { purchase ->
322
328
  val item = Arguments.createMap()
323
- val purchase = purchaseHistoryRecordList[i]
324
329
  item.putString("productId", purchase.skus[0])
325
330
  item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
326
331
  item.putString("transactionReceipt", purchase.originalJson)
@@ -437,8 +442,14 @@ class RNIapModule(
437
442
  }
438
443
  val flowParams = builder.build()
439
444
  val billingResult = billingClient.launchBillingFlow(activity, flowParams)
440
- val errorData: Array<String?> =
441
- PlayUtils.instance.getBillingResponseData(billingResult.responseCode)
445
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
446
+ promise.safeResolve(true)
447
+ return@ensureConnection
448
+ } else {
449
+ val errorData: Array<String?> =
450
+ PlayUtils.instance.getBillingResponseData(billingResult.responseCode)
451
+ promise.safeReject(errorData[0], errorData[1])
452
+ }
442
453
  }
443
454
  }
444
455
 
@@ -458,10 +469,8 @@ class RNIapModule(
458
469
  billingClient.acknowledgePurchase(
459
470
  acknowledgePurchaseParams
460
471
  ) { billingResult: BillingResult ->
461
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
462
- PlayUtils.instance
463
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
464
- }
472
+ if (!isValidResult(billingResult, promise)) return@acknowledgePurchase
473
+
465
474
  val map = Arguments.createMap()
466
475
  map.putInt("responseCode", billingResult.responseCode)
467
476
  map.putString("debugMessage", billingResult.debugMessage)
@@ -487,10 +496,7 @@ class RNIapModule(
487
496
  billingClient.consumeAsync(
488
497
  params
489
498
  ) { billingResult: BillingResult, purchaseToken: String? ->
490
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
491
- PlayUtils.instance
492
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
493
- }
499
+ if (!isValidResult(billingResult, promise)) return@consumeAsync
494
500
 
495
501
  val map = Arguments.createMap()
496
502
  map.putInt("responseCode", billingResult.responseCode)
@@ -575,14 +581,10 @@ class RNIapModule(
575
581
  for (type in types) {
576
582
  billingClient.queryPurchasesAsync(
577
583
  type
578
- ) { billingResult: BillingResult, list: List<Purchase>? ->
579
- val unacknowledgedPurchases = ArrayList<Purchase>()
584
+ ) { billingResult: BillingResult, list: List<Purchase> ->
585
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
580
586
 
581
- for (purchase in list!!) {
582
- if (!purchase.isAcknowledged) {
583
- unacknowledgedPurchases.add(purchase)
584
- }
585
- }
587
+ val unacknowledgedPurchases = list.filter { !it.isAcknowledged }
586
588
  onPurchasesUpdated(billingResult, unacknowledgedPurchases)
587
589
  }
588
590
  }
@@ -6,16 +6,25 @@ import com.android.billingclient.api.BillingResult
6
6
  import com.android.billingclient.api.ConsumeResponseListener
7
7
  import com.android.billingclient.api.Purchase
8
8
  import com.android.billingclient.api.PurchasesResponseListener
9
+ import com.android.billingclient.api.SkuDetailsResponseListener
10
+ import com.facebook.react.bridge.Arguments
9
11
  import com.facebook.react.bridge.Promise
10
12
  import com.facebook.react.bridge.ReactApplicationContext
13
+ import com.facebook.react.bridge.ReadableArray
14
+ import com.facebook.react.bridge.WritableArray
15
+ import com.facebook.react.bridge.WritableMap
11
16
  import com.google.android.gms.common.ConnectionResult
12
17
  import com.google.android.gms.common.GoogleApiAvailability
13
18
  import io.mockk.MockKAnnotations
14
19
  import io.mockk.every
15
20
  import io.mockk.impl.annotations.MockK
21
+ import io.mockk.just
16
22
  import io.mockk.mockk
23
+ import io.mockk.mockkStatic
24
+ import io.mockk.runs
17
25
  import io.mockk.slot
18
26
  import io.mockk.verify
27
+ import org.junit.Assert.assertEquals
19
28
  import org.junit.Assert.assertTrue
20
29
  import org.junit.Before
21
30
  import org.junit.Test
@@ -24,10 +33,13 @@ class RNIapModuleTest {
24
33
 
25
34
  @MockK
26
35
  lateinit var context: ReactApplicationContext
36
+
27
37
  @MockK
28
38
  lateinit var builder: BillingClient.Builder
39
+
29
40
  @MockK
30
41
  lateinit var billingClient: BillingClient
42
+
31
43
  @MockK
32
44
  lateinit var availability: GoogleApiAvailability
33
45
 
@@ -67,7 +79,10 @@ class RNIapModuleTest {
67
79
  every { billingClient.isReady } returns false
68
80
  val listener = slot<BillingClientStateListener>()
69
81
  every { billingClient.startConnection(capture(listener)) } answers {
70
- listener.captured.onBillingSetupFinished(BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK).build())
82
+ listener.captured.onBillingSetupFinished(
83
+ BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK)
84
+ .build()
85
+ )
71
86
  }
72
87
  every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
73
88
  val promise = mockk<Promise>(relaxed = true)
@@ -82,7 +97,10 @@ class RNIapModuleTest {
82
97
  every { billingClient.isReady } returns false
83
98
  val listener = slot<BillingClientStateListener>()
84
99
  every { billingClient.startConnection(capture(listener)) } answers {
85
- listener.captured.onBillingSetupFinished(BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR).build())
100
+ listener.captured.onBillingSetupFinished(
101
+ BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR)
102
+ .build()
103
+ )
86
104
  }
87
105
  every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
88
106
  val promise = mockk<Promise>(relaxed = true)
@@ -137,7 +155,11 @@ class RNIapModuleTest {
137
155
  }
138
156
  val consumeListener = slot<ConsumeResponseListener>()
139
157
  every { billingClient.consumeAsync(any(), capture(consumeListener)) } answers {
140
- consumeListener.captured.onConsumeResponse(BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED).build(), "")
158
+ consumeListener.captured.onConsumeResponse(
159
+ BillingResult.newBuilder()
160
+ .setResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED).build(),
161
+ ""
162
+ )
141
163
  }
142
164
 
143
165
  module.flushFailedPurchasesCachedAsPending(promise)
@@ -164,6 +186,55 @@ class RNIapModuleTest {
164
186
 
165
187
  @Test
166
188
  fun getItemsByType() {
189
+ every { billingClient.isReady } returns true
190
+ val promise = mockk<Promise>(relaxed = true)
191
+ val listener = slot<SkuDetailsResponseListener>()
192
+ every { billingClient.querySkuDetailsAsync(any(), capture(listener)) } answers {
193
+ listener.captured.onSkuDetailsResponse(
194
+ BillingResult.newBuilder().build(),
195
+ listOf(
196
+ mockk {
197
+ every { sku } returns "sku1"
198
+ every { introductoryPriceAmountMicros } returns 0
199
+ every { priceAmountMicros } returns 1
200
+ every { priceCurrencyCode } returns "USD"
201
+ every { type } returns "sub"
202
+ every { price } returns "$10.0"
203
+ every { title } returns "My product"
204
+ every { description } returns "My desc"
205
+ every { introductoryPrice } returns "$5.0"
206
+ every { zzc() } returns "com.mypackage"
207
+ every { originalPrice } returns "$13.0"
208
+ every { subscriptionPeriod } returns "3 months"
209
+ every { freeTrialPeriod } returns "1 week"
210
+ every { introductoryPriceCycles } returns 1
211
+ every { introductoryPricePeriod } returns "1"
212
+ every { iconUrl } returns "http://myicon.com/icon"
213
+ every { originalJson } returns "{}"
214
+ every { originalPriceAmountMicros } returns 2
215
+ }
216
+ )
217
+ )
218
+ }
219
+ val skus = mockk<ReadableArray>() {
220
+ every { size() } returns 1
221
+ every { getString(0) } returns "sku0"
222
+ }
223
+ mockkStatic(Arguments::class)
224
+
225
+ val itemsMap = mockk<WritableMap>()
226
+ val itemsArr = mockk<WritableArray>()
227
+ every { Arguments.createMap() } returns itemsMap
228
+ every { Arguments.createArray() } returns itemsArr
229
+ every { itemsMap.putString(any(), any()) } just runs
230
+ var itemsSize = 0
231
+ every { itemsArr.pushMap(any()) } answers {
232
+ itemsSize++
233
+ }
234
+
235
+ module.getItemsByType("subs", skus, promise)
236
+ verify { promise.resolve(any()) }
237
+ assertEquals(itemsSize, 1)
167
238
  }
168
239
 
169
240
  @Test
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iap",
3
- "version": "8.2.1",
3
+ "version": "8.2.2",
4
4
  "packageManager": "yarn@3.2.0",
5
5
  "description": "React Native In App Purchase Module.",
6
6
  "main": "index.js",