react-native-iap 8.1.2 → 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.
- package/.yarn/install-state.gz +0 -0
- package/android/build.gradle +5 -0
- 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 +360 -425
- package/android/src/testPlay/java/com/dooboolab/RNIap/RNIapModuleTest.kt +192 -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.12'
|
|
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"
|
|
@@ -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,8 +15,8 @@ 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
|
|
19
|
+
import com.facebook.react.bridge.PromiseImpl
|
|
20
20
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
21
21
|
import com.facebook.react.bridge.ReactContext
|
|
22
22
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
@@ -28,97 +28,93 @@ import com.facebook.react.bridge.WritableNativeMap
|
|
|
28
28
|
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
|
|
29
29
|
import com.google.android.gms.common.ConnectionResult
|
|
30
30
|
import com.google.android.gms.common.GoogleApiAvailability
|
|
31
|
-
import java.lang.Exception
|
|
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
|
-
|
|
39
|
-
private
|
|
40
|
-
private
|
|
41
|
-
private val skus: MutableMap<String, SkuDetails>
|
|
41
|
+
|
|
42
|
+
private var billingClient: BillingClient = builder.setListener(this).build()
|
|
43
|
+
private val skus: MutableMap<String, SkuDetails> = mutableMapOf()
|
|
42
44
|
override fun getName(): String {
|
|
43
45
|
return "RNIapModule"
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
private fun ensureConnection(promise: Promise, callback: EnsureConnectionCallback) {
|
|
51
|
-
val billingClient = billingClientCache
|
|
52
|
-
if (billingClient?.isReady == true) {
|
|
53
|
-
callback.run(billingClient)
|
|
48
|
+
internal fun ensureConnection(promise: Promise, callback: () -> Unit) {
|
|
49
|
+
if (billingClient.isReady) {
|
|
50
|
+
callback()
|
|
54
51
|
return
|
|
52
|
+
} else {
|
|
53
|
+
val nested = PromiseImpl(
|
|
54
|
+
{
|
|
55
|
+
if (it.isNotEmpty() && it[0] is Boolean && it[0] as Boolean) {
|
|
56
|
+
callback()
|
|
57
|
+
} else {
|
|
58
|
+
Log.i(TAG, "Incorrect parameter in resolve")
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
if (it.size > 1 && it[0] is String && it[1] is String) {
|
|
63
|
+
promise.safeReject(
|
|
64
|
+
it[0] as String, it[1] as String
|
|
65
|
+
)
|
|
66
|
+
} else {
|
|
67
|
+
Log.i(TAG, "Incorrect parameters in reject")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
initConnection(nested)
|
|
55
72
|
}
|
|
56
|
-
promise.reject(DoobooUtils.E_NOT_PREPARED, "Not initialized, Please call initConnection()")
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
@ReactMethod
|
|
60
76
|
fun initConnection(promise: Promise) {
|
|
61
|
-
if (
|
|
77
|
+
if (billingClient.isReady) {
|
|
62
78
|
Log.i(
|
|
63
79
|
TAG,
|
|
64
80
|
"Already initialized, you should only call initConnection() once when your app starts"
|
|
65
81
|
)
|
|
66
|
-
promise.
|
|
82
|
+
promise.safeResolve(true)
|
|
67
83
|
return
|
|
68
84
|
}
|
|
69
|
-
if (
|
|
85
|
+
if (googleApiAvailability.isGooglePlayServicesAvailable(reactContext)
|
|
70
86
|
!= ConnectionResult.SUCCESS
|
|
71
87
|
) {
|
|
72
88
|
Log.i(TAG, "Google Play Services are not available on this device")
|
|
73
|
-
promise.
|
|
89
|
+
promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Google Play Services are not available on this device")
|
|
74
90
|
return
|
|
75
91
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
|
|
93
|
+
billingClient = builder.setListener(this).build()
|
|
94
|
+
|
|
95
|
+
billingClient.startConnection(
|
|
80
96
|
object : BillingClientStateListener {
|
|
81
97
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
82
98
|
val responseCode = billingResult.responseCode
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
91
|
-
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)
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
override fun onBillingServiceDisconnected() {
|
|
96
|
-
|
|
97
|
-
billingClientCache = null
|
|
98
|
-
promise.reject("initConnection", "Billing service disconnected")
|
|
99
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
100
|
-
Log.e(TAG, oce.message!!)
|
|
101
|
-
}
|
|
109
|
+
Log.i(TAG, "Billing service disconnected")
|
|
102
110
|
}
|
|
103
111
|
})
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
@ReactMethod
|
|
107
115
|
fun endConnection(promise: Promise) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
billingClientCache!!.endConnection()
|
|
111
|
-
null
|
|
112
|
-
} catch (e: Exception) {
|
|
113
|
-
promise.reject("endConnection", e.message)
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
promise.resolve(true)
|
|
119
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
120
|
-
Log.e(TAG, oce.message!!)
|
|
121
|
-
}
|
|
116
|
+
billingClient.endConnection()
|
|
117
|
+
promise.safeResolve(true)
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
private fun consumeItems(
|
|
@@ -128,251 +124,215 @@ class RNIapModule(reactContext: ReactApplicationContext) :
|
|
|
128
124
|
) {
|
|
129
125
|
for (purchase in purchases) {
|
|
130
126
|
ensureConnection(
|
|
131
|
-
promise
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
promise.resolve(true)
|
|
149
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
150
|
-
promise.reject(oce.message)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
billingClient.consumeAsync(consumeParams, listener)
|
|
127
|
+
promise
|
|
128
|
+
) {
|
|
129
|
+
val consumeParams =
|
|
130
|
+
ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
|
|
131
|
+
.build()
|
|
132
|
+
val listener =
|
|
133
|
+
ConsumeResponseListener { billingResult: BillingResult, outToken: String? ->
|
|
134
|
+
if (billingResult.responseCode != expectedResponseCode) {
|
|
135
|
+
PlayUtils.instance
|
|
136
|
+
.rejectPromiseWithBillingError(
|
|
137
|
+
promise,
|
|
138
|
+
billingResult.responseCode
|
|
139
|
+
)
|
|
140
|
+
return@ConsumeResponseListener
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
promise.safeResolve(true)
|
|
154
144
|
}
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
billingClient.consumeAsync(consumeParams, listener)
|
|
146
|
+
}
|
|
157
147
|
}
|
|
158
148
|
}
|
|
159
149
|
|
|
160
150
|
@ReactMethod
|
|
161
151
|
fun flushFailedPurchasesCachedAsPending(promise: Promise) {
|
|
162
152
|
ensureConnection(
|
|
163
|
-
promise
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
promise.resolve(false)
|
|
173
|
-
return@queryPurchasesAsync
|
|
174
|
-
}
|
|
175
|
-
val pendingPurchases: MutableList<Purchase> = ArrayList()
|
|
176
|
-
for (purchase in list) {
|
|
177
|
-
// we only want to try to consume PENDING items, in order to force cache-refresh
|
|
178
|
-
// for
|
|
179
|
-
// them
|
|
180
|
-
if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
|
|
181
|
-
pendingPurchases.add(purchase)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (pendingPurchases.size == 0) {
|
|
185
|
-
promise.resolve(false)
|
|
186
|
-
return@queryPurchasesAsync
|
|
187
|
-
}
|
|
188
|
-
consumeItems(
|
|
189
|
-
pendingPurchases,
|
|
190
|
-
promise,
|
|
191
|
-
BillingClient.BillingResponseCode.ITEM_NOT_OWNED
|
|
192
|
-
)
|
|
193
|
-
}
|
|
153
|
+
promise
|
|
154
|
+
) {
|
|
155
|
+
billingClient.queryPurchasesAsync(
|
|
156
|
+
BillingClient.SkuType.INAPP
|
|
157
|
+
) { _: BillingResult?, list: List<Purchase>? ->
|
|
158
|
+
if (list == null) {
|
|
159
|
+
// No purchases found
|
|
160
|
+
promise.safeResolve(false)
|
|
161
|
+
return@queryPurchasesAsync
|
|
194
162
|
}
|
|
163
|
+
// we only want to try to consume PENDING items, in order to force cache-refresh
|
|
164
|
+
// for them
|
|
165
|
+
val pendingPurchases = list.filter { it.purchaseState == Purchase.PurchaseState.PENDING }
|
|
166
|
+
|
|
167
|
+
if (pendingPurchases.isEmpty()) {
|
|
168
|
+
promise.safeResolve(false)
|
|
169
|
+
return@queryPurchasesAsync
|
|
170
|
+
}
|
|
171
|
+
consumeItems(
|
|
172
|
+
pendingPurchases,
|
|
173
|
+
promise,
|
|
174
|
+
BillingClient.BillingResponseCode.ITEM_NOT_OWNED
|
|
175
|
+
)
|
|
195
176
|
}
|
|
196
|
-
|
|
177
|
+
}
|
|
197
178
|
}
|
|
198
179
|
|
|
199
180
|
@ReactMethod
|
|
200
181
|
fun getItemsByType(type: String?, skuArr: ReadableArray, promise: Promise) {
|
|
201
182
|
ensureConnection(
|
|
202
|
-
promise
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
skus.put(sku.getSku(), sku)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
val items = WritableNativeArray()
|
|
229
|
-
for (skuDetails in skuDetailsList!!) {
|
|
230
|
-
val item = Arguments.createMap()
|
|
231
|
-
item.putString("productId", skuDetails.sku)
|
|
232
|
-
val introductoryPriceMicros = skuDetails.introductoryPriceAmountMicros
|
|
233
|
-
val priceAmountMicros = skuDetails.priceAmountMicros
|
|
234
|
-
// Use valueOf instead of constructors.
|
|
235
|
-
// See:
|
|
236
|
-
// https://www.javaworld.com/article/2073176/caution--double-to-bigdecimal-in-java.html
|
|
237
|
-
val priceAmount = BigDecimal.valueOf(priceAmountMicros)
|
|
238
|
-
val introductoryPriceAmount =
|
|
239
|
-
BigDecimal.valueOf(introductoryPriceMicros)
|
|
240
|
-
val microUnitsDivisor = BigDecimal.valueOf(1000000)
|
|
241
|
-
val price = priceAmount.divide(microUnitsDivisor).toString()
|
|
242
|
-
val introductoryPriceAsAmountAndroid =
|
|
243
|
-
introductoryPriceAmount.divide(microUnitsDivisor).toString()
|
|
244
|
-
item.putString("price", price)
|
|
245
|
-
item.putString("currency", skuDetails.priceCurrencyCode)
|
|
246
|
-
item.putString("type", skuDetails.type)
|
|
247
|
-
item.putString("localizedPrice", skuDetails.price)
|
|
248
|
-
item.putString("title", skuDetails.title)
|
|
249
|
-
item.putString("description", skuDetails.description)
|
|
250
|
-
item.putString("introductoryPrice", skuDetails.introductoryPrice)
|
|
251
|
-
item.putString("typeAndroid", skuDetails.type)
|
|
252
|
-
item.putString("packageNameAndroid", skuDetails.zzc())
|
|
253
|
-
item.putString("originalPriceAndroid", skuDetails.originalPrice)
|
|
254
|
-
item.putString(
|
|
255
|
-
"subscriptionPeriodAndroid",
|
|
256
|
-
skuDetails.subscriptionPeriod
|
|
257
|
-
)
|
|
258
|
-
item.putString("freeTrialPeriodAndroid", skuDetails.freeTrialPeriod)
|
|
259
|
-
item.putString(
|
|
260
|
-
"introductoryPriceCyclesAndroid",
|
|
261
|
-
skuDetails.introductoryPriceCycles.toString()
|
|
262
|
-
)
|
|
263
|
-
item.putString(
|
|
264
|
-
"introductoryPricePeriodAndroid", skuDetails.introductoryPricePeriod
|
|
265
|
-
)
|
|
266
|
-
item.putString(
|
|
267
|
-
"introductoryPriceAsAmountAndroid", introductoryPriceAsAmountAndroid
|
|
268
|
-
)
|
|
269
|
-
item.putString("iconUrl", skuDetails.iconUrl)
|
|
270
|
-
item.putString("originalJson", skuDetails.originalJson)
|
|
271
|
-
val originalPriceAmountMicros =
|
|
272
|
-
BigDecimal.valueOf(skuDetails.originalPriceAmountMicros)
|
|
273
|
-
val originalPrice =
|
|
274
|
-
originalPriceAmountMicros.divide(microUnitsDivisor).toString()
|
|
275
|
-
item.putString("originalPrice", originalPrice)
|
|
276
|
-
items.pushMap(item)
|
|
277
|
-
}
|
|
278
|
-
try {
|
|
279
|
-
promise.resolve(items)
|
|
280
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
281
|
-
Log.e(TAG, oce.message!!)
|
|
282
|
-
}
|
|
183
|
+
promise
|
|
184
|
+
) {
|
|
185
|
+
val skuList = ArrayList<String>()
|
|
186
|
+
for (i in 0 until skuArr.size()) {
|
|
187
|
+
val sku = skuArr.getString(i)
|
|
188
|
+
if (sku is String) {
|
|
189
|
+
skuList.add(sku)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
val params = SkuDetailsParams.newBuilder()
|
|
193
|
+
params.setSkusList(skuList).setType(type!!)
|
|
194
|
+
billingClient.querySkuDetailsAsync(
|
|
195
|
+
params.build()
|
|
196
|
+
) { 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
|
+
}
|
|
203
|
+
if (skuDetailsList != null) {
|
|
204
|
+
for (sku in skuDetailsList) {
|
|
205
|
+
skus[sku.sku] = sku
|
|
283
206
|
}
|
|
284
207
|
}
|
|
208
|
+
val items = WritableNativeArray()
|
|
209
|
+
for (skuDetails in skuDetailsList!!) {
|
|
210
|
+
val item = Arguments.createMap()
|
|
211
|
+
item.putString("productId", skuDetails.sku)
|
|
212
|
+
val introductoryPriceMicros = skuDetails.introductoryPriceAmountMicros
|
|
213
|
+
val priceAmountMicros = skuDetails.priceAmountMicros
|
|
214
|
+
// Use valueOf instead of constructors.
|
|
215
|
+
// See:
|
|
216
|
+
// https://www.javaworld.com/article/2073176/caution--double-to-bigdecimal-in-java.html
|
|
217
|
+
val priceAmount = BigDecimal.valueOf(priceAmountMicros)
|
|
218
|
+
val introductoryPriceAmount =
|
|
219
|
+
BigDecimal.valueOf(introductoryPriceMicros)
|
|
220
|
+
val microUnitsDivisor = BigDecimal.valueOf(1000000)
|
|
221
|
+
val price = priceAmount.divide(microUnitsDivisor).toString()
|
|
222
|
+
val introductoryPriceAsAmountAndroid =
|
|
223
|
+
introductoryPriceAmount.divide(microUnitsDivisor).toString()
|
|
224
|
+
item.putString("price", price)
|
|
225
|
+
item.putString("currency", skuDetails.priceCurrencyCode)
|
|
226
|
+
item.putString("type", skuDetails.type)
|
|
227
|
+
item.putString("localizedPrice", skuDetails.price)
|
|
228
|
+
item.putString("title", skuDetails.title)
|
|
229
|
+
item.putString("description", skuDetails.description)
|
|
230
|
+
item.putString("introductoryPrice", skuDetails.introductoryPrice)
|
|
231
|
+
item.putString("typeAndroid", skuDetails.type)
|
|
232
|
+
item.putString("packageNameAndroid", skuDetails.zzc())
|
|
233
|
+
item.putString("originalPriceAndroid", skuDetails.originalPrice)
|
|
234
|
+
item.putString(
|
|
235
|
+
"subscriptionPeriodAndroid",
|
|
236
|
+
skuDetails.subscriptionPeriod
|
|
237
|
+
)
|
|
238
|
+
item.putString("freeTrialPeriodAndroid", skuDetails.freeTrialPeriod)
|
|
239
|
+
item.putString(
|
|
240
|
+
"introductoryPriceCyclesAndroid",
|
|
241
|
+
skuDetails.introductoryPriceCycles.toString()
|
|
242
|
+
)
|
|
243
|
+
item.putString(
|
|
244
|
+
"introductoryPricePeriodAndroid", skuDetails.introductoryPricePeriod
|
|
245
|
+
)
|
|
246
|
+
item.putString(
|
|
247
|
+
"introductoryPriceAsAmountAndroid", introductoryPriceAsAmountAndroid
|
|
248
|
+
)
|
|
249
|
+
item.putString("iconUrl", skuDetails.iconUrl)
|
|
250
|
+
item.putString("originalJson", skuDetails.originalJson)
|
|
251
|
+
val originalPriceAmountMicros =
|
|
252
|
+
BigDecimal.valueOf(skuDetails.originalPriceAmountMicros)
|
|
253
|
+
val originalPrice =
|
|
254
|
+
originalPriceAmountMicros.divide(microUnitsDivisor).toString()
|
|
255
|
+
item.putString("originalPrice", originalPrice)
|
|
256
|
+
items.pushMap(item)
|
|
257
|
+
}
|
|
258
|
+
promise.safeResolve(items)
|
|
285
259
|
}
|
|
286
|
-
|
|
260
|
+
}
|
|
287
261
|
}
|
|
288
262
|
|
|
289
263
|
@ReactMethod
|
|
290
264
|
fun getAvailableItemsByType(type: String, promise: Promise) {
|
|
291
265
|
ensureConnection(
|
|
292
|
-
promise
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
item.putBoolean("autoRenewingAndroid", purchase.isAutoRenewing)
|
|
324
|
-
}
|
|
325
|
-
items.pushMap(item)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
try {
|
|
329
|
-
promise.resolve(items)
|
|
330
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
331
|
-
Log.e(TAG, oce.message!!)
|
|
266
|
+
promise
|
|
267
|
+
) {
|
|
268
|
+
val items = WritableNativeArray()
|
|
269
|
+
billingClient.queryPurchasesAsync(
|
|
270
|
+
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
271
|
+
) { billingResult: BillingResult?, purchases: List<Purchase>? ->
|
|
272
|
+
if (purchases != null) {
|
|
273
|
+
for (i in purchases.indices) {
|
|
274
|
+
val purchase = purchases[i]
|
|
275
|
+
val item = WritableNativeMap()
|
|
276
|
+
item.putString("productId", purchase.skus[0])
|
|
277
|
+
item.putString("transactionId", purchase.orderId)
|
|
278
|
+
item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
279
|
+
item.putString("transactionReceipt", purchase.originalJson)
|
|
280
|
+
item.putString("orderId", purchase.orderId)
|
|
281
|
+
item.putString("purchaseToken", purchase.purchaseToken)
|
|
282
|
+
item.putString("developerPayloadAndroid", purchase.developerPayload)
|
|
283
|
+
item.putString("signatureAndroid", purchase.signature)
|
|
284
|
+
item.putInt("purchaseStateAndroid", purchase.purchaseState)
|
|
285
|
+
item.putBoolean("isAcknowledgedAndroid", purchase.isAcknowledged)
|
|
286
|
+
item.putString("packageNameAndroid", purchase.packageName)
|
|
287
|
+
item.putString(
|
|
288
|
+
"obfuscatedAccountIdAndroid",
|
|
289
|
+
purchase.accountIdentifiers!!.obfuscatedAccountId
|
|
290
|
+
)
|
|
291
|
+
item.putString(
|
|
292
|
+
"obfuscatedProfileIdAndroid",
|
|
293
|
+
purchase.accountIdentifiers!!.obfuscatedProfileId
|
|
294
|
+
)
|
|
295
|
+
if (type == BillingClient.SkuType.SUBS) {
|
|
296
|
+
item.putBoolean("autoRenewingAndroid", purchase.isAutoRenewing)
|
|
332
297
|
}
|
|
298
|
+
items.pushMap(item)
|
|
333
299
|
}
|
|
334
300
|
}
|
|
301
|
+
promise.safeResolve(items)
|
|
335
302
|
}
|
|
336
|
-
|
|
303
|
+
}
|
|
337
304
|
}
|
|
338
305
|
|
|
339
306
|
@ReactMethod
|
|
340
307
|
fun getPurchaseHistoryByType(type: String, promise: Promise) {
|
|
341
308
|
ensureConnection(
|
|
342
|
-
promise
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return@queryPurchaseHistoryAsync
|
|
352
|
-
}
|
|
353
|
-
Log.d(TAG, purchaseHistoryRecordList.toString())
|
|
354
|
-
val items = Arguments.createArray()
|
|
355
|
-
for (i in purchaseHistoryRecordList!!.indices) {
|
|
356
|
-
val item = Arguments.createMap()
|
|
357
|
-
val purchase = purchaseHistoryRecordList[i]
|
|
358
|
-
item.putString("productId", purchase.skus[0])
|
|
359
|
-
item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
360
|
-
item.putString("transactionReceipt", purchase.originalJson)
|
|
361
|
-
item.putString("purchaseToken", purchase.purchaseToken)
|
|
362
|
-
item.putString("dataAndroid", purchase.originalJson)
|
|
363
|
-
item.putString("signatureAndroid", purchase.signature)
|
|
364
|
-
item.putString("developerPayload", purchase.developerPayload)
|
|
365
|
-
items.pushMap(item)
|
|
366
|
-
}
|
|
367
|
-
try {
|
|
368
|
-
promise.resolve(items)
|
|
369
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
370
|
-
Log.e(TAG, oce.message!!)
|
|
371
|
-
}
|
|
372
|
-
}
|
|
309
|
+
promise
|
|
310
|
+
) {
|
|
311
|
+
billingClient.queryPurchaseHistoryAsync(
|
|
312
|
+
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
313
|
+
) { billingResult, purchaseHistoryRecordList ->
|
|
314
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
315
|
+
PlayUtils.instance
|
|
316
|
+
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
317
|
+
return@queryPurchaseHistoryAsync
|
|
373
318
|
}
|
|
319
|
+
Log.d(TAG, purchaseHistoryRecordList.toString())
|
|
320
|
+
val items = Arguments.createArray()
|
|
321
|
+
for (i in purchaseHistoryRecordList!!.indices) {
|
|
322
|
+
val item = Arguments.createMap()
|
|
323
|
+
val purchase = purchaseHistoryRecordList[i]
|
|
324
|
+
item.putString("productId", purchase.skus[0])
|
|
325
|
+
item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
326
|
+
item.putString("transactionReceipt", purchase.originalJson)
|
|
327
|
+
item.putString("purchaseToken", purchase.purchaseToken)
|
|
328
|
+
item.putString("dataAndroid", purchase.originalJson)
|
|
329
|
+
item.putString("signatureAndroid", purchase.signature)
|
|
330
|
+
item.putString("developerPayload", purchase.developerPayload)
|
|
331
|
+
items.pushMap(item)
|
|
332
|
+
}
|
|
333
|
+
promise.safeResolve(items)
|
|
374
334
|
}
|
|
375
|
-
|
|
335
|
+
}
|
|
376
336
|
}
|
|
377
337
|
|
|
378
338
|
@ReactMethod
|
|
@@ -387,102 +347,99 @@ class RNIapModule(reactContext: ReactApplicationContext) :
|
|
|
387
347
|
) {
|
|
388
348
|
val activity = currentActivity
|
|
389
349
|
if (activity == null) {
|
|
390
|
-
promise.
|
|
350
|
+
promise.safeReject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
|
|
391
351
|
return
|
|
392
352
|
}
|
|
393
353
|
ensureConnection(
|
|
394
|
-
promise
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
354
|
+
promise
|
|
355
|
+
) {
|
|
356
|
+
DoobooUtils.instance.addPromiseForKey(
|
|
357
|
+
PROMISE_BUY_ITEM, promise
|
|
358
|
+
)
|
|
359
|
+
val builder = BillingFlowParams.newBuilder()
|
|
360
|
+
val selectedSku: SkuDetails? = skus[sku]
|
|
361
|
+
if (selectedSku == null) {
|
|
362
|
+
val debugMessage =
|
|
363
|
+
"The sku was not found. Please fetch products first by calling getItems"
|
|
364
|
+
val error = Arguments.createMap()
|
|
365
|
+
error.putString("debugMessage", debugMessage)
|
|
366
|
+
error.putString("code", PROMISE_BUY_ITEM)
|
|
367
|
+
error.putString("message", debugMessage)
|
|
368
|
+
error.putString("productId", sku)
|
|
369
|
+
sendEvent(reactContext, "purchase-error", error)
|
|
370
|
+
promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
|
|
371
|
+
return@ensureConnection
|
|
372
|
+
}
|
|
373
|
+
builder.setSkuDetails(selectedSku)
|
|
374
|
+
val subscriptionUpdateParamsBuilder = SubscriptionUpdateParams.newBuilder()
|
|
375
|
+
if (purchaseToken != null) {
|
|
376
|
+
subscriptionUpdateParamsBuilder.setOldSkuPurchaseToken(purchaseToken)
|
|
377
|
+
}
|
|
378
|
+
if (obfuscatedAccountId != null) {
|
|
379
|
+
builder.setObfuscatedAccountId(obfuscatedAccountId)
|
|
380
|
+
}
|
|
381
|
+
if (obfuscatedProfileId != null) {
|
|
382
|
+
builder.setObfuscatedProfileId(obfuscatedProfileId)
|
|
383
|
+
}
|
|
384
|
+
if (prorationMode != null && prorationMode != -1) {
|
|
385
|
+
if (prorationMode
|
|
386
|
+
== BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE
|
|
387
|
+
) {
|
|
388
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
389
|
+
BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE
|
|
399
390
|
)
|
|
400
|
-
|
|
401
|
-
var selectedSku: SkuDetails? = skus.get(sku)
|
|
402
|
-
if (selectedSku == null) {
|
|
391
|
+
if (type != BillingClient.SkuType.SUBS) {
|
|
403
392
|
val debugMessage =
|
|
404
|
-
|
|
393
|
+
(
|
|
394
|
+
"IMMEDIATE_AND_CHARGE_PRORATED_PRICE for proration mode only works in" +
|
|
395
|
+
" subscription purchase."
|
|
396
|
+
)
|
|
405
397
|
val error = Arguments.createMap()
|
|
406
398
|
error.putString("debugMessage", debugMessage)
|
|
407
399
|
error.putString("code", PROMISE_BUY_ITEM)
|
|
408
400
|
error.putString("message", debugMessage)
|
|
409
401
|
error.putString("productId", sku)
|
|
410
402
|
sendEvent(reactContext, "purchase-error", error)
|
|
411
|
-
promise.
|
|
412
|
-
return
|
|
413
|
-
}
|
|
414
|
-
builder.setSkuDetails(selectedSku)
|
|
415
|
-
val subscriptionUpdateParamsBuilder = SubscriptionUpdateParams.newBuilder()
|
|
416
|
-
if (purchaseToken != null) {
|
|
417
|
-
subscriptionUpdateParamsBuilder.setOldSkuPurchaseToken(purchaseToken)
|
|
418
|
-
}
|
|
419
|
-
if (obfuscatedAccountId != null) {
|
|
420
|
-
builder.setObfuscatedAccountId(obfuscatedAccountId)
|
|
421
|
-
}
|
|
422
|
-
if (obfuscatedProfileId != null) {
|
|
423
|
-
builder.setObfuscatedProfileId(obfuscatedProfileId)
|
|
424
|
-
}
|
|
425
|
-
if (prorationMode != null && prorationMode != -1) {
|
|
426
|
-
if (prorationMode
|
|
427
|
-
== BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE
|
|
428
|
-
) {
|
|
429
|
-
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
430
|
-
BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE
|
|
431
|
-
)
|
|
432
|
-
if (type != BillingClient.SkuType.SUBS) {
|
|
433
|
-
val debugMessage =
|
|
434
|
-
(
|
|
435
|
-
"IMMEDIATE_AND_CHARGE_PRORATED_PRICE for proration mode only works in" +
|
|
436
|
-
" subscription purchase."
|
|
437
|
-
)
|
|
438
|
-
val error = Arguments.createMap()
|
|
439
|
-
error.putString("debugMessage", debugMessage)
|
|
440
|
-
error.putString("code", PROMISE_BUY_ITEM)
|
|
441
|
-
error.putString("message", debugMessage)
|
|
442
|
-
error.putString("productId", sku)
|
|
443
|
-
sendEvent(reactContext, "purchase-error", error)
|
|
444
|
-
promise.reject(PROMISE_BUY_ITEM, debugMessage)
|
|
445
|
-
return
|
|
446
|
-
}
|
|
447
|
-
} else if (prorationMode
|
|
448
|
-
== BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION
|
|
449
|
-
) {
|
|
450
|
-
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
451
|
-
BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION
|
|
452
|
-
)
|
|
453
|
-
} else if (prorationMode == BillingFlowParams.ProrationMode.DEFERRED) {
|
|
454
|
-
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
455
|
-
BillingFlowParams.ProrationMode.DEFERRED
|
|
456
|
-
)
|
|
457
|
-
} else if (prorationMode
|
|
458
|
-
== BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION
|
|
459
|
-
) {
|
|
460
|
-
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
461
|
-
BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION
|
|
462
|
-
)
|
|
463
|
-
} else if (prorationMode
|
|
464
|
-
== BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
|
|
465
|
-
) {
|
|
466
|
-
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
467
|
-
BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
|
|
468
|
-
)
|
|
469
|
-
} else {
|
|
470
|
-
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
471
|
-
BillingFlowParams.ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY
|
|
472
|
-
)
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
if (purchaseToken != null) {
|
|
476
|
-
val subscriptionUpdateParams = subscriptionUpdateParamsBuilder.build()
|
|
477
|
-
builder.setSubscriptionUpdateParams(subscriptionUpdateParams)
|
|
403
|
+
promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
|
|
404
|
+
return@ensureConnection
|
|
478
405
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
406
|
+
} else if (prorationMode
|
|
407
|
+
== BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION
|
|
408
|
+
) {
|
|
409
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
410
|
+
BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION
|
|
411
|
+
)
|
|
412
|
+
} else if (prorationMode == BillingFlowParams.ProrationMode.DEFERRED) {
|
|
413
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
414
|
+
BillingFlowParams.ProrationMode.DEFERRED
|
|
415
|
+
)
|
|
416
|
+
} else if (prorationMode
|
|
417
|
+
== BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION
|
|
418
|
+
) {
|
|
419
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
420
|
+
BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION
|
|
421
|
+
)
|
|
422
|
+
} else if (prorationMode
|
|
423
|
+
== BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
|
|
424
|
+
) {
|
|
425
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
426
|
+
BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
|
|
427
|
+
)
|
|
428
|
+
} else {
|
|
429
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
430
|
+
BillingFlowParams.ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY
|
|
431
|
+
)
|
|
483
432
|
}
|
|
484
433
|
}
|
|
485
|
-
|
|
434
|
+
if (purchaseToken != null) {
|
|
435
|
+
val subscriptionUpdateParams = subscriptionUpdateParamsBuilder.build()
|
|
436
|
+
builder.setSubscriptionUpdateParams(subscriptionUpdateParams)
|
|
437
|
+
}
|
|
438
|
+
val flowParams = builder.build()
|
|
439
|
+
val billingResult = billingClient.launchBillingFlow(activity, flowParams)
|
|
440
|
+
val errorData: Array<String?> =
|
|
441
|
+
PlayUtils.instance.getBillingResponseData(billingResult.responseCode)
|
|
442
|
+
}
|
|
486
443
|
}
|
|
487
444
|
|
|
488
445
|
@ReactMethod
|
|
@@ -492,36 +449,29 @@ class RNIapModule(reactContext: ReactApplicationContext) :
|
|
|
492
449
|
promise: Promise
|
|
493
450
|
) {
|
|
494
451
|
ensureConnection(
|
|
495
|
-
promise
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
508
|
-
}
|
|
509
|
-
try {
|
|
510
|
-
val map = Arguments.createMap()
|
|
511
|
-
map.putInt("responseCode", billingResult.responseCode)
|
|
512
|
-
map.putString("debugMessage", billingResult.debugMessage)
|
|
513
|
-
val errorData: Array<String?> = PlayUtils.instance
|
|
514
|
-
.getBillingResponseData(billingResult.responseCode)
|
|
515
|
-
map.putString("code", errorData[0])
|
|
516
|
-
map.putString("message", errorData[1])
|
|
517
|
-
promise.resolve(map)
|
|
518
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
519
|
-
Log.e(TAG, oce.message!!)
|
|
520
|
-
}
|
|
521
|
-
}
|
|
452
|
+
promise
|
|
453
|
+
) {
|
|
454
|
+
val acknowledgePurchaseParams =
|
|
455
|
+
AcknowledgePurchaseParams.newBuilder().setPurchaseToken(
|
|
456
|
+
token!!
|
|
457
|
+
).build()
|
|
458
|
+
billingClient.acknowledgePurchase(
|
|
459
|
+
acknowledgePurchaseParams
|
|
460
|
+
) { billingResult: BillingResult ->
|
|
461
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
462
|
+
PlayUtils.instance
|
|
463
|
+
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
522
464
|
}
|
|
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)
|
|
523
473
|
}
|
|
524
|
-
|
|
474
|
+
}
|
|
525
475
|
}
|
|
526
476
|
|
|
527
477
|
@ReactMethod
|
|
@@ -532,32 +482,26 @@ class RNIapModule(reactContext: ReactApplicationContext) :
|
|
|
532
482
|
) {
|
|
533
483
|
val params = ConsumeParams.newBuilder().setPurchaseToken(token!!).build()
|
|
534
484
|
ensureConnection(
|
|
535
|
-
promise
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
544
|
-
}
|
|
545
|
-
try {
|
|
546
|
-
val map = Arguments.createMap()
|
|
547
|
-
map.putInt("responseCode", billingResult.responseCode)
|
|
548
|
-
map.putString("debugMessage", billingResult.debugMessage)
|
|
549
|
-
val errorData: Array<String?> = PlayUtils.instance
|
|
550
|
-
.getBillingResponseData(billingResult.responseCode)
|
|
551
|
-
map.putString("code", errorData[0])
|
|
552
|
-
map.putString("message", errorData[1])
|
|
553
|
-
promise.resolve(map)
|
|
554
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
555
|
-
promise.reject(oce.message)
|
|
556
|
-
}
|
|
557
|
-
}
|
|
485
|
+
promise
|
|
486
|
+
) {
|
|
487
|
+
billingClient.consumeAsync(
|
|
488
|
+
params
|
|
489
|
+
) { billingResult: BillingResult, purchaseToken: String? ->
|
|
490
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
491
|
+
PlayUtils.instance
|
|
492
|
+
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
558
493
|
}
|
|
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)
|
|
559
503
|
}
|
|
560
|
-
|
|
504
|
+
}
|
|
561
505
|
}
|
|
562
506
|
|
|
563
507
|
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
|
|
@@ -625,30 +569,25 @@ class RNIapModule(reactContext: ReactApplicationContext) :
|
|
|
625
569
|
|
|
626
570
|
private fun sendUnconsumedPurchases(promise: Promise) {
|
|
627
571
|
ensureConnection(
|
|
628
|
-
promise
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
for (purchase in list!!) {
|
|
641
|
-
if (!purchase.isAcknowledged) {
|
|
642
|
-
unacknowledgedPurchases.add(purchase)
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
onPurchasesUpdated(billingResult, unacknowledgedPurchases)
|
|
572
|
+
promise
|
|
573
|
+
) {
|
|
574
|
+
val types = arrayOf(BillingClient.SkuType.INAPP, BillingClient.SkuType.SUBS)
|
|
575
|
+
for (type in types) {
|
|
576
|
+
billingClient.queryPurchasesAsync(
|
|
577
|
+
type
|
|
578
|
+
) { billingResult: BillingResult, list: List<Purchase>? ->
|
|
579
|
+
val unacknowledgedPurchases = ArrayList<Purchase>()
|
|
580
|
+
|
|
581
|
+
for (purchase in list!!) {
|
|
582
|
+
if (!purchase.isAcknowledged) {
|
|
583
|
+
unacknowledgedPurchases.add(purchase)
|
|
646
584
|
}
|
|
647
585
|
}
|
|
648
|
-
|
|
586
|
+
onPurchasesUpdated(billingResult, unacknowledgedPurchases)
|
|
649
587
|
}
|
|
650
588
|
}
|
|
651
|
-
|
|
589
|
+
promise.safeResolve(true)
|
|
590
|
+
}
|
|
652
591
|
}
|
|
653
592
|
|
|
654
593
|
@ReactMethod
|
|
@@ -682,19 +621,15 @@ class RNIapModule(reactContext: ReactApplicationContext) :
|
|
|
682
621
|
|
|
683
622
|
companion object {
|
|
684
623
|
private const val PROMISE_BUY_ITEM = "PROMISE_BUY_ITEM"
|
|
624
|
+
const val TAG = "RNIapModule"
|
|
685
625
|
}
|
|
686
626
|
|
|
687
627
|
init {
|
|
688
|
-
this.reactContext = reactContext
|
|
689
|
-
skus = mutableMapOf<String, SkuDetails>()
|
|
690
628
|
val lifecycleEventListener: LifecycleEventListener = object : LifecycleEventListener {
|
|
691
629
|
override fun onHostResume() {}
|
|
692
630
|
override fun onHostPause() {}
|
|
693
631
|
override fun onHostDestroy() {
|
|
694
|
-
|
|
695
|
-
billingClientCache!!.endConnection()
|
|
696
|
-
billingClientCache = null
|
|
697
|
-
}
|
|
632
|
+
billingClient.endConnection()
|
|
698
633
|
}
|
|
699
634
|
}
|
|
700
635
|
reactContext.addLifecycleEventListener(lifecycleEventListener)
|
|
@@ -0,0 +1,192 @@
|
|
|
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.facebook.react.bridge.Promise
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.google.android.gms.common.ConnectionResult
|
|
12
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
13
|
+
import io.mockk.MockKAnnotations
|
|
14
|
+
import io.mockk.every
|
|
15
|
+
import io.mockk.impl.annotations.MockK
|
|
16
|
+
import io.mockk.mockk
|
|
17
|
+
import io.mockk.slot
|
|
18
|
+
import io.mockk.verify
|
|
19
|
+
import org.junit.Assert.assertTrue
|
|
20
|
+
import org.junit.Before
|
|
21
|
+
import org.junit.Test
|
|
22
|
+
|
|
23
|
+
class RNIapModuleTest {
|
|
24
|
+
|
|
25
|
+
@MockK
|
|
26
|
+
lateinit var context: ReactApplicationContext
|
|
27
|
+
@MockK
|
|
28
|
+
lateinit var builder: BillingClient.Builder
|
|
29
|
+
@MockK
|
|
30
|
+
lateinit var billingClient: BillingClient
|
|
31
|
+
@MockK
|
|
32
|
+
lateinit var availability: GoogleApiAvailability
|
|
33
|
+
|
|
34
|
+
private lateinit var module: RNIapModule
|
|
35
|
+
|
|
36
|
+
@Before
|
|
37
|
+
fun setUp() {
|
|
38
|
+
MockKAnnotations.init(this, relaxUnitFun = true)
|
|
39
|
+
every { builder.setListener(any()) } returns builder
|
|
40
|
+
every { builder.build() } returns billingClient
|
|
41
|
+
module = RNIapModule(context, builder, availability)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun `initConnection Already connected should resolve to true`() {
|
|
46
|
+
every { billingClient.isReady } returns true
|
|
47
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
48
|
+
|
|
49
|
+
module.initConnection(promise)
|
|
50
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
51
|
+
verify { promise.resolve(true) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
fun `initConnection Play Services not available on device should reject`() {
|
|
56
|
+
every { billingClient.isReady } returns false
|
|
57
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.DEVELOPER_ERROR
|
|
58
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
59
|
+
|
|
60
|
+
module.initConnection(promise)
|
|
61
|
+
verify { promise.safeReject(any(), any<String>()) }
|
|
62
|
+
verify(exactly = 0) { promise.resolve(any()) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Test
|
|
66
|
+
fun `initConnection start new connection succeeds`() {
|
|
67
|
+
every { billingClient.isReady } returns false
|
|
68
|
+
val listener = slot<BillingClientStateListener>()
|
|
69
|
+
every { billingClient.startConnection(capture(listener)) } answers {
|
|
70
|
+
listener.captured.onBillingSetupFinished(BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK).build())
|
|
71
|
+
}
|
|
72
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
73
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
74
|
+
|
|
75
|
+
module.initConnection(promise)
|
|
76
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
77
|
+
verify { promise.resolve(any()) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Test
|
|
81
|
+
fun `initConnection start new connection fails`() {
|
|
82
|
+
every { billingClient.isReady } returns false
|
|
83
|
+
val listener = slot<BillingClientStateListener>()
|
|
84
|
+
every { billingClient.startConnection(capture(listener)) } answers {
|
|
85
|
+
listener.captured.onBillingSetupFinished(BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR).build())
|
|
86
|
+
}
|
|
87
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
88
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
89
|
+
|
|
90
|
+
module.initConnection(promise)
|
|
91
|
+
verify { promise.safeReject(any(), any<String>()) }
|
|
92
|
+
verify(exactly = 0) { promise.resolve(any()) }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
fun `endConnection resolves`() {
|
|
97
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
98
|
+
|
|
99
|
+
module.endConnection(promise)
|
|
100
|
+
|
|
101
|
+
verify { billingClient.endConnection() }
|
|
102
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
103
|
+
verify { promise.resolve(true) }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test
|
|
107
|
+
fun `flushFailedPurchasesCachedAsPending resolves to false if no pending purchases`() {
|
|
108
|
+
every { billingClient.isReady } returns true
|
|
109
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
110
|
+
val listener = slot<PurchasesResponseListener>()
|
|
111
|
+
every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
|
|
112
|
+
listener.captured.onQueryPurchasesResponse(BillingResult.newBuilder().build(), listOf())
|
|
113
|
+
}
|
|
114
|
+
module.flushFailedPurchasesCachedAsPending(promise)
|
|
115
|
+
|
|
116
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
117
|
+
verify { promise.resolve(false) } // empty list
|
|
118
|
+
}
|
|
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
|
+
|
|
165
|
+
@Test
|
|
166
|
+
fun getItemsByType() {
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Test
|
|
170
|
+
fun getAvailableItemsByType() {
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@Test
|
|
174
|
+
fun getPurchaseHistoryByType() {
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@Test
|
|
178
|
+
fun buyItemByType() {
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Test
|
|
182
|
+
fun acknowledgePurchase() {
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@Test
|
|
186
|
+
fun consumeProduct() {
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@Test
|
|
190
|
+
fun onPurchasesUpdated() {
|
|
191
|
+
}
|
|
192
|
+
}
|