react-native-iap 8.2.0 → 8.3.0

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
package/RNIap.podspec CHANGED
@@ -11,6 +11,11 @@ Pod::Spec.new do |s|
11
11
  s.platforms = { :ios => "9.0", :tvos => "9.0" }
12
12
  s.source = { :git => "https://github.com/dooboolab/react-native-iap.git", :tag => "#{s.version}" }
13
13
  s.source_files = "ios/*.{h,m,swift}"
14
+ s.script_phase = {
15
+ :name => 'Copy Swift Header',
16
+ :script => 'ditto "${DERIVED_SOURCES_DIR}/${PRODUCT_MODULE_NAME}-Swift.h" "${PODS_ROOT}/Headers/Public/${PRODUCT_MODULE_NAME}/${PRODUCT_MODULE_NAME}-Swift.h"',
17
+ :execution_position => :after_compile
18
+ }
14
19
  s.swift_version = "4.2"
15
20
  s.requires_arc = true
16
21
 
@@ -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
  }
@@ -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
@@ -23,6 +22,7 @@ import com.facebook.react.bridge.ReactContext
23
22
  import com.facebook.react.bridge.ReactContextBaseJavaModule
24
23
  import com.facebook.react.bridge.ReactMethod
25
24
  import com.facebook.react.bridge.ReadableArray
25
+ import com.facebook.react.bridge.ReadableType
26
26
  import com.facebook.react.bridge.WritableMap
27
27
  import com.facebook.react.bridge.WritableNativeArray
28
28
  import com.facebook.react.bridge.WritableNativeMap
