Skip to content

feat(Android Amazon): Amazon SDK 3.0.7 + Add proration and modify subscription API #2943

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dooboolab.test;

import android.os.Bundle;
import com.dooboolab.rniap.RNIapActivityListener;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
Expand Down Expand Up @@ -29,4 +31,11 @@ protected ReactActivityDelegate createReactActivityDelegate() {
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled());
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Required by Amazon IAP SDK
RNIapActivityListener.registerActivity(this);
}
}
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ RNIap_compileSdkVersion=33
RNIap_buildToolsVersion=33.0.0
RNIap_ndkversion=23.1.7779620
RNIap_playServicesVersion=18.1.0
RNIap_amazonSdkVersion=3.0.5
RNIap_amazonSdkVersion=3.0.7
RNIap_playBillingSdkVersion=7.0.0
RNIap_isAmazonDrmEnabled=true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.dooboolab.rniap
import android.content.Context
import com.amazon.device.iap.PurchasingListener
import com.amazon.device.iap.model.FulfillmentResult
import com.amazon.device.iap.model.ProrationMode
import com.amazon.device.iap.model.RequestId

interface PurchasingServiceProxy {
Expand All @@ -23,4 +24,6 @@ interface PurchasingServiceProxy {
var0: String?,
var1: FulfillmentResult?,
)

fun modifySubscription(var0: String?, var1: ProrationMode): RequestId
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import com.amazon.device.iap.PurchasingListener
import com.amazon.device.iap.PurchasingService
import com.amazon.device.iap.model.FulfillmentResult
import com.amazon.device.iap.model.ProrationMode
import com.amazon.device.iap.model.RequestId

class PurchasingServiceProxyAmazonImpl : PurchasingServiceProxy {
Expand All @@ -24,4 +25,7 @@ class PurchasingServiceProxyAmazonImpl : PurchasingServiceProxy {
var0: String?,
var1: FulfillmentResult?,
) = PurchasingService.notifyFulfillment(var0, var1)

override fun modifySubscription(var0: String?, var1: ProrationMode): RequestId =
PurchasingService.modifySubscription(var0, var1)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.dooboolab.rniap
import android.app.Activity
import android.util.Log
import com.amazon.device.iap.PurchasingService
import com.dooboolab.rniap.modifysubscription.RNIapAmazonModifySubscriptionListener

/**
* In order of the IAP process to show correctly, AmazonPurchasingService must be registered on Activity.onCreate
Expand All @@ -16,12 +17,33 @@ class RNIapActivityListener {
@JvmStatic
var amazonListener: RNIapAmazonListener? = null

@JvmStatic
var amazonModifySubscriptionListener: RNIapAmazonModifySubscriptionListener? = null

@JvmStatic
fun initListeners(eventSender: EventSender?, purchasingService: PurchasingServiceProxy?) {
if (amazonListener == null || amazonModifySubscriptionListener == null) {
Log.e(
RNIapAmazonModule.TAG,
"registerActivity should be called on Activity.onCreate ",
)
}

amazonListener?.eventSender = eventSender
amazonListener?.purchasingService = purchasingService
amazonModifySubscriptionListener?.eventSender = eventSender
}

@JvmStatic
fun registerActivity(activity: Activity) {
amazonListener = RNIapAmazonListener(null, null)
amazonModifySubscriptionListener = RNIapAmazonModifySubscriptionListener(null)

try {
PurchasingService.registerListener(activity, amazonListener)
PurchasingService.registerListener(activity, amazonModifySubscriptionListener)
hasListener = true

// Prefetch user and purchases as per Amazon SDK documentation:
PurchasingService.getUserData()
PurchasingService.getPurchaseUpdates(false)
Expand Down
26 changes: 4 additions & 22 deletions android/src/amazon/java/com/dooboolab/rniap/RNIapAmazonListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import com.amazon.device.iap.model.ProductDataResponse
import com.amazon.device.iap.model.ProductType
import com.amazon.device.iap.model.PurchaseResponse
import com.amazon.device.iap.model.PurchaseUpdatesResponse
import com.amazon.device.iap.model.Receipt
import com.amazon.device.iap.model.UserData
import com.amazon.device.iap.model.UserDataResponse
import com.dooboolab.rniap.utils.toMap
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeArray
Expand Down Expand Up @@ -90,7 +89,7 @@ class RNIapAmazonListener(
var promiseItem: WritableMap? = null
val purchases = response.receipts
for (receipt in purchases) {
val item = receiptToMap(userData, receipt)
val item = receipt.toMap(userData)
promiseItem = WritableNativeMap()
promiseItem.merge(item)
eventSender?.sendEvent("purchase-updated", item)
Expand Down Expand Up @@ -169,24 +168,6 @@ class RNIapAmazonListener(
}
}

private fun receiptToMap(
userData: UserData,
receipt: Receipt,
): WritableMap {
val item = Arguments.createMap()
item.putString("productId", receipt.sku)
item.putDouble("transactionDate", receipt.purchaseDate.time.toDouble())
item.putString("purchaseToken", receipt.receiptId)
item.putString("transactionReceipt", receipt.toJSON().toString())
item.putString("userIdAmazon", userData.userId)
item.putString("userMarketplaceAmazon", userData.marketplace)
item.putString("userJsonAmazon", userData.toJSON().toString())
item.putBoolean("isCanceledAmazon", receipt.isCanceled)
item.putString("termSku", receipt.termSku)
item.putString("productType", receipt.productType.typeString)
return item
}

override fun onPurchaseResponse(response: PurchaseResponse) {
val requestId = response.requestId.toString()
val userId = response.userData.userId
Expand All @@ -195,7 +176,7 @@ class RNIapAmazonListener(
PurchaseResponse.RequestStatus.SUCCESSFUL -> {
val receipt = response.receipt
val userData = response.userData
val item = receiptToMap(userData, receipt)
val item = receipt.toMap(userData)
val promiseItem: WritableMap = Arguments.createMap()
promiseItem.merge(item)
eventSender?.sendEvent("purchase-updated", item)
Expand Down Expand Up @@ -301,6 +282,7 @@ class RNIapAmazonListener(
val item = Arguments.createMap()
item.putString("userIdAmazon", userData.userId)
item.putString("userMarketplaceAmazon", userData.marketplace)
item.putString("userCountryCode", userData.countryCode)
item.putString("userJsonAmazon", userData.toJSON().toString())
PromiseUtils
.resolvePromisesForKey(RNIapAmazonModule.PROMISE_GET_USER_DATA, item)
Expand Down
14 changes: 11 additions & 3 deletions android/src/amazon/java/com/dooboolab/rniap/RNIapAmazonModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.core.content.ContextCompat.startActivity
import com.amazon.device.drm.LicensingService
import com.amazon.device.drm.model.LicenseResponse
import com.amazon.device.iap.model.FulfillmentResult
import com.amazon.device.iap.model.ProrationMode
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
Expand Down Expand Up @@ -50,8 +51,7 @@ class RNIapAmazonModule(
}
}
}
RNIapActivityListener.amazonListener?.eventSender = eventSender
RNIapActivityListener.amazonListener?.purchasingService = purchasingService
RNIapActivityListener.initListeners(eventSender, purchasingService)
promise.resolve(true)
}

Expand Down Expand Up @@ -131,10 +131,18 @@ class RNIapAmazonModule(
@ReactMethod
fun buyItemByType(
sku: String?,
proration: String?,
promise: Promise,
) {
PromiseUtils.addPromiseForKey(PROMISE_BUY_ITEM, promise)
val requestId = purchasingService.purchase(sku)
val requestId = if (!proration.isNullOrEmpty()) {
purchasingService.modifySubscription(
sku,
if (proration == "DEFERRED") ProrationMode.DEFERRED else ProrationMode.IMMEDIATE,
)
} else {
purchasingService.purchase(sku)
}
}

@ReactMethod
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.dooboolab.rniap.modifysubscription

import android.util.Log
import com.amazon.device.iap.ModifySubscriptionListener
import com.amazon.device.iap.model.ModifySubscriptionResponse
import com.dooboolab.rniap.EventSender
import com.dooboolab.rniap.PromiseUtils
import com.dooboolab.rniap.RNIapAmazonModule
import com.dooboolab.rniap.utils.toMap
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap

class RNIapAmazonModifySubscriptionListener(
var eventSender: EventSender?,
) : ModifySubscriptionListener {

companion object {
private const val TAG = "AmazonModifySbnListener"
}

override fun onModifySubscriptionResponse(response: ModifySubscriptionResponse) {
Log.d(TAG, "onModifySubscriptionResponse ${response.requestStatus}")

when (response.requestStatus) {
ModifySubscriptionResponse.RequestStatus.SUCCESSFUL -> {
response.receipts.map { receipt ->
val item = receipt.toMap(response.userData)
val promiseItem: WritableMap = Arguments.createMap()
promiseItem.merge(item)
eventSender?.sendEvent("purchase-updated", item)
PromiseUtils
.resolvePromisesForKey(
RNIapAmazonModule.PROMISE_BUY_ITEM,
promiseItem,
)
}
}

else -> {
val messageAndCode = when (response.requestStatus) {
ModifySubscriptionResponse.RequestStatus.FAILED -> Pair(
"An unknown or unexpected error has occurred. Please try again later.",
PromiseUtils.E_UNKNOWN,
)

ModifySubscriptionResponse.RequestStatus.INVALID_SKU -> Pair(
"That item is unavailable.",
PromiseUtils.E_ITEM_UNAVAILABLE,
)

ModifySubscriptionResponse.RequestStatus.NOT_SUPPORTED -> Pair(
"This feature is not available on your device.",
PromiseUtils.E_SERVICE_ERROR,
)

else -> null
}

if (messageAndCode == null) {
Log.d(
TAG,
"onModifySubscriptionResponse: ${response.requestStatus} is not handled",
)
return
}

val debugMessage = messageAndCode.first
val errorCode = messageAndCode.second

Arguments.createMap().also {
it.putInt("responseCode", 0)
it.putString("debugMessage", debugMessage)
it.putString("code", errorCode)
it.putString("message", debugMessage)
eventSender?.sendEvent("purchase-error", it)
}

PromiseUtils
.rejectPromisesForKey(
RNIapAmazonModule.PROMISE_BUY_ITEM,
errorCode,
debugMessage,
null,
)
}
}
}
}
22 changes: 22 additions & 0 deletions android/src/amazon/java/com/dooboolab/rniap/utils/Extensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.dooboolab.rniap.utils

import com.amazon.device.iap.model.Receipt
import com.amazon.device.iap.model.UserData
import com.dooboolab.rniap.typeString
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap

fun Receipt.toMap(userData: UserData): WritableMap {
return Arguments.createMap().also {
it.putString("productId", sku)
it.putDouble("transactionDate", purchaseDate.time.toDouble())
it.putString("purchaseToken", receiptId)
it.putString("transactionReceipt", this.toJSON().toString())
it.putString("userIdAmazon", userData.userId)
it.putString("userMarketplaceAmazon", userData.marketplace)
it.putString("userJsonAmazon", userData.toJSON().toString())
it.putBoolean("isCanceledAmazon", isCanceled)
it.putString("termSku", termSku)
it.putString("productType", productType.typeString)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class RNIapAmazonModuleTest {

val promise = mockk<Promise>(relaxed = true)

module.buyItemByType("mySku", promise)
module.buyItemByType("mySku", "", promise)
verify(exactly = 0) { promise.reject(any(), any<String>()) }
val response = slot<WritableMap>()
verify { promise.resolve(capture(response)) }
Expand Down
14 changes: 14 additions & 0 deletions patches/react-native+0.73.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
diff --git a/node_modules/react-native/third-party-podspecs/boost.podspec b/node_modules/react-native/third-party-podspecs/boost.podspec
index 3950fce..8cd4a8a 100644
--- a/node_modules/react-native/third-party-podspecs/boost.podspec
+++ b/node_modules/react-native/third-party-podspecs/boost.podspec
@@ -10,7 +10,8 @@ Pod::Spec.new do |spec|
spec.homepage = 'http://www.boost.org'
spec.summary = 'Boost provides free peer-reviewed portable C++ source libraries.'
spec.authors = 'Rene Rivera'
- spec.source = { :http => 'https://boostorg.jfrog.io/artifactory/main/release/1.83.0/source/boost_1_83_0.tar.bz2',
+ # Patched due to issue https://github.com/boostorg/boost/issues/843
+ spec.source = { :http => 'https://sourceforge.net/projects/boost/files/boost/1.83.0/boost_1_83_0.tar.bz2',
:sha256 => '6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e' }

# Pinning to the same version as React.podspec.
11 changes: 8 additions & 3 deletions src/iap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
Product,
ProductPurchase,
ProductType,
ProrationModesAmazon,
Purchase,
PurchaseResult,
PurchaseStateAndroid,
Expand Down Expand Up @@ -632,7 +633,7 @@ export const requestPurchase = (
throw new Error('sku is required for Amazon purchase');
}
const {sku} = request;
return RNIapAmazonModule.buyItemByType(sku);
return RNIapAmazonModule.buyItemByType(sku, '');
} else {
if (!('skus' in request) || !request.skus.length) {
throw new Error('skus is required for Android purchase');
Expand All @@ -644,7 +645,7 @@ export const requestPurchase = (
obfuscatedProfileIdAndroid,
isOfferPersonalized,
} = request;
return getAndroidModule().buyItemByType(
return RNIapModule.buyItemByType(
ANDROID_ITEM_TYPE_IAP,
skus,
undefined,
Expand Down Expand Up @@ -789,7 +790,11 @@ export const requestSubscription = (
throw new Error('sku is required for Amazon subscriptions');
}
const {sku} = request;
return RNIapAmazonModule.buyItemByType(sku);
var prorationModeAmazon: ProrationModesAmazon = '';
if ('prorationModeAmazon' in request) {
prorationModeAmazon = request.prorationModeAmazon || '';
}
return RNIapAmazonModule.buyItemByType(sku, prorationModeAmazon);
} else {
if (
!('subscriptionOffers' in request) ||
Expand Down
7 changes: 5 additions & 2 deletions src/modules/amazon.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {NativeModules} from 'react-native';

import {enhancedFetch} from '../internal';
import type {Product, Purchase, Sku} from '../types';
import type {Product, ProrationModesAmazon, Purchase, Sku} from '../types';
import type {
AmazonLicensingStatus,
ReceiptType,
Expand All @@ -15,7 +15,10 @@ type GetUser = () => Promise<UserDataAmazon>;
type FlushFailedPurchasesCachedAsPending = () => Promise<boolean>;
type GetItemsByType = (type: string, skus: Sku[]) => Promise<Product[]>;
type GetAvailableItems = () => Promise<Purchase[]>;
type BuyItemByType = (sku: Sku) => Promise<Purchase>;
type BuyItemByType = (
sku: Sku,
proration: ProrationModesAmazon,
) => Promise<Purchase>;

type AcknowledgePurchase = (
purchaseToken: string,
Expand Down
Loading
Loading