@@ -2,12 +2,22 @@ package expo.modules.iap
22
33import android.content.Context
44import android.util.Log
5+ import dev.hyo.openiap.AndroidSubscriptionOfferInput
6+ import dev.hyo.openiap.DeepLinkOptions
7+ import dev.hyo.openiap.FetchProductsResultProducts
8+ import dev.hyo.openiap.FetchProductsResultSubscriptions
59import dev.hyo.openiap.OpenIapError
610import dev.hyo.openiap.OpenIapModule
7- import dev.hyo.openiap.models.DeepLinkOptions
8- import dev.hyo.openiap.models.ProductRequest
9- import dev.hyo.openiap.models.RequestPurchaseParams
10- import dev.hyo.openiap.models.RequestSubscriptionAndroidProps
11+ import dev.hyo.openiap.ProductQueryType
12+ import dev.hyo.openiap.ProductRequest
13+ import dev.hyo.openiap.Purchase
14+ import dev.hyo.openiap.RequestPurchaseAndroidProps
15+ import dev.hyo.openiap.RequestPurchaseProps
16+ import dev.hyo.openiap.RequestPurchasePropsByPlatforms
17+ import dev.hyo.openiap.RequestPurchaseResultPurchase
18+ import dev.hyo.openiap.RequestPurchaseResultPurchases
19+ import dev.hyo.openiap.RequestSubscriptionAndroidProps
20+ import dev.hyo.openiap.RequestSubscriptionPropsByPlatforms
1121import expo.modules.kotlin.Promise
1222import expo.modules.kotlin.exception.Exceptions
1323import expo.modules.kotlin.modules.Module
@@ -18,6 +28,7 @@ import kotlinx.coroutines.Job
1828import kotlinx.coroutines.launch
1929import kotlinx.coroutines.sync.Mutex
2030import kotlinx.coroutines.sync.withLock
31+ import java.util.Locale
2132import java.util.concurrent.ConcurrentLinkedQueue
2233import java.util.concurrent.atomic.AtomicBoolean
2334
@@ -59,7 +70,20 @@ class ExpoIapModule : Module() {
5970 pendingEvents.add(name to payload)
6071 }
6172
62- // Mapping helpers now provided by openiap-google (toJSON helpers)
73+ private fun parseProductQueryType (rawType : String? ): ProductQueryType {
74+ val normalized =
75+ rawType
76+ ?.trim()
77+ ?.lowercase(Locale .US )
78+ ?.replace(" -" , " " )
79+ ?.replace(" _" , " " )
80+
81+ return when (normalized) {
82+ " subs" -> ProductQueryType .Subs
83+ " all" -> ProductQueryType .All
84+ else -> ProductQueryType .InApp
85+ }
86+ }
6387
6488 override fun definition () =
6589 ModuleDefinition {
@@ -90,8 +114,9 @@ class ExpoIapModule : Module() {
90114 if (! listenersAttached) {
91115 listenersAttached = true
92116 openIap.addPurchaseUpdateListener { p ->
93- runCatching { emitOrQueue(EVENT_PURCHASE_UPDATED , p.toJSON()) }
94- .onFailure { Log .e(TAG , " Failed to buffer/send PURCHASE_UPDATED" , it) }
117+ runCatching {
118+ emitOrQueue(EVENT_PURCHASE_UPDATED , p.toJson())
119+ }.onFailure { Log .e(TAG , " Failed to buffer/send PURCHASE_UPDATED" , it) }
95120 }
96121 openIap.addPurchaseErrorListener { e ->
97122 runCatching { emitOrQueue(EVENT_PURCHASE_ERROR , e.toJSON()) }
@@ -140,9 +165,16 @@ class ExpoIapModule : Module() {
140165 AsyncFunction (" fetchProducts" ) { type: String , skuArr: Array <String >, promise: Promise ->
141166 scope.launch {
142167 try {
143- val reqType = ProductRequest .ProductRequestType .fromString(type)
144- val products = openIap.fetchProducts(ProductRequest (skuArr.toList(), reqType))
145- promise.resolve(products.map { it.toJSON() })
168+ val queryType = parseProductQueryType(type)
169+ val request = ProductRequest (skuArr.toList(), queryType)
170+ val result = openIap.fetchProducts(request)
171+ val payload =
172+ when (result) {
173+ is FetchProductsResultProducts -> result.value.orEmpty().map { it.toJson() }
174+ is FetchProductsResultSubscriptions -> result.value.orEmpty().map { it.toJson() }
175+ else -> emptyList<Map <String , Any ?>>()
176+ }
177+ promise.resolve(payload)
146178 } catch (e: Exception ) {
147179 promise.reject(OpenIapError .QueryProduct .CODE , e.message, null )
148180 }
@@ -153,7 +185,7 @@ class ExpoIapModule : Module() {
153185 scope.launch {
154186 try {
155187 val purchases = openIap.getAvailablePurchases(null )
156- promise.resolve(purchases.map { it.toJSON () })
188+ promise.resolve(purchases.map { it.toJson () })
157189 } catch (e: Exception ) {
158190 promise.reject(OpenIapError .ServiceUnavailable .CODE , e.message, null )
159191 }
@@ -166,7 +198,12 @@ class ExpoIapModule : Module() {
166198 val packageName = (params[" packageName" ] ? : params[" packageNameAndroid" ]) as ? String
167199 scope.launch {
168200 try {
169- openIap.deepLinkToSubscriptions(DeepLinkOptions (sku, packageName))
201+ openIap.deepLinkToSubscriptions(
202+ DeepLinkOptions (
203+ packageNameAndroid = packageName,
204+ skuAndroid = sku,
205+ ),
206+ )
170207 promise.resolve(null )
171208 } catch (e: Exception ) {
172209 promise.reject(OpenIapError .ServiceUnavailable .CODE , e.message, null )
@@ -187,7 +224,7 @@ class ExpoIapModule : Module() {
187224 }
188225
189226 AsyncFunction (" requestPurchase" ) { params: Map <String , Any ?>, promise: Promise ->
190- val type = params[" type" ] as String
227+ val type = params[" type" ] as ? String
191228 val skus: List <String > =
192229 (params[" skus" ] as ? List <* >)?.filterIsInstance<String >()
193230 ? : (params[" skuArr" ] as ? List <* >)?.filterIsInstance<String >()
@@ -199,75 +236,126 @@ class ExpoIapModule : Module() {
199236 val isOfferPersonalized = params[" isOfferPersonalized" ] as ? Boolean ? : false
200237 val offerTokenArr =
201238 (params[" offerTokenArr" ] as ? List <* >)?.filterIsInstance<String >() ? : emptyList()
202- val subscriptionOffersParam =
239+ val explicitSubscriptionOffers =
203240 (params[" subscriptionOffers" ] as ? List <* >)?.mapNotNull { rawOffer ->
204241 val offerMap = rawOffer as ? Map <* , * > ? : return @mapNotNull null
205242 val sku = offerMap[" sku" ] as ? String
206243 val offerToken = offerMap[" offerToken" ] as ? String
207244 if (sku.isNullOrEmpty() || offerToken.isNullOrEmpty()) {
208245 null
209246 } else {
210- RequestSubscriptionAndroidProps . SubscriptionOffer (sku = sku, offerToken = offerToken )
247+ AndroidSubscriptionOfferInput (offerToken = offerToken, sku = sku )
211248 }
212249 } ? : emptyList()
250+ val purchaseToken =
251+ (params[" purchaseTokenAndroid" ] ? : params[" purchaseToken" ]) as ? String
252+ val replacementMode =
253+ (params[" replacementModeAndroid" ] ? : params[" replacementMode" ]) as ? Number
213254
214- PromiseUtils .addPromiseForKey(PromiseUtils .PROMISE_BUY_ITEM , promise)
215- scope.launch {
216- try {
217- openIap.setActivity(currentActivity)
218- val reqType = ProductRequest .ProductRequestType .fromString(type)
219- val subscriptionOffers =
220- if (reqType == ProductRequest .ProductRequestType .Subs ) {
221- when {
222- subscriptionOffersParam.isNotEmpty() -> subscriptionOffersParam
223- offerTokenArr.isNotEmpty() ->
224- skus.zip(offerTokenArr).mapNotNull { (sku, token) ->
225- if (token.isNotEmpty()) {
226- RequestSubscriptionAndroidProps .SubscriptionOffer (
227- sku = sku,
228- offerToken = token,
229- )
230- } else {
231- null
232- }
233- }
234- else -> emptyList()
235- }
255+ val productType =
256+ when (parseProductQueryType(type)) {
257+ ProductQueryType .Subs -> ProductQueryType .Subs
258+ else -> ProductQueryType .InApp
259+ }
260+
261+ val fallbackOffers =
262+ if (explicitSubscriptionOffers.isEmpty() && offerTokenArr.isNotEmpty()) {
263+ skus.zip(offerTokenArr).mapNotNull { (sku, token) ->
264+ if (token.isNotEmpty()) {
265+ AndroidSubscriptionOfferInput (offerToken = token, sku = sku)
236266 } else {
237- emptyList()
267+ null
238268 }
239- val result =
240- openIap.requestPurchase(
241- RequestPurchaseParams (
242- skus = skus,
269+ }
270+ } else {
271+ emptyList()
272+ }
273+
274+ val subscriptionOffers =
275+ (explicitSubscriptionOffers.ifEmpty { fallbackOffers })
276+ .takeIf { it.isNotEmpty() }
277+
278+ val requestProps =
279+ when (productType) {
280+ ProductQueryType .Subs -> {
281+ val android =
282+ RequestSubscriptionAndroidProps (
283+ isOfferPersonalized = isOfferPersonalized,
243284 obfuscatedAccountIdAndroid = obfuscatedAccountId,
244285 obfuscatedProfileIdAndroid = obfuscatedProfileId,
245- isOfferPersonalized = isOfferPersonalized,
286+ purchaseTokenAndroid = purchaseToken,
287+ replacementModeAndroid = replacementMode?.toInt(),
288+ skus = skus,
246289 subscriptionOffers = subscriptionOffers,
247- ),
248- reqType,
290+ )
291+ RequestPurchaseProps (
292+ request =
293+ RequestPurchaseProps .Request .Subscription (
294+ RequestSubscriptionPropsByPlatforms (android = android),
295+ ),
296+ type = ProductQueryType .Subs ,
249297 )
250- result.forEach { p ->
251- try {
252- emitOrQueue(EVENT_PURCHASE_UPDATED , p.toJSON())
253- } catch (ex: Exception ) {
254- Log .e(TAG , " Failed to send PURCHASE_UPDATED event (requestPurchase)" , ex)
298+ }
299+
300+ else -> {
301+ val android =
302+ RequestPurchaseAndroidProps (
303+ isOfferPersonalized = isOfferPersonalized,
304+ obfuscatedAccountIdAndroid = obfuscatedAccountId,
305+ obfuscatedProfileIdAndroid = obfuscatedProfileId,
306+ skus = skus,
307+ )
308+ RequestPurchaseProps (
309+ request =
310+ RequestPurchaseProps .Request .Purchase (
311+ RequestPurchasePropsByPlatforms (android = android),
312+ ),
313+ type = ProductQueryType .InApp ,
314+ )
315+ }
316+ }
317+
318+ PromiseUtils .addPromiseForKey(PromiseUtils .PROMISE_BUY_ITEM , promise)
319+ scope.launch {
320+ try {
321+ openIap.setActivity(currentActivity)
322+ val result = openIap.requestPurchase(requestProps)
323+ val purchases =
324+ when (result) {
325+ is RequestPurchaseResultPurchases -> result.value.orEmpty()
326+ is RequestPurchaseResultPurchase -> result.value?.let (::listOf).orEmpty()
327+ else -> emptyList()
328+ }
329+ purchases.forEach { purchase ->
330+ runCatching {
331+ emitOrQueue(EVENT_PURCHASE_UPDATED , purchase.toJson())
332+ }.onFailure { ex ->
333+ Log .e(
334+ TAG ,
335+ " Failed to send PURCHASE_UPDATED event (requestPurchase)" ,
336+ ex,
337+ )
255338 }
256339 }
257- PromiseUtils .resolvePromisesForKey(PromiseUtils .PROMISE_BUY_ITEM , result.map { it.toJSON() })
340+ PromiseUtils .resolvePromisesForKey(
341+ PromiseUtils .PROMISE_BUY_ITEM ,
342+ purchases.map { it.toJson() },
343+ )
258344 } catch (e: Exception ) {
259345 val errorMap =
260346 mapOf (
261347 " code" to OpenIapError .PurchaseFailed .CODE ,
262348 " message" to (e.message ? : " Purchase failed" ),
263349 " platform" to " android" ,
264350 )
265- try {
266- emitOrQueue(EVENT_PURCHASE_ERROR , errorMap)
267- } catch (ex: Exception ) {
268- Log .e(TAG , " Failed to send PURCHASE_ERROR event (requestPurchase)" , ex)
269- }
270- // Reject and clear any pending promises for this purchase flow
351+ runCatching { emitOrQueue(EVENT_PURCHASE_ERROR , errorMap) }
352+ .onFailure { ex ->
353+ Log .e(
354+ TAG ,
355+ " Failed to send PURCHASE_ERROR event (requestPurchase)" ,
356+ ex,
357+ )
358+ }
271359 PromiseUtils .rejectPromisesForKey(
272360 PromiseUtils .PROMISE_BUY_ITEM ,
273361 OpenIapError .PurchaseFailed .CODE ,
0 commit comments