@@ -40,28 +40,37 @@ class RNIapModule(
40
40
  ReactContextBaseJavaModule(reactContext),
41
41
  PurchasesUpdatedListener {
42
42
 
43
- private var billingClient: BillingClient = builder.setListener(this).build()
43
+ private var billingClientCache: BillingClient? = null
44
44
  private val skus: MutableMap<String, SkuDetails> = mutableMapOf()
45
45
  override fun getName(): String {
46
- return "RNIapModule"
46
+ return TAG
47
47
  }
48
48
 
49
- private fun ensureConnection(promise: Promise, callback: () -> Unit) {
50
- if (billingClient.isReady) {
51
- callback()
49
+ internal fun ensureConnection(
50
+ promise: Promise,
51
+ callback: (billingClient: BillingClient) -> Unit
52
+ ) {
53
+ val billingClient = billingClientCache
54
+ if (billingClient?.isReady == true) {
55
+ callback(billingClient)
52
56
  return
53
57
  } else {
54
58
  val nested = PromiseImpl(
55
59
  {
56
60
  if (it.isNotEmpty() && it[0] is Boolean && it[0] as Boolean) {
57
- callback()
61
+ val connectedBillingClient = billingClientCache
62
+ if (connectedBillingClient?.isReady == true) {
63
+ callback(connectedBillingClient)
64
+ } else {
65
+ promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Unable to auto-initialize connection")
66
+ }
58
67
  } else {
59
68
  Log.i(TAG, "Incorrect parameter in resolve")
60
69
  }
61
70
  },
62
71
  {
63
72
  if (it.size > 1 && it[0] is String && it[1] is String) {
64
- promise.reject(
73
+ promise.safeReject(
65
74
  it[0] as String, it[1] as String
66
75
  )
67
76
  } else {
@@ -75,50 +84,44 @@ class RNIapModule(
75
84
 
76
85
  @ReactMethod
77
86
  fun initConnection(promise: Promise) {
78
- if (billingClient.isReady) {
79
- Log.i(
80
- TAG,
81
- "Already initialized, you should only call initConnection() once when your app starts"
82
- )
83
- promise.resolve(true)
84
- return
85
- }
86
87
  if (googleApiAvailability.isGooglePlayServicesAvailable(reactContext)
87
88
  != ConnectionResult.SUCCESS
88
89
  ) {
89
90
  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")
91
+ promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Google Play Services are not available on this device")
91
92
  return
92
93
  }
93
94
 
94
- billingClient = builder.setListener(this).build()
95
+ if (billingClientCache?.isReady == true) {
96
+ Log.i(
97
+ TAG,
98
+ "Already initialized, you should only call initConnection() once when your app starts"
99
+ )
100
+ promise.safeResolve(true)
101
+ return
102
+ }
103
+ builder.setListener(this).build().also {
104
+ billingClientCache = it
105
+ it.startConnection(
106
+ object : BillingClientStateListener {
107
+ override fun onBillingSetupFinished(billingResult: BillingResult) {
108
+ if (!isValidResult(billingResult, promise)) return
95
109
 
96
- billingClient.startConnection(
97
- object : BillingClientStateListener {
98
- override fun onBillingSetupFinished(billingResult: BillingResult) {
99
- 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!!)
110
+ promise.safeResolve(true)
109
111
  }
110
- }
111
112
 
112
- override fun onBillingServiceDisconnected() {
113
- Log.i(TAG, "Billing service disconnected")
114
- }
115
- })
113
+ override fun onBillingServiceDisconnected() {
114
+ Log.i(TAG, "Billing service disconnected")
115
+ }
116
+ })
117
+ }
116
118
  }
117
119
 
118
120
  @ReactMethod
119
121
  fun endConnection(promise: Promise) {
120
- billingClient.endConnection()
121
- promise.resolve(true)
122
+ billingClientCache?.endConnection()
123
+ billingClientCache = null
124
+ promise.safeResolve(true)
122
125
  }
123
126
 
124
127
  private fun consumeItems(
@@ -129,7 +132,7 @@ class RNIapModule(
129
132
  for (purchase in purchases) {
130
133
  ensureConnection(
131
134
  promise
132
- ) {
135
+ ) { billingClient ->
133
136
  val consumeParams =
134
137
  ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
135
138
  .build()
@@ -143,11 +146,8 @@ class RNIapModule(
143
146
  )
144
147
  return@ConsumeResponseListener
145
148
  }
146
- try {
147
- promise.resolve(true)
148
- } catch (oce: ObjectAlreadyConsumedException) {
149
- promise.reject(oce.message)
150
- }
149
+
150
+ promise.safeResolve(true)
151
151
  }
152
152
  billingClient.consumeAsync(consumeParams, listener)
153
153
  }
@@ -158,13 +158,14 @@ class RNIapModule(
158
158
  fun flushFailedPurchasesCachedAsPending(promise: Promise) {
159
159
  ensureConnection(
160
160
  promise
161
- ) {
161
+ ) { billingClient ->
162
162
  billingClient.queryPurchasesAsync(
163
163
  BillingClient.SkuType.INAPP
164
- ) { _: BillingResult?, list: List<Purchase>? ->
164
+ ) { billingResult: BillingResult, list: List<Purchase>? ->
165
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
165
166
  if (list == null) {
166
167
  // No purchases found
167
- promise.resolve(false)
168
+ promise.safeResolve(false)
168
169
  return@queryPurchasesAsync
169
170
  }
170
171
  // we only want to try to consume PENDING items, in order to force cache-refresh
@@ -172,7 +173,7 @@ class RNIapModule(
172
173
  val pendingPurchases = list.filter { it.purchaseState == Purchase.PurchaseState.PENDING }
173
174
 
174
175
  if (pendingPurchases.isEmpty()) {
175
- promise.resolve(false)
176
+ promise.safeResolve(false)
176
177
  return@queryPurchasesAsync
177
178
  }
178
179
  consumeItems(
@@ -185,101 +186,109 @@ class RNIapModule(
185
186
  }
186
187
 
187
188
  @ReactMethod
188
- fun getItemsByType(type: String?, skuArr: ReadableArray, promise: Promise) {
189
+ fun getItemsByType(type: String, skuArr: ReadableArray, promise: Promise) {
189
190
  ensureConnection(
190
191
  promise
191
- ) {
192
+ ) { billingClient ->
192
193
  val skuList = ArrayList<String>()
193
194
  for (i in 0 until skuArr.size()) {
194
- val sku = skuArr.getString(i)
195
- if (sku is String) {
195
+ if (skuArr.getType(i) == ReadableType.String) {
196
+ val sku = skuArr.getString(i)
196
197
  skuList.add(sku)
197
198
  }
198
199
  }
199
200
  val params = SkuDetailsParams.newBuilder()
200
- params.setSkusList(skuList).setType(type!!)
201
+ params.setSkusList(skuList).setType(type)
201
202
  billingClient.querySkuDetailsAsync(
202
203
  params.build()
203
204
  ) { billingResult: BillingResult, skuDetailsList: List<SkuDetails>? ->
204
- Log.d(TAG, "responseCode: " + billingResult.responseCode)
205
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
206
- PlayUtils.instance
207
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
208
- return@querySkuDetailsAsync
209
- }
205
+ if (!isValidResult(billingResult, promise)) return@querySkuDetailsAsync
206
+
207
+ val items = Arguments.createArray()
210
208
  if (skuDetailsList != null) {
211
- for (sku in skuDetailsList) {
212
- skus[sku.sku] = sku
209
+ for (skuDetails in skuDetailsList) {
210
+ skus[skuDetails.sku] = skuDetails
211
+
212
+ val item = Arguments.createMap()
213
+ item.putString("productId", skuDetails.sku)
214
+ val introductoryPriceMicros = skuDetails.introductoryPriceAmountMicros
215
+ val priceAmountMicros = skuDetails.priceAmountMicros
216
+ // Use valueOf instead of constructors.
217
+ // See:
218
+ // https://www.javaworld.com/article/2073176/caution--double-to-bigdecimal-in-java.html
219
+ val priceAmount = BigDecimal.valueOf(priceAmountMicros)
220
+ val introductoryPriceAmount =
221
+ BigDecimal.valueOf(introductoryPriceMicros)
222
+ val microUnitsDivisor = BigDecimal.valueOf(1000000)
223
+ val price = priceAmount.divide(microUnitsDivisor).toString()
224
+ val introductoryPriceAsAmountAndroid =
225
+ introductoryPriceAmount.divide(microUnitsDivisor).toString()
226
+ item.putString("price", price)
227
+ item.putString("currency", skuDetails.priceCurrencyCode)
228
+ item.putString("type", skuDetails.type)
229
+ item.putString("localizedPrice", skuDetails.price)
230
+ item.putString("title", skuDetails.title)
231
+ item.putString("description", skuDetails.description)
232
+ item.putString("introductoryPrice", skuDetails.introductoryPrice)
233
+ item.putString("typeAndroid", skuDetails.type)
234
+ item.putString("packageNameAndroid", skuDetails.zzc())
235
+ item.putString("originalPriceAndroid", skuDetails.originalPrice)
236
+ item.putString(
237
+ "subscriptionPeriodAndroid",
238
+ skuDetails.subscriptionPeriod
239
+ )
240
+ item.putString("freeTrialPeriodAndroid", skuDetails.freeTrialPeriod)
241
+ item.putString(
242
+ "introductoryPriceCyclesAndroid",
243
+ skuDetails.introductoryPriceCycles.toString()
244
+ )
245
+ item.putString(
246
+ "introductoryPricePeriodAndroid", skuDetails.introductoryPricePeriod
247
+ )
248
+ item.putString(
249
+ "introductoryPriceAsAmountAndroid", introductoryPriceAsAmountAndroid
250
+ )
251
+ item.putString("iconUrl", skuDetails.iconUrl)
252
+ item.putString("originalJson", skuDetails.originalJson)
253
+ val originalPriceAmountMicros =
254
+ BigDecimal.valueOf(skuDetails.originalPriceAmountMicros)
255
+ val originalPrice =
256
+ originalPriceAmountMicros.divide(microUnitsDivisor).toString()
257
+ item.putString("originalPrice", originalPrice)
258
+ items.pushMap(item)
213
259
  }
214
260
  }
215
- val items = WritableNativeArray()
216
- for (skuDetails in skuDetailsList!!) {
217
- val item = Arguments.createMap()
218
- item.putString("productId", skuDetails.sku)
219
- val introductoryPriceMicros = skuDetails.introductoryPriceAmountMicros
220
- val priceAmountMicros = skuDetails.priceAmountMicros
221
- // Use valueOf instead of constructors.
222
- // See:
223
- // https://www.javaworld.com/article/2073176/caution--double-to-bigdecimal-in-java.html
224
- val priceAmount = BigDecimal.valueOf(priceAmountMicros)
225
- val introductoryPriceAmount =
226
- BigDecimal.valueOf(introductoryPriceMicros)
227
- val microUnitsDivisor = BigDecimal.valueOf(1000000)
228
- val price = priceAmount.divide(microUnitsDivisor).toString()
229
- val introductoryPriceAsAmountAndroid =
230
- introductoryPriceAmount.divide(microUnitsDivisor).toString()
231
- item.putString("price", price)
232
- item.putString("currency", skuDetails.priceCurrencyCode)
233
- item.putString("type", skuDetails.type)
234
- item.putString("localizedPrice", skuDetails.price)
235
- item.putString("title", skuDetails.title)
236
- item.putString("description", skuDetails.description)
237
- item.putString("introductoryPrice", skuDetails.introductoryPrice)
238
- item.putString("typeAndroid", skuDetails.type)
239
- item.putString("packageNameAndroid", skuDetails.zzc())
240
- item.putString("originalPriceAndroid", skuDetails.originalPrice)
241
- item.putString(
242
- "subscriptionPeriodAndroid",
243
- skuDetails.subscriptionPeriod
244
- )
245
- item.putString("freeTrialPeriodAndroid", skuDetails.freeTrialPeriod)
246
- item.putString(
247
- "introductoryPriceCyclesAndroid",
248
- skuDetails.introductoryPriceCycles.toString()
249
- )
250
- item.putString(
251
- "introductoryPricePeriodAndroid", skuDetails.introductoryPricePeriod
252
- )
253
- item.putString(
254
- "introductoryPriceAsAmountAndroid", introductoryPriceAsAmountAndroid
255
- )
256
- item.putString("iconUrl", skuDetails.iconUrl)
257
- item.putString("originalJson", skuDetails.originalJson)
258
- val originalPriceAmountMicros =
259
- BigDecimal.valueOf(skuDetails.originalPriceAmountMicros)
260
- val originalPrice =
261
- originalPriceAmountMicros.divide(microUnitsDivisor).toString()
262
- item.putString("originalPrice", originalPrice)
263
- items.pushMap(item)
264
- }
265
- try {
266
- promise.resolve(items)
267
- } catch (oce: ObjectAlreadyConsumedException) {
268
- Log.e(TAG, oce.message!!)
269
- }
261
+ promise.safeResolve(items)
270
262
  }
271
263
  }
272
264
  }
273
265
 
266
+ /**
267
+ * Rejects promise with billing code if BillingResult is not OK
268
+ */
269
+ private fun isValidResult(
270
+ billingResult: BillingResult,
271
+ promise: Promise
272
+ ): Boolean {
273
+ Log.d(TAG, "responseCode: " + billingResult.responseCode)
274
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
275
+ PlayUtils.instance
276
+ .rejectPromiseWithBillingError(promise, billingResult.responseCode)
277
+ return false
278
+ }
279
+ return true
280
+ }
281
+
274
282
  @ReactMethod
275
283
  fun getAvailableItemsByType(type: String, promise: Promise) {
276
284
  ensureConnection(
277
285
  promise
278
- ) {
286
+ ) { billingClient ->
279
287
  val items = WritableNativeArray()
280
288
  billingClient.queryPurchasesAsync(
281
289
  if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
282
- ) { billingResult: BillingResult?, purchases: List<Purchase>? ->
290
+ ) { billingResult: BillingResult, purchases: List<Purchase>? ->
291
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
283
292
  if (purchases != null) {
284
293
  for (i in purchases.indices) {
285
294
  val purchase = purchases[i]
@@ -297,11 +306,11 @@ class RNIapModule(
297
306
  item.putString("packageNameAndroid", purchase.packageName)
298
307
  item.putString(
299
308
  "obfuscatedAccountIdAndroid",
300
- purchase.accountIdentifiers!!.obfuscatedAccountId
309
+ purchase.accountIdentifiers?.obfuscatedAccountId
301
310
  )
302
311
  item.putString(
303
312
  "obfuscatedProfileIdAndroid",
304
- purchase.accountIdentifiers!!.obfuscatedProfileId
313
+ purchase.accountIdentifiers?.obfuscatedProfileId
305
314
  )
306
315
  if (type == BillingClient.SkuType.SUBS) {
307
316
  item.putBoolean("autoRenewingAndroid", purchase.isAutoRenewing)
@@ -309,11 +318,7 @@ class RNIapModule(
309
318
  items.pushMap(item)
310
319
  }
311
320
  }
312
- try {
313
- promise.resolve(items)
314
- } catch (oce: ObjectAlreadyConsumedException) {
315
- Log.e(TAG, oce.message!!)
316
- }
321
+ promise.safeResolve(items)
317
322
  }
318
323
  }
319
324
  }
@@ -322,20 +327,16 @@ class RNIapModule(
322
327
  fun getPurchaseHistoryByType(type: String, promise: Promise) {
323
328
  ensureConnection(
324
329
  promise
325
- ) {
330
+ ) { billingClient ->
326
331
  billingClient.queryPurchaseHistoryAsync(
327
332
  if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
328
333
  ) { billingResult, purchaseHistoryRecordList ->
329
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
330
- PlayUtils.instance
331
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
332
- return@queryPurchaseHistoryAsync
333
- }
334
+ if (!isValidResult(billingResult, promise)) return@queryPurchaseHistoryAsync
335
+
334
336
  Log.d(TAG, purchaseHistoryRecordList.toString())
335
337
  val items = Arguments.createArray()
336
- for (i in purchaseHistoryRecordList!!.indices) {
338
+ purchaseHistoryRecordList?.forEach { purchase ->
337
339
  val item = Arguments.createMap()
338
- val purchase = purchaseHistoryRecordList[i]
339
340
  item.putString("productId", purchase.skus[0])
340
341
  item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
341
342
  item.putString("transactionReceipt", purchase.originalJson)
@@ -345,11 +346,7 @@ class RNIapModule(
345
346
  item.putString("developerPayload", purchase.developerPayload)
346
347
  items.pushMap(item)
347
348
  }
348
- try {
349
- promise.resolve(items)
350
- } catch (oce: ObjectAlreadyConsumedException) {
351
- Log.e(TAG, oce.message!!)
352
- }
349
+ promise.safeResolve(items)
353
350
  }
354
351
  }
355
352
  }
@@ -366,12 +363,12 @@ class RNIapModule(
366
363
  ) {
367
364
  val activity = currentActivity
368
365
  if (activity == null) {
369
- promise.reject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
366
+ promise.safeReject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
370
367
  return
371
368
  }
372
369
  ensureConnection(
373
370
  promise
374
- ) {
371
+ ) { billingClient ->
375
372
  DoobooUtils.instance.addPromiseForKey(
376
373
  PROMISE_BUY_ITEM, promise
377
374
  )
@@ -386,7 +383,7 @@ class RNIapModule(
386
383
  error.putString("message", debugMessage)
387
384
  error.putString("productId", sku)
388
385
  sendEvent(reactContext, "purchase-error", error)
389
- promise.reject(PROMISE_BUY_ITEM, debugMessage)
386
+ promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
390
387
  return@ensureConnection
391
388
  }
392
389
  builder.setSkuDetails(selectedSku)
@@ -419,7 +416,7 @@ class RNIapModule(
419
416
  error.putString("message", debugMessage)
420
417
  error.putString("productId", sku)
421
418
  sendEvent(reactContext, "purchase-error", error)
422
- promise.reject(PROMISE_BUY_ITEM, debugMessage)
419
+ promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
423
420
  return@ensureConnection
424
421
  }
425
422
  } else if (prorationMode
@@ -455,77 +452,71 @@ class RNIapModule(
455
452
  builder.setSubscriptionUpdateParams(subscriptionUpdateParams)
456
453
  }
457
454
  val flowParams = builder.build()
458
- val billingResult = billingClient.launchBillingFlow(activity, flowParams)
459
- val errorData: Array<String?> =
460
- PlayUtils.instance.getBillingResponseData(billingResult.responseCode)
455
+ val billingResultCode = billingClient.launchBillingFlow(activity, flowParams)?.responseCode ?: BillingClient.BillingResponseCode.ERROR
456
+ if (billingResultCode == BillingClient.BillingResponseCode.OK) {
457
+ promise.safeResolve(true)
458
+ return@ensureConnection
459
+ } else {
460
+ val errorData: Array<String?> =
461
+ PlayUtils.instance.getBillingResponseData(billingResultCode)
462
+ promise.safeReject(errorData[0], errorData[1])
463
+ }
461
464
  }
462
465
  }
463
466
 
464
467
  @ReactMethod
465
468
  fun acknowledgePurchase(
466
- token: String?,
469
+ token: String,
467
470
  developerPayLoad: String?,
468
471
  promise: Promise
469
472
  ) {
470
473
  ensureConnection(
471
474
  promise
472
- ) {
475
+ ) { billingClient ->
473
476
  val acknowledgePurchaseParams =
474
477
  AcknowledgePurchaseParams.newBuilder().setPurchaseToken(
475
- token!!
478
+ token
476
479
  ).build()
477
480
  billingClient.acknowledgePurchase(
478
481
  acknowledgePurchaseParams
479
482
  ) { billingResult: BillingResult ->
480
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
481
- PlayUtils.instance
482
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
483
- }
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
- }
483
+ if (!isValidResult(billingResult, promise)) return@acknowledgePurchase
484
+
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.safeResolve(map)
496
493
  }
497
494
  }
498
495
  }
499
496
 
500
497
  @ReactMethod
501
498
  fun consumeProduct(
502
- token: String?,
499
+ token: String,
503
500
  developerPayLoad: String?,
504
501
  promise: Promise
505
502
  ) {
506
- val params = ConsumeParams.newBuilder().setPurchaseToken(token!!).build()
503
+ val params = ConsumeParams.newBuilder().setPurchaseToken(token).build()
507
504
  ensureConnection(
508
505
  promise
509
- ) {
506
+ ) { billingClient ->
510
507
  billingClient.consumeAsync(
511
508
  params
512
509
  ) { billingResult: BillingResult, purchaseToken: String? ->
513
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
514
- PlayUtils.instance
515
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
516
- }
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
- }
510
+ if (!isValidResult(billingResult, promise)) return@consumeAsync
511
+
512
+ val map = Arguments.createMap()
513
+ map.putInt("responseCode", billingResult.responseCode)
514
+ map.putString("debugMessage", billingResult.debugMessage)
515
+ val errorData: Array<String?> = PlayUtils.instance
516
+ .getBillingResponseData(billingResult.responseCode)
517
+ map.putString("code", errorData[0])
518
+ map.putString("message", errorData[1])
519
+ promise.safeResolve(map)
529
520
  }
530
521
  }
531
522
  }
@@ -596,23 +587,19 @@ class RNIapModule(
596
587
  private fun sendUnconsumedPurchases(promise: Promise) {
597
588
  ensureConnection(
598
589
  promise
599
- ) {
590
+ ) { billingClient ->
600
591
  val types = arrayOf(BillingClient.SkuType.INAPP, BillingClient.SkuType.SUBS)
601
592
  for (type in types) {
602
593
  billingClient.queryPurchasesAsync(
603
594
  type
604
- ) { billingResult: BillingResult, list: List<Purchase>? ->
605
- val unacknowledgedPurchases = ArrayList<Purchase>()
595
+ ) { billingResult: BillingResult, list: List<Purchase> ->
596
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
606
597
 
607
- for (purchase in list!!) {
608
- if (!purchase.isAcknowledged) {
609
- unacknowledgedPurchases.add(purchase)
610
- }
611
- }
598
+ val unacknowledgedPurchases = list.filter { !it.isAcknowledged }
612
599
  onPurchasesUpdated(billingResult, unacknowledgedPurchases)
613
600
  }
614
601
  }
615
- promise.resolve(true)
602
+ promise.safeResolve(true)
616
603
  }
617
604
  }
618
605
 
@@ -631,9 +618,8 @@ class RNIapModule(
631
618
  // Keep: Required for RN built-in Event Emitter Calls.
632
619
  }
633
620
 
634
- @get:ReactMethod
635
- val packageName: String
636
- get() = reactApplicationContext.packageName
621
+ @ReactMethod
622
+ fun getPackageName(promise: Promise) = promise.resolve(reactApplicationContext.packageName)
637
623
 
638
624
  private fun sendEvent(
639
625
  reactContext: ReactContext,
@@ -655,7 +641,7 @@ class RNIapModule(
655
641
  override fun onHostResume() {}
656
642
  override fun onHostPause() {}
657
643
  override fun onHostDestroy() {
658
- billingClient.endConnection()
644
+ billingClientCache?.endConnection()
659
645
  }
660
646
  }
661
647
  reactContext.addLifecycleEventListener(lifecycleEventListener)
@@ -3,17 +3,30 @@ 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
9
+ import com.android.billingclient.api.SkuDetailsResponseListener
10
+ import com.facebook.react.bridge.Arguments
7
11
  import com.facebook.react.bridge.Promise
8
12
  import com.facebook.react.bridge.ReactApplicationContext
13
+ import com.facebook.react.bridge.ReadableArray
14
+ import com.facebook.react.bridge.ReadableType
15
+ import com.facebook.react.bridge.WritableArray
16
+ import com.facebook.react.bridge.WritableMap
9
17
  import com.google.android.gms.common.ConnectionResult
10
18
  import com.google.android.gms.common.GoogleApiAvailability
11
19
  import io.mockk.MockKAnnotations
12
20
  import io.mockk.every
13
21
  import io.mockk.impl.annotations.MockK
22
+ import io.mockk.just
14
23
  import io.mockk.mockk
24
+ import io.mockk.mockkStatic
25
+ import io.mockk.runs
15
26
  import io.mockk.slot
16
27
  import io.mockk.verify
28
+ import org.junit.Assert.assertEquals
29
+ import org.junit.Assert.assertTrue
17
30
  import org.junit.Before
18
31
  import org.junit.Test
19
32
 
@@ -21,10 +34,13 @@ class RNIapModuleTest {
21
34
 
22
35
  @MockK
23
36
  lateinit var context: ReactApplicationContext
37
+
24
38
  @MockK
25
39
  lateinit var builder: BillingClient.Builder
40
+
26
41
  @MockK
27
42
  lateinit var billingClient: BillingClient
43
+
28
44
  @MockK
29
45
  lateinit var availability: GoogleApiAvailability
30
46
 
@@ -40,9 +56,13 @@ class RNIapModuleTest {
40
56
 
41
57
  @Test
42
58
  fun `initConnection Already connected should resolve to true`() {
59
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
60
+ module.initConnection(mockk())
61
+
43
62
  every { billingClient.isReady } returns true
44
- val promise = mockk<Promise>(relaxed = true)
45
63
 
64
+ val promise = mockk<Promise>(relaxed = true)
65
+ // Already connected
46
66
  module.initConnection(promise)
47
67
  verify(exactly = 0) { promise.reject(any(), any<String>()) }
48
68
  verify { promise.resolve(true) }
@@ -55,7 +75,7 @@ class RNIapModuleTest {
55
75
  val promise = mockk<Promise>(relaxed = true)
56
76
 
57
77
  module.initConnection(promise)
58
- verify { promise.reject(any(), any<String>()) }
78
+ verify { promise.safeReject(any(), any<String>()) }
59
79
  verify(exactly = 0) { promise.resolve(any()) }
60
80
  }
61
81
 
@@ -64,7 +84,10 @@ class RNIapModuleTest {
64
84
  every { billingClient.isReady } returns false
65
85
  val listener = slot<BillingClientStateListener>()
66
86
  every { billingClient.startConnection(capture(listener)) } answers {
67
- listener.captured.onBillingSetupFinished(BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK).build())
87
+ listener.captured.onBillingSetupFinished(
88
+ BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK)
89
+ .build()
90
+ )
68
91
  }
69
92
  every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
70
93
  val promise = mockk<Promise>(relaxed = true)
@@ -79,20 +102,25 @@ class RNIapModuleTest {
79
102
  every { billingClient.isReady } returns false
80
103
  val listener = slot<BillingClientStateListener>()
81
104
  every { billingClient.startConnection(capture(listener)) } answers {
82
- listener.captured.onBillingSetupFinished(BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR).build())
105
+ listener.captured.onBillingSetupFinished(
106
+ BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR)
107
+ .build()
108
+ )
83
109
  }
84
110
  every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
85
111
  val promise = mockk<Promise>(relaxed = true)
86
112
 
87
113
  module.initConnection(promise)
88
- verify { promise.reject(any(), any<String>()) }
114
+ verify { promise.safeReject(any(), any<String>()) }
89
115
  verify(exactly = 0) { promise.resolve(any()) }
90
116
  }
91
117
 
92
118
  @Test
93
119
  fun `endConnection resolves`() {
120
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
94
121
  val promise = mockk<Promise>(relaxed = true)
95
122
 
123
+ module.initConnection(mockk())
96
124
  module.endConnection(promise)
97
125
 
98
126
  verify { billingClient.endConnection() }
@@ -102,20 +130,131 @@ class RNIapModuleTest {
102
130
 
103
131
  @Test
104
132
  fun `flushFailedPurchasesCachedAsPending resolves to false if no pending purchases`() {
133
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
105
134
  every { billingClient.isReady } returns true
106
135
  val promise = mockk<Promise>(relaxed = true)
107
136
  val listener = slot<PurchasesResponseListener>()
108
137
  every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
109
138
  listener.captured.onQueryPurchasesResponse(BillingResult.newBuilder().build(), listOf())
110
139
  }
140
+ module.initConnection(mockk())
111
141
  module.flushFailedPurchasesCachedAsPending(promise)
112
142
 
113
143
  verify(exactly = 0) { promise.reject(any(), any<String>()) }
114
144
  verify { promise.resolve(false) } // empty list
115
145
  }
116
146
 
147
+ @Test
148
+ fun `flushFailedPurchasesCachedAsPending resolves to true if pending purchases`() {
149
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
150
+ every { billingClient.isReady } returns true
151
+ val promise = mockk<Promise>(relaxed = true)
152
+ val listener = slot<PurchasesResponseListener>()
153
+ every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
154
+ listener.captured.onQueryPurchasesResponse(
155
+ BillingResult.newBuilder().build(),
156
+ listOf(
157
+ // 4 = Pending
158
+ mockk<Purchase> {
159
+ every { purchaseState } returns 2
160
+ every { purchaseToken } returns "token"
161
+ },
162
+ Purchase("", "1")
163
+ )
164
+ )
165
+ }
166
+ val consumeListener = slot<ConsumeResponseListener>()
167
+ every { billingClient.consumeAsync(any(), capture(consumeListener)) } answers {
168
+ consumeListener.captured.onConsumeResponse(
169
+ BillingResult.newBuilder()
170
+ .setResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED).build(),
171
+ ""
172
+ )
173
+ }
174
+ module.initConnection(mockk())
175
+ module.flushFailedPurchasesCachedAsPending(promise)
176
+
177
+ verify(exactly = 0) { promise.reject(any(), any<String>()) }
178
+ verify { promise.resolve(true) } // at least one pending transactions
179
+ }
180
+
181
+ @Test
182
+ fun `ensureConnection should attempt to reconnect, if not in ready state`() {
183
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
184
+ val promise = mockk<Promise>(relaxed = true)
185
+ var isCallbackCalled = false
186
+ val callback = { _: BillingClient ->
187
+ isCallbackCalled = true
188
+ promise.resolve(true)
189
+ }
190
+
191
+ every { billingClient.isReady } returns true
192
+ val listener = slot<BillingClientStateListener>()
193
+ every { billingClient.startConnection(capture(listener)) } answers {
194
+ listener.captured.onBillingSetupFinished(
195
+ BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK)
196
+ .build()
197
+ )
198
+ }
199
+
200
+ module.ensureConnection(promise, callback)
201
+ verify { promise.resolve(true) } // at least one pending transactions
202
+ assertTrue("Should call callback", isCallbackCalled)
203
+ }
204
+
117
205
  @Test
118
206
  fun getItemsByType() {
207
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
208
+ every { billingClient.isReady } returns true
209
+ val promise = mockk<Promise>(relaxed = true)
210
+ val listener = slot<SkuDetailsResponseListener>()
211
+ every { billingClient.querySkuDetailsAsync(any(), capture(listener)) } answers {
212
+ listener.captured.onSkuDetailsResponse(
213
+ BillingResult.newBuilder().build(),
214
+ listOf(
215
+ mockk {
216
+ every { sku } returns "sku1"
217
+ every { introductoryPriceAmountMicros } returns 0
218
+ every { priceAmountMicros } returns 1
219
+ every { priceCurrencyCode } returns "USD"
220
+ every { type } returns "sub"
221
+ every { price } returns "$10.0"
222
+ every { title } returns "My product"
223
+ every { description } returns "My desc"
224
+ every { introductoryPrice } returns "$5.0"
225
+ every { zzc() } returns "com.mypackage"
226
+ every { originalPrice } returns "$13.0"
227
+ every { subscriptionPeriod } returns "3 months"
228
+ every { freeTrialPeriod } returns "1 week"
229
+ every { introductoryPriceCycles } returns 1
230
+ every { introductoryPricePeriod } returns "1"
231
+ every { iconUrl } returns "http://myicon.com/icon"
232
+ every { originalJson } returns "{}"
233
+ every { originalPriceAmountMicros } returns 2
234
+ }
235
+ )
236
+ )
237
+ }
238
+ val skus = mockk<ReadableArray>() {
239
+ every { size() } returns 1
240
+ every { getString(0) } returns "sku0"
241
+ every { getType(0) } returns ReadableType.String
242
+ }
243
+ mockkStatic(Arguments::class)
244
+
245
+ val itemsMap = mockk<WritableMap>()
246
+ val itemsArr = mockk<WritableArray>()
247
+ every { Arguments.createMap() } returns itemsMap
248
+ every { Arguments.createArray() } returns itemsArr
249
+ every { itemsMap.putString(any(), any()) } just runs
250
+ var itemsSize = 0
251
+ every { itemsArr.pushMap(any()) } answers {
252
+ itemsSize++
253
+ }
254
+ module.initConnection(mockk())
255
+ module.getItemsByType("subs", skus, promise)
256
+ verify { promise.resolve(any()) }
257
+ assertEquals(itemsSize, 1)
119
258
  }
120
259
 
121
260
  @Test
@@ -10,12 +10,13 @@ import StoreKit
10
10
 
11
11
  // Temporarily stores payment information since it is sent by the OS before RN instantiates the RNModule
12
12
  @objc(RNIapQueue)
13
- class RNIapQueue: NSObject, SKPaymentTransactionObserver {
14
- func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
13
+ public class RNIapQueue: NSObject, SKPaymentTransactionObserver {
14
+ public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
15
15
  //No-op
16
16
  }
17
17
 
18
- static let shared = RNIapQueue()
18
+ @objc
19
+ public static let shared = RNIapQueue()
19
20
 
20
21
  var queue: SKPaymentQueue? = nil;
21
22
  var payment: SKPayment? = nil;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iap",
3
- "version": "8.2.0",
3
+ "version": "8.3.0",
4
4
  "packageManager": "yarn@3.2.0",
5
5
  "description": "React Native In App Purchase Module.",
6
6
  "main": "index.js",
package/src/iap.js CHANGED
@@ -400,10 +400,19 @@ export var acknowledgePurchaseAndroid = function (token, developerPayload) {
400
400
  * @param {string} sku The product's SKU (on Android)
401
401
  * @returns {Promise<void>}
402
402
  */
403
- export var deepLinkToSubscriptionsAndroid = function (sku) {
404
- checkNativeAndroidAvailable();
405
- return Linking.openURL("https://play.google.com/store/account/subscriptions?package=".concat(RNIapModule.getPackageName(), "&sku=").concat(sku));
406
- };
403
+ export var deepLinkToSubscriptionsAndroid = function (sku) { return __awaiter(void 0, void 0, void 0, function () {
404
+ var _a, _b, _c;
405
+ return __generator(this, function (_d) {
406
+ switch (_d.label) {
407
+ case 0:
408
+ checkNativeAndroidAvailable();
409
+ _b = (_a = Linking).openURL;
410
+ _c = "https://play.google.com/store/account/subscriptions?package=".concat;
411
+ return [4 /*yield*/, RNIapModule.getPackageName()];
412
+ case 1: return [2 /*return*/, _b.apply(_a, [_c.apply("https://play.google.com/store/account/subscriptions?package=", [_d.sent(), "&sku="]).concat(sku)])];
413
+ }
414
+ });
415
+ }); };
407
416
  /**
408
417
  * Should Add Store Payment (iOS only)
409
418
  * Indicates the the App Store purchase should continue from the app instead of the App Store.