expo-iap 1.0.3 → 2.0.0-rc.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.
Files changed (103) hide show
  1. package/.eslintrc.js +9 -0
  2. package/.prettierrc.js +9 -0
  3. package/.swiftlint.yml +10 -0
  4. package/README.md +28 -21
  5. package/android/build.gradle +34 -72
  6. package/android/src/main/AndroidManifest.xml +1 -4
  7. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +535 -0
  8. package/android/src/main/java/expo/modules/iap/MissingCurrentActivityException.kt +6 -0
  9. package/android/src/main/java/expo/modules/iap/PlayUtils.kt +124 -0
  10. package/build/ExpoIap.types.d.ts +89 -0
  11. package/build/ExpoIap.types.d.ts.map +1 -0
  12. package/build/ExpoIap.types.js +59 -0
  13. package/build/ExpoIap.types.js.map +1 -0
  14. package/build/ExpoIapModule.d.ts +3 -0
  15. package/build/ExpoIapModule.d.ts.map +1 -0
  16. package/build/ExpoIapModule.js +5 -0
  17. package/build/ExpoIapModule.js.map +1 -0
  18. package/build/index.d.ts +38 -0
  19. package/build/index.d.ts.map +1 -0
  20. package/build/index.js +202 -0
  21. package/build/index.js.map +1 -0
  22. package/build/modules/android.d.ts +40 -0
  23. package/build/modules/android.d.ts.map +1 -0
  24. package/build/modules/android.js +54 -0
  25. package/build/modules/android.js.map +1 -0
  26. package/build/modules/ios.d.ts +41 -0
  27. package/build/modules/ios.d.ts.map +1 -0
  28. package/build/modules/ios.js +44 -0
  29. package/build/modules/ios.js.map +1 -0
  30. package/build/types/ExpoIapAndroid.types.d.ts +113 -0
  31. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -0
  32. package/build/types/ExpoIapAndroid.types.js +23 -0
  33. package/build/types/ExpoIapAndroid.types.js.map +1 -0
  34. package/build/types/ExpoIapIos.types.d.ts +122 -0
  35. package/build/types/ExpoIapIos.types.d.ts.map +1 -0
  36. package/build/types/ExpoIapIos.types.js +2 -0
  37. package/build/types/ExpoIapIos.types.js.map +1 -0
  38. package/bun.lockb +0 -0
  39. package/expo-module.config.json +4 -8
  40. package/ios/ExpoIap.podspec +27 -0
  41. package/ios/ExpoIapModule.swift +498 -0
  42. package/ios/ProductStore.swift +27 -0
  43. package/ios/Types.swift +54 -0
  44. package/package.json +33 -62
  45. package/src/ExpoIap.types.ts +125 -0
  46. package/src/ExpoIapModule.ts +5 -0
  47. package/src/index.ts +397 -0
  48. package/src/modules/android.ts +89 -0
  49. package/src/modules/ios.ts +81 -0
  50. package/src/types/ExpoIapAndroid.types.ts +123 -0
  51. package/src/types/ExpoIapIos.types.ts +141 -0
  52. package/tsconfig.json +9 -0
  53. package/.editorconfig +0 -10
  54. package/.flowconfig +0 -11
  55. package/.monolinterrc +0 -3
  56. package/.yarn/install-state.gz +0 -0
  57. package/.yarn/releases/yarn-3.1.1.cjs +0 -768
  58. package/.yarnrc.yml +0 -3
  59. package/LICENSE +0 -21
  60. package/RNIap.podspec +0 -18
  61. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  62. package/android/gradle/wrapper/gradle-wrapper.properties +0 -6
  63. package/android/gradle.properties +0 -2
  64. package/android/gradlew +0 -160
  65. package/android/gradlew.bat +0 -90
  66. package/android/libs/in-app-purchasing-2.0.76.jar +0 -0
  67. package/android/src/amazon/AndroidManifest.xml +0 -12
  68. package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonListener.kt +0 -356
  69. package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt +0 -128
  70. package/android/src/amazon/java/com/dooboolab/RNIap/RNIapPackage.kt +0 -20
  71. package/android/src/main/java/com/dooboolab/RNIap/DoobooUtils.kt +0 -180
  72. package/android/src/play/java/com/dooboolab/RNIap/PlayUtils.kt +0 -77
  73. package/android/src/play/java/com/dooboolab/RNIap/RNIapModule.kt +0 -698
  74. package/android/src/play/java/com/dooboolab/RNIap/RNIapPackage.kt +0 -20
  75. package/babel.config.js +0 -10
  76. package/index.d.ts +0 -3
  77. package/index.js +0 -3
  78. package/index.js.flow +0 -9
  79. package/ios/RNIap.xcodeproj/project.pbxproj +0 -370
  80. package/ios/RNIap.xcodeproj/xcshareddata/xcschemes/RNIap.xcscheme +0 -80
  81. package/ios/RNIapIos.m +0 -60
  82. package/ios/RNIapIos.swift +0 -932
  83. package/ios/RNIapQueue.swift +0 -35
  84. package/jest.config.js +0 -194
  85. package/src/__test__/iap.test.d.ts +0 -1
  86. package/src/__test__/iap.test.js +0 -59
  87. package/src/hooks/useIAP.d.ts +0 -21
  88. package/src/hooks/useIAP.js +0 -140
  89. package/src/hooks/withIAPContext.d.ts +0 -21
  90. package/src/hooks/withIAPContext.js +0 -142
  91. package/src/iap.d.ts +0 -197
  92. package/src/iap.js +0 -625
  93. package/src/index.d.ts +0 -4
  94. package/src/index.js +0 -4
  95. package/src/types/amazon.d.ts +0 -23
  96. package/src/types/amazon.js +0 -1
  97. package/src/types/android.d.ts +0 -47
  98. package/src/types/android.js +0 -22
  99. package/src/types/apple.d.ts +0 -424
  100. package/src/types/apple.js +0 -165
  101. package/src/types/index.d.ts +0 -117
  102. package/src/types/index.js +0 -40
  103. package/test/mocks/react-native-modules.js +0 -14
