react-native-iap 8.1.3 → 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.
- package/.yarn/install-state.gz +0 -0
- package/android/build.gradle +5 -0
- package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt +20 -2
- package/android/src/play/java/com/dooboolab/RNIap/PlayUtils.kt +2 -2
- package/android/src/play/java/com/dooboolab/RNIap/PromiseUtlis.kt +33 -0
- package/android/src/play/java/com/dooboolab/RNIap/RNIapModule.kt +93 -118
- package/android/src/testPlay/java/com/dooboolab/RNIap/RNIapModuleTest.kt +263 -0
- package/package.json +1 -1
package/.yarn/install-state.gz
CHANGED
|
Binary file
|
package/android/build.gradle
CHANGED
|
@@ -50,6 +50,9 @@ android {
|
|
|
50
50
|
dimension "store"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
testOptions {
|
|
54
|
+
unitTests.returnDefaultValues = true
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
repositories {
|
|
@@ -63,6 +66,8 @@ repositories {
|
|
|
63
66
|
|
|
64
67
|
dependencies {
|
|
65
68
|
implementation 'com.facebook.react:react-native:+'
|
|
69
|
+
testImplementation 'junit:junit:4.13.1'
|
|
70
|
+
testImplementation "io.mockk:mockk:1.12.4"
|
|
66
71
|
playImplementation 'com.android.billingclient:billing:4.0.0'
|
|
67
72
|
def playServicesVersion = safeExtGet('playServicesVersion', DEFAULT_PLAY_SERVICES_VERSION)
|
|
68
73
|
playImplementation "com.google.android.gms:play-services-base:$playServicesVersion"
|
|
@@ -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.
|
|
10
|
+
promise.safeReject(errorData[0], errorData[1])
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
fun getBillingResponseData(responseCode: Int): Array<String?> {
|
|
@@ -49,7 +49,7 @@ class PlayUtils {
|
|
|
49
49
|
}
|
|
50
50
|
BillingClient.BillingResponseCode.ERROR -> {
|
|
51
51
|
errorData[0] = DoobooUtils.E_UNKNOWN
|
|
52
|
-
errorData[1] = "An unknown or unexpected error has
|
|
52
|
+
errorData[1] = "An unknown or unexpected error has occurred. Please try again later."
|
|
53
53
|
}
|
|
54
54
|
BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
|
|
55
55
|
errorData[0] = DoobooUtils.E_ALREADY_OWNED
|
|
@@ -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
|
|
@@ -32,18 +31,21 @@ import com.google.android.gms.common.GoogleApiAvailability
|
|
|
32
31
|
import java.math.BigDecimal
|
|
33
32
|
import java.util.ArrayList
|
|
34
33
|
|
|
35
|
-
class RNIapModule(
|
|
34
|
+
class RNIapModule(
|
|
35
|
+
private val reactContext: ReactApplicationContext,
|
|
36
|
+
private val builder: BillingClient.Builder = BillingClient.newBuilder(reactContext).enablePendingPurchases(),
|
|
37
|
+
private val googleApiAvailability: GoogleApiAvailability = GoogleApiAvailability.getInstance()
|
|
38
|
+
) :
|
|
36
39
|
ReactContextBaseJavaModule(reactContext),
|
|
37
40
|
PurchasesUpdatedListener {
|
|
38
41
|
|
|
39
|
-
private
|
|
40
|
-
.build()
|
|
42
|
+
private var billingClient: BillingClient = builder.setListener(this).build()
|
|
41
43
|
private val skus: MutableMap<String, SkuDetails> = mutableMapOf()
|
|
42
44
|
override fun getName(): String {
|
|
43
|
-
return
|
|
45
|
+
return TAG
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
internal fun ensureConnection(promise: Promise, callback: () -> Unit) {
|
|
47
49
|
if (billingClient.isReady) {
|
|
48
50
|
callback()
|
|
49
51
|
return
|
|
@@ -58,7 +60,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
58
60
|
},
|
|
59
61
|
{
|
|
60
62
|
if (it.size > 1 && it[0] is String && it[1] is String) {
|
|
61
|
-
promise.
|
|
63
|
+
promise.safeReject(
|
|
62
64
|
it[0] as String, it[1] as String
|
|
63
65
|
)
|
|
64
66
|
} else {
|
|
@@ -77,31 +79,25 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
77
79
|
TAG,
|
|
78
80
|
"Already initialized, you should only call initConnection() once when your app starts"
|
|
79
81
|
)
|
|
80
|
-
promise.
|
|
82
|
+
promise.safeResolve(true)
|
|
81
83
|
return
|
|
82
84
|
}
|
|
83
|
-
if (
|
|
85
|
+
if (googleApiAvailability.isGooglePlayServicesAvailable(reactContext)
|
|
84
86
|
!= ConnectionResult.SUCCESS
|
|
85
87
|
) {
|
|
86
88
|
Log.i(TAG, "Google Play Services are not available on this device")
|
|
87
|
-
promise.
|
|
89
|
+
promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Google Play Services are not available on this device")
|
|
88
90
|
return
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
billingClient = builder.setListener(this).build()
|
|
94
|
+
|
|
91
95
|
billingClient.startConnection(
|
|
92
96
|
object : BillingClientStateListener {
|
|
93
97
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
promise.resolve(true)
|
|
98
|
-
} else {
|
|
99
|
-
PlayUtils.instance
|
|
100
|
-
.rejectPromiseWithBillingError(promise, responseCode)
|
|
101
|
-
}
|
|
102
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
103
|
-
Log.e(TAG, oce.message!!)
|
|
104
|
-
}
|
|
98
|
+
if (!isValidResult(billingResult, promise)) return
|
|
99
|
+
|
|
100
|
+
promise.safeResolve(true)
|
|
105
101
|
}
|
|
106
102
|
|
|
107
103
|
override fun onBillingServiceDisconnected() {
|
|
@@ -113,7 +109,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
113
109
|
@ReactMethod
|
|
114
110
|
fun endConnection(promise: Promise) {
|
|
115
111
|
billingClient.endConnection()
|
|
116
|
-
promise.
|
|
112
|
+
promise.safeResolve(true)
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
private fun consumeItems(
|
|
@@ -138,11 +134,8 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
138
134
|
)
|
|
139
135
|
return@ConsumeResponseListener
|
|
140
136
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
144
|
-
promise.reject(oce.message)
|
|
145
|
-
}
|
|
137
|
+
|
|
138
|
+
promise.safeResolve(true)
|
|
146
139
|
}
|
|
147
140
|
billingClient.consumeAsync(consumeParams, listener)
|
|
148
141
|
}
|
|
@@ -154,26 +147,21 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
154
147
|
ensureConnection(
|
|
155
148
|
promise
|
|
156
149
|
) {
|
|
157
|
-
val array = WritableNativeArray()
|
|
158
150
|
billingClient.queryPurchasesAsync(
|
|
159
151
|
BillingClient.SkuType.INAPP
|
|
160
|
-
) {
|
|
152
|
+
) { billingResult: BillingResult, list: List<Purchase>? ->
|
|
153
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
161
154
|
if (list == null) {
|
|
162
155
|
// No purchases found
|
|
163
|
-
promise.
|
|
156
|
+
promise.safeResolve(false)
|
|
164
157
|
return@queryPurchasesAsync
|
|
165
158
|
}
|
|
166
|
-
|
|
167
|
-
for
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
pendingPurchases.add(purchase)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
if (pendingPurchases.size == 0) {
|
|
176
|
-
promise.resolve(false)
|
|
159
|
+
// we only want to try to consume PENDING items, in order to force cache-refresh
|
|
160
|
+
// for them
|
|
161
|
+
val pendingPurchases = list.filter { it.purchaseState == Purchase.PurchaseState.PENDING }
|
|
162
|
+
|
|
163
|
+
if (pendingPurchases.isEmpty()) {
|
|
164
|
+
promise.safeResolve(false)
|
|
177
165
|
return@queryPurchasesAsync
|
|
178
166
|
}
|
|
179
167
|
consumeItems(
|
|
@@ -186,7 +174,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
186
174
|
}
|
|
187
175
|
|
|
188
176
|
@ReactMethod
|
|
189
|
-
fun getItemsByType(type: String
|
|
177
|
+
fun getItemsByType(type: String, skuArr: ReadableArray, promise: Promise) {
|
|
190
178
|
ensureConnection(
|
|
191
179
|
promise
|
|
192
180
|
) {
|
|
@@ -198,22 +186,18 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
198
186
|
}
|
|
199
187
|
}
|
|
200
188
|
val params = SkuDetailsParams.newBuilder()
|
|
201
|
-
params.setSkusList(skuList).setType(type
|
|
189
|
+
params.setSkusList(skuList).setType(type)
|
|
202
190
|
billingClient.querySkuDetailsAsync(
|
|
203
191
|
params.build()
|
|
204
192
|
) { billingResult: BillingResult, skuDetailsList: List<SkuDetails>? ->
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
PlayUtils.instance
|
|
208
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
209
|
-
return@querySkuDetailsAsync
|
|
210
|
-
}
|
|
193
|
+
if (!isValidResult(billingResult, promise)) return@querySkuDetailsAsync
|
|
194
|
+
|
|
211
195
|
if (skuDetailsList != null) {
|
|
212
196
|
for (sku in skuDetailsList) {
|
|
213
197
|
skus[sku.sku] = sku
|
|
214
198
|
}
|
|
215
199
|
}
|
|
216
|
-
val items =
|
|
200
|
+
val items = Arguments.createArray()
|
|
217
201
|
for (skuDetails in skuDetailsList!!) {
|
|
218
202
|
val item = Arguments.createMap()
|
|
219
203
|
item.putString("productId", skuDetails.sku)
|
|
@@ -263,15 +247,27 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
263
247
|
item.putString("originalPrice", originalPrice)
|
|
264
248
|
items.pushMap(item)
|
|
265
249
|
}
|
|
266
|
-
|
|
267
|
-
promise.resolve(items)
|
|
268
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
269
|
-
Log.e(TAG, oce.message!!)
|
|
270
|
-
}
|
|
250
|
+
promise.safeResolve(items)
|
|
271
251
|
}
|
|
272
252
|
}
|
|
273
253
|
}
|
|
274
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
|
+
|
|
275
271
|
@ReactMethod
|
|
276
272
|
fun getAvailableItemsByType(type: String, promise: Promise) {
|
|
277
273
|
ensureConnection(
|
|
@@ -280,7 +276,8 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
280
276
|
val items = WritableNativeArray()
|
|
281
277
|
billingClient.queryPurchasesAsync(
|
|
282
278
|
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
283
|
-
) { billingResult: BillingResult
|
|
279
|
+
) { billingResult: BillingResult, purchases: List<Purchase>? ->
|
|
280
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
284
281
|
if (purchases != null) {
|
|
285
282
|
for (i in purchases.indices) {
|
|
286
283
|
val purchase = purchases[i]
|
|
@@ -310,11 +307,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
310
307
|
items.pushMap(item)
|
|
311
308
|
}
|
|
312
309
|
}
|
|
313
|
-
|
|
314
|
-
promise.resolve(items)
|
|
315
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
316
|
-
Log.e(TAG, oce.message!!)
|
|
317
|
-
}
|
|
310
|
+
promise.safeResolve(items)
|
|
318
311
|
}
|
|
319
312
|
}
|
|
320
313
|
}
|
|
@@ -327,16 +320,12 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
327
320
|
billingClient.queryPurchaseHistoryAsync(
|
|
328
321
|
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
329
322
|
) { billingResult, purchaseHistoryRecordList ->
|
|
330
|
-
if (billingResult
|
|
331
|
-
|
|
332
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
333
|
-
return@queryPurchaseHistoryAsync
|
|
334
|
-
}
|
|
323
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchaseHistoryAsync
|
|
324
|
+
|
|
335
325
|
Log.d(TAG, purchaseHistoryRecordList.toString())
|
|
336
326
|
val items = Arguments.createArray()
|
|
337
|
-
|
|
327
|
+
purchaseHistoryRecordList?.forEach { purchase ->
|
|
338
328
|
val item = Arguments.createMap()
|
|
339
|
-
val purchase = purchaseHistoryRecordList[i]
|
|
340
329
|
item.putString("productId", purchase.skus[0])
|
|
341
330
|
item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
342
331
|
item.putString("transactionReceipt", purchase.originalJson)
|
|
@@ -346,11 +335,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
346
335
|
item.putString("developerPayload", purchase.developerPayload)
|
|
347
336
|
items.pushMap(item)
|
|
348
337
|
}
|
|
349
|
-
|
|
350
|
-
promise.resolve(items)
|
|
351
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
352
|
-
Log.e(TAG, oce.message!!)
|
|
353
|
-
}
|
|
338
|
+
promise.safeResolve(items)
|
|
354
339
|
}
|
|
355
340
|
}
|
|
356
341
|
}
|
|
@@ -367,7 +352,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
367
352
|
) {
|
|
368
353
|
val activity = currentActivity
|
|
369
354
|
if (activity == null) {
|
|
370
|
-
promise.
|
|
355
|
+
promise.safeReject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
|
|
371
356
|
return
|
|
372
357
|
}
|
|
373
358
|
ensureConnection(
|
|
@@ -387,7 +372,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
387
372
|
error.putString("message", debugMessage)
|
|
388
373
|
error.putString("productId", sku)
|
|
389
374
|
sendEvent(reactContext, "purchase-error", error)
|
|
390
|
-
promise.
|
|
375
|
+
promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
|
|
391
376
|
return@ensureConnection
|
|
392
377
|
}
|
|
393
378
|
builder.setSkuDetails(selectedSku)
|
|
@@ -420,7 +405,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
420
405
|
error.putString("message", debugMessage)
|
|
421
406
|
error.putString("productId", sku)
|
|
422
407
|
sendEvent(reactContext, "purchase-error", error)
|
|
423
|
-
promise.
|
|
408
|
+
promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
|
|
424
409
|
return@ensureConnection
|
|
425
410
|
}
|
|
426
411
|
} else if (prorationMode
|
|
@@ -457,8 +442,14 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
457
442
|
}
|
|
458
443
|
val flowParams = builder.build()
|
|
459
444
|
val billingResult = billingClient.launchBillingFlow(activity, flowParams)
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
}
|
|
462
453
|
}
|
|
463
454
|
}
|
|
464
455
|
|
|
@@ -478,22 +469,16 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
478
469
|
billingClient.acknowledgePurchase(
|
|
479
470
|
acknowledgePurchaseParams
|
|
480
471
|
) { billingResult: BillingResult ->
|
|
481
|
-
if (billingResult
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
map.putString("code", errorData[0])
|
|
492
|
-
map.putString("message", errorData[1])
|
|
493
|
-
promise.resolve(map)
|
|
494
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
495
|
-
Log.e(TAG, oce.message!!)
|
|
496
|
-
}
|
|
472
|
+
if (!isValidResult(billingResult, promise)) return@acknowledgePurchase
|
|
473
|
+
|
|
474
|
+
val map = Arguments.createMap()
|
|
475
|
+
map.putInt("responseCode", billingResult.responseCode)
|
|
476
|
+
map.putString("debugMessage", billingResult.debugMessage)
|
|
477
|
+
val errorData: Array<String?> = PlayUtils.instance
|
|
478
|
+
.getBillingResponseData(billingResult.responseCode)
|
|
479
|
+
map.putString("code", errorData[0])
|
|
480
|
+
map.putString("message", errorData[1])
|
|
481
|
+
promise.safeResolve(map)
|
|
497
482
|
}
|
|
498
483
|
}
|
|
499
484
|
}
|
|
@@ -511,22 +496,16 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
511
496
|
billingClient.consumeAsync(
|
|
512
497
|
params
|
|
513
498
|
) { billingResult: BillingResult, purchaseToken: String? ->
|
|
514
|
-
if (billingResult
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
map.putString("code", errorData[0])
|
|
525
|
-
map.putString("message", errorData[1])
|
|
526
|
-
promise.resolve(map)
|
|
527
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
528
|
-
promise.reject(oce.message)
|
|
529
|
-
}
|
|
499
|
+
if (!isValidResult(billingResult, promise)) return@consumeAsync
|
|
500
|
+
|
|
501
|
+
val map = Arguments.createMap()
|
|
502
|
+
map.putInt("responseCode", billingResult.responseCode)
|
|
503
|
+
map.putString("debugMessage", billingResult.debugMessage)
|
|
504
|
+
val errorData: Array<String?> = PlayUtils.instance
|
|
505
|
+
.getBillingResponseData(billingResult.responseCode)
|
|
506
|
+
map.putString("code", errorData[0])
|
|
507
|
+
map.putString("message", errorData[1])
|
|
508
|
+
promise.safeResolve(map)
|
|
530
509
|
}
|
|
531
510
|
}
|
|
532
511
|
}
|
|
@@ -602,18 +581,14 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
|
|
|
602
581
|
for (type in types) {
|
|
603
582
|
billingClient.queryPurchasesAsync(
|
|
604
583
|
type
|
|
605
|
-
) { billingResult: BillingResult, list: List<Purchase
|
|
606
|
-
|
|
584
|
+
) { billingResult: BillingResult, list: List<Purchase> ->
|
|
585
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
607
586
|
|
|
608
|
-
|
|
609
|
-
if (!purchase.isAcknowledged) {
|
|
610
|
-
unacknowledgedPurchases.add(purchase)
|
|
611
|
-
}
|
|
612
|
-
}
|
|
587
|
+
val unacknowledgedPurchases = list.filter { !it.isAcknowledged }
|
|
613
588
|
onPurchasesUpdated(billingResult, unacknowledgedPurchases)
|
|
614
589
|
}
|
|
615
590
|
}
|
|
616
|
-
promise.
|
|
591
|
+
promise.safeResolve(true)
|
|
617
592
|
}
|
|
618
593
|
}
|
|
619
594
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
package com.dooboolab.RNIap
|
|
2
|
+
|
|
3
|
+
import com.android.billingclient.api.BillingClient
|
|
4
|
+
import com.android.billingclient.api.BillingClientStateListener
|
|
5
|
+
import com.android.billingclient.api.BillingResult
|
|
6
|
+
import com.android.billingclient.api.ConsumeResponseListener
|
|
7
|
+
import com.android.billingclient.api.Purchase
|
|
8
|
+
import com.android.billingclient.api.PurchasesResponseListener
|
|
9
|
+
import com.android.billingclient.api.SkuDetailsResponseListener
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
11
|
+
import com.facebook.react.bridge.Promise
|
|
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
|
|
16
|
+
import com.google.android.gms.common.ConnectionResult
|
|
17
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
18
|
+
import io.mockk.MockKAnnotations
|
|
19
|
+
import io.mockk.every
|
|
20
|
+
import io.mockk.impl.annotations.MockK
|
|
21
|
+
import io.mockk.just
|
|
22
|
+
import io.mockk.mockk
|
|
23
|
+
import io.mockk.mockkStatic
|
|
24
|
+
import io.mockk.runs
|
|
25
|
+
import io.mockk.slot
|
|
26
|
+
import io.mockk.verify
|
|
27
|
+
import org.junit.Assert.assertEquals
|
|
28
|
+
import org.junit.Assert.assertTrue
|
|
29
|
+
import org.junit.Before
|
|
30
|
+
import org.junit.Test
|
|
31
|
+
|
|
32
|
+
class RNIapModuleTest {
|
|
33
|
+
|
|
34
|
+
@MockK
|
|
35
|
+
lateinit var context: ReactApplicationContext
|
|
36
|
+
|
|
37
|
+
@MockK
|
|
38
|
+
lateinit var builder: BillingClient.Builder
|
|
39
|
+
|
|
40
|
+
@MockK
|
|
41
|
+
lateinit var billingClient: BillingClient
|
|
42
|
+
|
|
43
|
+
@MockK
|
|
44
|
+
lateinit var availability: GoogleApiAvailability
|
|
45
|
+
|
|
46
|
+
private lateinit var module: RNIapModule
|
|
47
|
+
|
|
48
|
+
@Before
|
|
49
|
+
fun setUp() {
|
|
50
|
+
MockKAnnotations.init(this, relaxUnitFun = true)
|
|
51
|
+
every { builder.setListener(any()) } returns builder
|
|
52
|
+
every { builder.build() } returns billingClient
|
|
53
|
+
module = RNIapModule(context, builder, availability)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Test
|
|
57
|
+
fun `initConnection Already connected should resolve to true`() {
|
|
58
|
+
every { billingClient.isReady } returns true
|
|
59
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
60
|
+
|
|
61
|
+
module.initConnection(promise)
|
|
62
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
63
|
+
verify { promise.resolve(true) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun `initConnection Play Services not available on device should reject`() {
|
|
68
|
+
every { billingClient.isReady } returns false
|
|
69
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.DEVELOPER_ERROR
|
|
70
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
71
|
+
|
|
72
|
+
module.initConnection(promise)
|
|
73
|
+
verify { promise.safeReject(any(), any<String>()) }
|
|
74
|
+
verify(exactly = 0) { promise.resolve(any()) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
fun `initConnection start new connection succeeds`() {
|
|
79
|
+
every { billingClient.isReady } returns false
|
|
80
|
+
val listener = slot<BillingClientStateListener>()
|
|
81
|
+
every { billingClient.startConnection(capture(listener)) } answers {
|
|
82
|
+
listener.captured.onBillingSetupFinished(
|
|
83
|
+
BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK)
|
|
84
|
+
.build()
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
88
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
89
|
+
|
|
90
|
+
module.initConnection(promise)
|
|
91
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
92
|
+
verify { promise.resolve(any()) }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
fun `initConnection start new connection fails`() {
|
|
97
|
+
every { billingClient.isReady } returns false
|
|
98
|
+
val listener = slot<BillingClientStateListener>()
|
|
99
|
+
every { billingClient.startConnection(capture(listener)) } answers {
|
|
100
|
+
listener.captured.onBillingSetupFinished(
|
|
101
|
+
BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR)
|
|
102
|
+
.build()
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
106
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
107
|
+
|
|
108
|
+
module.initConnection(promise)
|
|
109
|
+
verify { promise.safeReject(any(), any<String>()) }
|
|
110
|
+
verify(exactly = 0) { promise.resolve(any()) }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
fun `endConnection resolves`() {
|
|
115
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
116
|
+
|
|
117
|
+
module.endConnection(promise)
|
|
118
|
+
|
|
119
|
+
verify { billingClient.endConnection() }
|
|
120
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
121
|
+
verify { promise.resolve(true) }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Test
|
|
125
|
+
fun `flushFailedPurchasesCachedAsPending resolves to false if no pending purchases`() {
|
|
126
|
+
every { billingClient.isReady } returns true
|
|
127
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
128
|
+
val listener = slot<PurchasesResponseListener>()
|
|
129
|
+
every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
|
|
130
|
+
listener.captured.onQueryPurchasesResponse(BillingResult.newBuilder().build(), listOf())
|
|
131
|
+
}
|
|
132
|
+
module.flushFailedPurchasesCachedAsPending(promise)
|
|
133
|
+
|
|
134
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
135
|
+
verify { promise.resolve(false) } // empty list
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Test
|
|
139
|
+
fun `flushFailedPurchasesCachedAsPending resolves to true if pending purchases`() {
|
|
140
|
+
every { billingClient.isReady } returns true
|
|
141
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
142
|
+
val listener = slot<PurchasesResponseListener>()
|
|
143
|
+
every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
|
|
144
|
+
listener.captured.onQueryPurchasesResponse(
|
|
145
|
+
BillingResult.newBuilder().build(),
|
|
146
|
+
listOf(
|
|
147
|
+
// 4 = Pending
|
|
148
|
+
mockk<Purchase> {
|
|
149
|
+
every { purchaseState } returns 2
|
|
150
|
+
every { purchaseToken } returns "token"
|
|
151
|
+
},
|
|
152
|
+
Purchase("", "1")
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
val consumeListener = slot<ConsumeResponseListener>()
|
|
157
|
+
every { billingClient.consumeAsync(any(), capture(consumeListener)) } answers {
|
|
158
|
+
consumeListener.captured.onConsumeResponse(
|
|
159
|
+
BillingResult.newBuilder()
|
|
160
|
+
.setResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED).build(),
|
|
161
|
+
""
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.flushFailedPurchasesCachedAsPending(promise)
|
|
166
|
+
|
|
167
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
168
|
+
verify { promise.resolve(true) } // at least one pending transactions
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Test
|
|
172
|
+
fun `ensureConnection should attempt to reconnect, if not in ready state`() {
|
|
173
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
174
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
175
|
+
var isCallbackCalled = false
|
|
176
|
+
val callback = {
|
|
177
|
+
isCallbackCalled = true
|
|
178
|
+
promise.resolve(true)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
every { billingClient.isReady } returns false andThen true
|
|
182
|
+
module.ensureConnection(promise, callback)
|
|
183
|
+
verify { promise.resolve(true) } // at least one pending transactions
|
|
184
|
+
assertTrue("Should call callback", isCallbackCalled)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@Test
|
|
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)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@Test
|
|
241
|
+
fun getAvailableItemsByType() {
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@Test
|
|
245
|
+
fun getPurchaseHistoryByType() {
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@Test
|
|
249
|
+
fun buyItemByType() {
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@Test
|
|
253
|
+
fun acknowledgePurchase() {
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@Test
|
|
257
|
+
fun consumeProduct() {
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@Test
|
|
261
|
+
fun onPurchasesUpdated() {
|
|
262
|
+
}
|
|
263
|
+
}
|