@@ -0,0 +1,535 @@
1
+ package expo.modules.iap
2
+
3
+ import android.content.Context
4
+ import android.util.Log
5
+ import com.android.billingclient.api.AcknowledgePurchaseParams
6
+ import com.android.billingclient.api.BillingClient
7
+ import com.android.billingclient.api.BillingClientStateListener
8
+ import com.android.billingclient.api.BillingFlowParams
9
+ import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams
10
+ import com.android.billingclient.api.BillingResult
11
+ import com.android.billingclient.api.ConsumeParams
12
+ import com.android.billingclient.api.ProductDetails
13
+ import com.android.billingclient.api.Purchase
14
+ import com.android.billingclient.api.PurchaseHistoryRecord
15
+ import com.android.billingclient.api.PurchasesUpdatedListener
16
+ import com.android.billingclient.api.QueryProductDetailsParams
17
+ import com.android.billingclient.api.QueryPurchaseHistoryParams
18
+ import com.android.billingclient.api.QueryPurchasesParams
19
+ import com.google.android.gms.common.ConnectionResult
20
+ import com.google.android.gms.common.GoogleApiAvailability
21
+ import expo.modules.kotlin.Promise
22
+ import expo.modules.kotlin.exception.Exceptions
23
+ import expo.modules.kotlin.modules.Module
24
+ import expo.modules.kotlin.modules.ModuleDefinition
25
+
26
+ class ExpoIapModule :
27
+ Module(),
28
+ PurchasesUpdatedListener {
29
+ companion object {
30
+ const val TAG = "ExpoIapModule"
31
+ const val E_NOT_PREPARED = "E_NOT_PREPARED"
32
+ const val E_INIT_CONNECTION = "E_INIT_CONNECTION"
33
+ const val E_QUERY_PRODUCT = "E_QUERY_PRODUCT"
34
+ const val EMPTY_SKU_LIST = "EMPTY_SKU_LIST"
35
+ }
36
+
37
+ object IapEvent {
38
+ const val PURCHASE_UPDATED = "purchase-updated"
39
+ const val PURCHASE_ERROR = "purchase-error"
40
+ }
41
+
42
+ private var billingClientCache: BillingClient? = null
43
+ private val skus: MutableMap<String, ProductDetails> = mutableMapOf()
44
+ private val context: Context
45
+ get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
46
+ private val currentActivity
47
+ get() =
48
+ appContext.activityProvider?.currentActivity
49
+ ?: throw MissingCurrentActivityException()
50
+
51
+ override fun onPurchasesUpdated(
52
+ billingResult: BillingResult,
53
+ purchases: List<Purchase>?,
54
+ ) {
55
+ val responseCode = billingResult.responseCode
56
+ if (responseCode != BillingClient.BillingResponseCode.OK) {
57
+ val error =
58
+ mutableMapOf<String, Any?>(
59
+ "responseCode" to responseCode,
60
+ "debugMessage" to billingResult.debugMessage,
61
+ )
62
+ val errorData = PlayUtils.getBillingResponseData(responseCode)
63
+ error["code"] = errorData.code
64
+ error["message"] = errorData.message
65
+ sendEvent(IapEvent.PURCHASE_ERROR, error.toMap())
66
+ return
67
+ }
68
+
69
+ if (purchases != null) {
70
+ val promiseItems = mutableListOf<Map<String, Any?>>()
71
+ purchases.forEach { purchase ->
72
+ val item =
73
+ mutableMapOf<String, Any?>(
74
+ "productId" to purchase.products[0],
75
+ "productIds" to purchase.products,
76
+ "transactionId" to purchase.orderId,
77
+ "transactionDate" to purchase.purchaseTime.toDouble(),
78
+ "transactionReceipt" to purchase.originalJson,
79
+ "purchaseToken" to purchase.purchaseToken,
80
+ "dataAndroid" to purchase.originalJson,
81
+ "signatureAndroid" to purchase.signature,
82
+ "autoRenewingAndroid" to purchase.isAutoRenewing,
83
+ "isAcknowledgedAndroid" to purchase.isAcknowledged,
84
+ "purchaseStateAndroid" to purchase.purchaseState,
85
+ "packageNameAndroid" to purchase.packageName,
86
+ "developerPayloadAndroid" to purchase.developerPayload,
87
+ )
88
+ purchase.accountIdentifiers?.let { accountIdentifiers ->
89
+ item["obfuscatedAccountIdAndroid"] = accountIdentifiers.obfuscatedAccountId
90
+ item["obfuscatedProfileIdAndroid"] = accountIdentifiers.obfuscatedProfileId
91
+ }
92
+ promiseItems.add(item.toMap())
93
+ sendEvent(IapEvent.PURCHASE_UPDATED, item.toMap())
94
+ }
95
+ } else {
96
+ val result =
97
+ mutableMapOf<String, Any?>(
98
+ "responseCode" to billingResult.responseCode,
99
+ "debugMessage" to billingResult.debugMessage,
100
+ "extraMessage" to
101
+ "The purchases are null. This is a normal behavior if you have requested DEFERRED proration. If not please report an issue.",
102
+ )
103
+ sendEvent(IapEvent.PURCHASE_UPDATED, result.toMap())
104
+ }
105
+ }
106
+
107
+ override fun definition() =
108
+ ModuleDefinition {
109
+ Name("ExpoIap")
110
+
111
+ Constants("PI" to Math.PI)
112
+
113
+ Events(IapEvent.PURCHASE_UPDATED, IapEvent.PURCHASE_ERROR)
114
+
115
+ AsyncFunction("initConnection") { promise: Promise ->
116
+ initBillingClient(promise) { promise.resolve(true) }
117
+ }
118
+
119
+ AsyncFunction("endConnection") { promise: Promise ->
120
+ billingClientCache?.endConnection()
121
+ billingClientCache = null
122
+ skus.clear()
123
+ promise.resolve(true)
124
+ }
125
+
126
+ AsyncFunction("getItemsByType") { type: String, skuArr: Array<String>, promise: Promise ->
127
+ ensureConnection(promise) { billingClient ->
128
+ val skuList =
129
+ skuArr.map { sku ->
130
+ QueryProductDetailsParams.Product
131
+ .newBuilder()
132
+ .setProductId(sku)
133
+ .setProductType(type)
134
+ .build()
135
+ }
136
+
137
+ if (skuList.isEmpty()) {
138
+ promise.reject(EMPTY_SKU_LIST, "The SKU list is empty.", null)
139
+ return@ensureConnection
140
+ }
141
+
142
+ val params =
143
+ QueryProductDetailsParams
144
+ .newBuilder()
145
+ .setProductList(skuList)
146
+ .build()
147
+
148
+ billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
149
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
150
+ promise.reject(
151
+ E_QUERY_PRODUCT,
152
+ "Error querying product details: ${billingResult.debugMessage}",
153
+ null,
154
+ )
155
+ return@queryProductDetailsAsync
156
+ }
157
+
158
+ val items =
159
+ productDetailsList.map { productDetails ->
160
+ skus[productDetails.productId] = productDetails
161
+
162
+ mapOf(
163
+ "productId" to productDetails.productId,
164
+ "title" to productDetails.title,
165
+ "description" to productDetails.description,
166
+ "productType" to productDetails.productType,
167
+ "name" to productDetails.name,
168
+ "oneTimePurchaseOfferDetails" to
169
+ productDetails.oneTimePurchaseOfferDetails?.let {
170
+ mapOf(
171
+ "priceCurrencyCode" to it.priceCurrencyCode,
172
+ "formattedPrice" to it.formattedPrice,
173
+ "priceAmountMicros" to it.priceAmountMicros.toString(),
174
+ )
175
+ },
176
+ "subscriptionOfferDetails" to
177
+ productDetails.subscriptionOfferDetails?.map { subscriptionOfferDetailsItem ->
178
+ mapOf(
179
+ "basePlanId" to subscriptionOfferDetailsItem.basePlanId,
180
+ "offerId" to subscriptionOfferDetailsItem.offerId,
181
+ "offerToken" to subscriptionOfferDetailsItem.offerToken,
182
+ "offerTags" to subscriptionOfferDetailsItem.offerTags,
183
+ "pricingPhases" to
184
+ mapOf(
185
+ "pricingPhaseList" to
186
+ subscriptionOfferDetailsItem.pricingPhases.pricingPhaseList.map
187
+ { pricingPhaseItem ->
188
+ mapOf(
189
+ "formattedPrice" to pricingPhaseItem.formattedPrice,
190
+ "priceCurrencyCode" to pricingPhaseItem.priceCurrencyCode,
191
+ "billingPeriod" to pricingPhaseItem.billingPeriod,
192
+ "billingCycleCount" to pricingPhaseItem.billingCycleCount,
193
+ "priceAmountMicros" to
194
+ pricingPhaseItem.priceAmountMicros.toString(),
195
+ "recurrenceMode" to pricingPhaseItem.recurrenceMode,
196
+ )
197
+ },
198
+ ),
199
+ )
200
+ },
201
+ )
202
+ }
203
+ promise.resolve(items)
204
+ }
205
+ }
206
+ }
207
+
208
+ AsyncFunction("getAvailableItemsByType") { type: String, promise: Promise ->
209
+ ensureConnection(promise) { billingClient ->
210
+ val items = mutableListOf<Map<String, Any?>>()
211
+ billingClient.queryPurchasesAsync(
212
+ QueryPurchasesParams
213
+ .newBuilder()
214
+ .setProductType(
215
+ if (type == "subs") BillingClient.ProductType.SUBS else BillingClient.ProductType.INAPP,
216
+ ).build(),
217
+ ) { billingResult: BillingResult, purchases: List<Purchase>? ->
218
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
219
+ purchases?.forEach { purchase ->
220
+ val item =
221
+ mutableMapOf<String, Any?>(
222
+ // kept for convenience/backward-compatibility. productIds has the complete list
223
+ "productId" to purchase.products[0],
224
+ "productIds" to purchase.products,
225
+ "transactionId" to purchase.orderId,
226
+ "transactionDate" to purchase.purchaseTime.toDouble(),
227
+ "transactionReceipt" to purchase.originalJson,
228
+ "orderId" to purchase.orderId,
229
+ "purchaseToken" to purchase.purchaseToken,
230
+ "developerPayloadAndroid" to purchase.developerPayload,
231
+ "signatureAndroid" to purchase.signature,
232
+ "purchaseStateAndroid" to purchase.purchaseState,
233
+ "isAcknowledgedAndroid" to purchase.isAcknowledged,
234
+ "packageNameAndroid" to purchase.packageName,
235
+ "obfuscatedAccountIdAndroid" to purchase.accountIdentifiers?.obfuscatedAccountId,
236
+ "obfuscatedProfileIdAndroid" to purchase.accountIdentifiers?.obfuscatedProfileId,
237
+ )
238
+ if (type == BillingClient.ProductType.SUBS) {
239
+ item["autoRenewingAndroid"] = purchase.isAutoRenewing
240
+ }
241
+ items.add(item)
242
+ }
243
+ promise.resolve(items)
244
+ }
245
+ }
246
+ }
247
+
248
+ AsyncFunction("getPurchaseHistoryByType") { type: String, promise: Promise ->
249
+ ensureConnection(promise) { billingClient ->
250
+ billingClient.queryPurchaseHistoryAsync(
251
+ QueryPurchaseHistoryParams
252
+ .newBuilder()
253
+ .setProductType(
254
+ if (type == "subs") BillingClient.ProductType.SUBS else BillingClient.ProductType.INAPP,
255
+ ).build(),
256
+ ) { billingResult: BillingResult, purchaseHistoryRecordList: List<PurchaseHistoryRecord>? ->
257
+
258
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
259
+ PlayUtils.rejectPromiseWithBillingError(
260
+ promise,
261
+ billingResult.responseCode,
262
+ )
263
+ return@queryPurchaseHistoryAsync
264
+ }
265
+
266
+ Log.d(TAG, purchaseHistoryRecordList.toString())
267
+ val items = mutableListOf<Map<String, Any?>>()
268
+ purchaseHistoryRecordList?.forEach { purchase ->
269
+ val item =
270
+ mutableMapOf<String, Any?>(
271
+ "productId" to purchase.products[0],
272
+ "productIds" to purchase.products,
273
+ "transactionDate" to purchase.purchaseTime.toDouble(),
274
+ "transactionReceipt" to purchase.originalJson,
275
+ "purchaseToken" to purchase.purchaseToken,
276
+ "dataAndroid" to purchase.originalJson,
277
+ "signatureAndroid" to purchase.signature,
278
+ "developerPayload" to purchase.developerPayload,
279
+ )
280
+ items.add(item)
281
+ }
282
+ promise.resolve(items)
283
+ }
284
+ }
285
+ }
286
+
287
+ AsyncFunction("buyItemByType") { params: Map<String, Any?>, promise: Promise ->
288
+ val type = params["type"] as String
289
+ val skuArr =
290
+ (params["skuArr"] as? List<*>)?.filterIsInstance<String>()?.toTypedArray()
291
+ ?: emptyArray()
292
+ val purchaseToken = params["purchaseToken"] as? String
293
+ val replacementMode = (params["replacementMode"] as? Double)?.toInt() ?: -1
294
+ val obfuscatedAccountId = params["obfuscatedAccountId"] as? String
295
+ val obfuscatedProfileId = params["obfuscatedProfileId"] as? String
296
+ val offerTokenArr =
297
+ (params["offerTokenArr"] as? List<*>)
298
+ ?.filterIsInstance<String>()
299
+ ?.toTypedArray() ?: emptyArray()
300
+ val isOfferPersonalized = params["isOfferPersonalized"] as? Boolean ?: false
301
+
302
+ if (currentActivity == null) {
303
+ throw Exception("getCurrentActivity returned null")
304
+ }
305
+
306
+ ensureConnection(promise) { billingClient ->
307
+ if (type == BillingClient.ProductType.SUBS && skuArr.size != offerTokenArr.size) {
308
+ val debugMessage =
309
+ "The number of skus (${skuArr.size}) must match: the number of offerTokens (${offerTokenArr.size}) for Subscriptions"
310
+ sendEvent(
311
+ IapEvent.PURCHASE_ERROR,
312
+ mapOf(
313
+ "debugMessage" to debugMessage,
314
+ "code" to "E_SKU_OFFER_MISMATCH",
315
+ "message" to debugMessage,
316
+ ),
317
+ )
318
+ throw Exception(debugMessage)
319
+ }
320
+
321
+ val productParamsList =
322
+ skuArr.mapIndexed { index, sku ->
323
+ val selectedSku = skus[sku]
324
+ if (selectedSku == null) {
325
+ val debugMessage =
326
+ "The sku was not found. Please fetch products first by calling getItems"
327
+ sendEvent(
328
+ IapEvent.PURCHASE_ERROR,
329
+ mapOf(
330
+ "debugMessage" to debugMessage,
331
+ "code" to "E_SKU_NOT_FOUND",
332
+ "message" to debugMessage,
333
+ "productId" to sku,
334
+ ),
335
+ )
336
+ throw Exception(debugMessage)
337
+ }
338
+
339
+ val productDetailParams =
340
+ BillingFlowParams.ProductDetailsParams
341
+ .newBuilder()
342
+ .setProductDetails(selectedSku)
343
+
344
+ if (type == BillingClient.ProductType.SUBS) {
345
+ productDetailParams.setOfferToken(offerTokenArr[index])
346
+ }
347
+
348
+ productDetailParams.build()
349
+ }
350
+
351
+ val builder =
352
+ BillingFlowParams
353
+ .newBuilder()
354
+ .setProductDetailsParamsList(productParamsList)
355
+ .setIsOfferPersonalized(isOfferPersonalized)
356
+
357
+ if (purchaseToken != null) {
358
+ val subscriptionUpdateParams =
359
+ SubscriptionUpdateParams
360
+ .newBuilder()
361
+ .setOldPurchaseToken(purchaseToken)
362
+
363
+ if (type == BillingClient.ProductType.SUBS && replacementMode != -1) {
364
+ val mode =
365
+ when (replacementMode) {
366
+ SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE ->
367
+ SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE
368
+ SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION ->
369
+ SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION
370
+ SubscriptionUpdateParams.ReplacementMode.DEFERRED ->
371
+ SubscriptionUpdateParams.ReplacementMode.DEFERRED
372
+ SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION ->
373
+ SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION
374
+ SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE ->
375
+ SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE
376
+ else -> SubscriptionUpdateParams.ReplacementMode.UNKNOWN_REPLACEMENT_MODE
377
+ }
378
+ subscriptionUpdateParams.setSubscriptionReplacementMode(mode)
379
+ }
380
+
381
+ builder.setSubscriptionUpdateParams(subscriptionUpdateParams.build())
382
+ }
383
+
384
+ obfuscatedAccountId?.let { builder.setObfuscatedAccountId(it) }
385
+ obfuscatedProfileId?.let { builder.setObfuscatedProfileId(it) }
386
+
387
+ val flowParams = builder.build()
388
+ val billingResult = billingClient.launchBillingFlow(currentActivity, flowParams)
389
+
390
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
391
+ promise.reject(
392
+ "Billing Error",
393
+ billingResult.debugMessage,
394
+ null,
395
+ )
396
+ }
397
+
398
+ promise.resolve(true)
399
+ }
400
+ }
401
+
402
+ AsyncFunction("acknowledgePurchase") {
403
+ token: String,
404
+ promise: Promise,
405
+ ->
406
+
407
+ ensureConnection(promise) { billingClient ->
408
+ val acknowledgePurchaseParams =
409
+ AcknowledgePurchaseParams
410
+ .newBuilder()
411
+ .setPurchaseToken(token)
412
+ .build()
413
+
414
+ billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult: BillingResult ->
415
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
416
+ PlayUtils.rejectPromiseWithBillingError(
417
+ promise,
418
+ billingResult.responseCode,
419
+ )
420
+ return@acknowledgePurchase
421
+ }
422
+
423
+ val map = mutableMapOf<String, Any?>()
424
+ map["responseCode"] = billingResult.responseCode
425
+ map["debugMessage"] = billingResult.debugMessage
426
+ val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
427
+ map["code"] = errorData.code
428
+ map["message"] = errorData.message
429
+ promise.resolve(map)
430
+ }
431
+ }
432
+ }
433
+
434
+ AsyncFunction("consumeProduct") {
435
+ token: String,
436
+ promise: Promise,
437
+ ->
438
+
439
+ val params = ConsumeParams.newBuilder().setPurchaseToken(token).build()
440
+
441
+ ensureConnection(promise) { billingClient ->
442
+ billingClient.consumeAsync(params) { billingResult: BillingResult, purchaseToken: String? ->
443
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
444
+ PlayUtils.rejectPromiseWithBillingError(
445
+ promise,
446
+ billingResult.responseCode,
447
+ )
448
+ return@consumeAsync
449
+ }
450
+
451
+ val map = mutableMapOf<String, Any?>()
452
+ map["responseCode"] = billingResult.responseCode
453
+ map["debugMessage"] = billingResult.debugMessage
454
+ val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
455
+ map["code"] = errorData.code
456
+ map["message"] = errorData.message
457
+ map["purchaseToken"] = purchaseToken
458
+ promise.resolve(map)
459
+ }
460
+ }
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Rejects promise with billing code if BillingResult is not OK
466
+ */
467
+ private fun isValidResult(
468
+ billingResult: BillingResult,
469
+ promise: Promise,
470
+ ): Boolean {
471
+ Log.d(TAG, "responseCode: " + billingResult.responseCode)
472
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
473
+ PlayUtils.rejectPromiseWithBillingError(promise, billingResult.responseCode)
474
+ return false
475
+ }
476
+ return true
477
+ }
478
+
479
+ private fun ensureConnection(
480
+ promise: Promise,
481
+ callback: (billingClient: BillingClient) -> Unit,
482
+ ) {
483
+ if (billingClientCache?.isReady == true) {
484
+ callback(billingClientCache!!)
485
+ return
486
+ }
487
+
488
+ initBillingClient(promise, callback)
489
+ }
490
+
491
+ private fun initBillingClient(
492
+ promise: Promise,
493
+ callback: (billingClient: BillingClient) -> Unit,
494
+ ) {
495
+ if (GoogleApiAvailability
496
+ .getInstance()
497
+ .isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS
498
+ ) {
499
+ Log.i(TAG, "Google Play Services are not available on this device")
500
+ promise.reject(
501
+ E_NOT_PREPARED,
502
+ "Google Play Services are not available on this device",
503
+ null,
504
+ )
505
+ return
506
+ }
507
+
508
+ billingClientCache =
509
+ BillingClient
510
+ .newBuilder(context)
511
+ .setListener(this)
512
+ .enablePendingPurchases()
513
+ .build()
514
+
515
+ billingClientCache?.startConnection(
516
+ object : BillingClientStateListener {
517
+ override fun onBillingSetupFinished(billingResult: BillingResult) {
518
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
519
+ promise.reject(
520
+ E_INIT_CONNECTION,
521
+ "Billing setup finished with error: ${billingResult.debugMessage}",
522
+ null,
523
+ )
524
+ return
525
+ }
526
+ callback(billingClientCache!!)
527
+ }
528
+
529
+ override fun onBillingServiceDisconnected() {
530
+ Log.i(TAG, "Billing service disconnected")
531
+ }
532
+ },
533
+ )
534
+ }
535
+ }
@@ -0,0 +1,6 @@
1
+ package expo.modules.iap
2
+
3
+ import expo.modules.kotlin.exception.CodedException
4
+
5
+ class MissingCurrentActivityException :
6
+ CodedException("Activity which was provided during module initialization is no longer available")
@@ -0,0 +1,124 @@
1
+ package expo.modules.iap
2
+
3
+ import android.util.Log
4
+ import com.android.billingclient.api.BillingClient
5
+ import expo.modules.kotlin.Promise
6
+
7
+ data class BillingResponse(
8
+ val code: String,
9
+ val message: String,
10
+ )
11
+
12
+ object PromiseUtils {
13
+ private val promises = HashMap<String, MutableList<Promise>>()
14
+
15
+ const val TAG = "PromiseUtils"
16
+ const val E_ACTIVITY_UNAVAILABLE = "E_ACTIVITY_UNAVAILABLE"
17
+ const val E_UNKNOWN = "E_UNKNOWN"
18
+ const val E_NOT_PREPARED = "E_NOT_PREPARED"
19
+ const val E_ALREADY_PREPARED = "E_ALREADY_PREPARED"
20
+ const val E_PENDING = "E_PENDING"
21
+ const val E_NOT_ENDED = "E_NOT_ENDED"
22
+ const val E_USER_CANCELLED = "E_USER_CANCELLED"
23
+ const val E_ITEM_UNAVAILABLE = "E_ITEM_UNAVAILABLE"
24
+ const val E_NETWORK_ERROR = "E_NETWORK_ERROR"
25
+ const val E_SERVICE_ERROR = "E_SERVICE_ERROR"
26
+ const val E_ALREADY_OWNED = "E_ALREADY_OWNED"
27
+ const val E_REMOTE_ERROR = "E_REMOTE_ERROR"
28
+ const val E_USER_ERROR = "E_USER_ERROR"
29
+ const val E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR"
30
+ const val E_BILLING_RESPONSE_JSON_PARSE_ERROR = "E_BILLING_RESPONSE_JSON_PARSE_ERROR"
31
+ const val E_CONNECTION_CLOSED = "E_CONNECTION_CLOSED"
32
+ }
33
+
34
+ object PlayUtils {
35
+ const val TAG = "PlayUtils"
36
+
37
+ fun rejectPromiseWithBillingError(
38
+ promise: Promise,
39
+ responseCode: Int,
40
+ ) {
41
+ val errorData = getBillingResponseData(responseCode)
42
+ promise.reject(errorData.code, errorData.message, null)
43
+ }
44
+
45
+ fun getBillingResponseData(responseCode: Int): BillingResponse {
46
+ val errorData =
47
+ when (responseCode) {
48
+ BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> {
49
+ BillingResponse(
50
+ PromiseUtils.E_SERVICE_ERROR,
51
+ "This feature is not available on your device.",
52
+ )
53
+ }
54
+ BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> {
55
+ BillingResponse(
56
+ PromiseUtils.E_NETWORK_ERROR,
57
+ "The service is disconnected (check your internet connection.)",
58
+ )
59
+ }
60
+ BillingClient.BillingResponseCode.NETWORK_ERROR -> {
61
+ BillingResponse(
62
+ PromiseUtils.E_NETWORK_ERROR,
63
+ "You have a problem with network connection.",
64
+ )
65
+ }
66
+ BillingClient.BillingResponseCode.OK -> {
67
+ BillingResponse(
68
+ "OK",
69
+ "",
70
+ )
71
+ }
72
+ BillingClient.BillingResponseCode.USER_CANCELED -> {
73
+ BillingResponse(
74
+ PromiseUtils.E_USER_CANCELLED,
75
+ "Payment is cancelled.",
76
+ )
77
+ }
78
+ BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> {
79
+ BillingResponse(
80
+ PromiseUtils.E_SERVICE_ERROR,
81
+ "The service is unreachable. This may be your internet connection, or the Play Store may be down.",
82
+ )
83
+ }
84
+ BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> {
85
+ BillingResponse(
86
+ PromiseUtils.E_SERVICE_ERROR,
87
+ "Billing is unavailable. This may be a problem with your device, or the Play Store may be down.",
88
+ )
89
+ }
90
+ BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> {
91
+ BillingResponse(
92
+ PromiseUtils.E_ITEM_UNAVAILABLE,
93
+ "That item is unavailable.",
94
+ )
95
+ }
96
+ BillingClient.BillingResponseCode.DEVELOPER_ERROR -> {
97
+ BillingResponse(
98
+ PromiseUtils.E_DEVELOPER_ERROR,
99
+ "Google is indicating that we have some issue connecting to payment.",
100
+ )
101
+ }
102
+ BillingClient.BillingResponseCode.ERROR -> {
103
+ BillingResponse(
104
+ PromiseUtils.E_UNKNOWN,
105
+ "An unknown or unexpected error has occurred. Please try again later.",
106
+ )
107
+ }
108
+ BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
109
+ BillingResponse(
110
+ PromiseUtils.E_ALREADY_OWNED,
111
+ "You already own this item.",
112
+ )
113
+ }
114
+ else -> {
115
+ BillingResponse(
116
+ PromiseUtils.E_UNKNOWN,
117
+ "Purchase failed with code: $responseCode",
118
+ )
119
+ }
120
+ }
121
+ Log.e(TAG, "Error Code: $responseCode")
122
+ return errorData
123
+ }
124
+ }