diff --git a/IapExample/ios/Podfile.lock b/IapExample/ios/Podfile.lock index 0b9223e4e..9f45487db 100644 --- a/IapExample/ios/Podfile.lock +++ b/IapExample/ios/Podfile.lock @@ -360,7 +360,7 @@ PODS: - React-Core - RNGestureHandler (2.5.0): - React-Core - - RNIap (11.0.0-alpha.1): + - RNIap (12.4.1): - React-Core - RNScreens (3.15.0): - React-Core @@ -575,7 +575,7 @@ SPEC CHECKSUMS: ReactCommon: e30ec17dfb1d4c4f3419eac254350d6abca6d5a2 RNCMaskedView: cb9670ea9239998340eaab21df13fa12a1f9de15 RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50 - RNIap: ec6593b74c48fa06f4614ff9ec2dc2e4dffcba0c + RNIap: a694c95267d38c882adf68999814edd17fc343ce RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7 SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 Yoga: 7ab6e3ee4ce47d7b789d1cb520163833e515f452 diff --git a/IapExample/package.json b/IapExample/package.json index 96fbcb41e..0ccc4da8e 100644 --- a/IapExample/package.json +++ b/IapExample/package.json @@ -7,7 +7,7 @@ "android:amazon": "react-native run-android --variant=AmazonDebug", "ios": "react-native run-ios", "start": "react-native start", - "pods": "pod-install --quiet", + "pods": "cd ios && pod install", "test": "jest" }, "dependencies": { diff --git a/android/build.gradle b/android/build.gradle index 688f3c519..ed5c7be53 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -70,6 +70,9 @@ android { } testOptions { + unitTests.all { + jvmArgs '-noverify' + } unitTests.returnDefaultValues = true } } diff --git a/android/src/amazon/java/com/dooboolab/RNIap/PurchasingServiceProxy.kt b/android/src/amazon/java/com/dooboolab/RNIap/PurchasingServiceProxy.kt new file mode 100644 index 000000000..46c06db2d --- /dev/null +++ b/android/src/amazon/java/com/dooboolab/RNIap/PurchasingServiceProxy.kt @@ -0,0 +1,20 @@ +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.RequestId + +interface PurchasingServiceProxy { + fun registerListener(var0: Context?, var1: PurchasingListener?) + + fun getUserData(): RequestId + + fun purchase(var0: String?): RequestId + + fun getProductData(var0: Set?): RequestId + + fun getPurchaseUpdates(var0: Boolean): RequestId + + fun notifyFulfillment(var0: String?, var1: FulfillmentResult?) +} diff --git a/android/src/amazon/java/com/dooboolab/RNIap/PurchasingServiceProxyAmazonImpl.kt b/android/src/amazon/java/com/dooboolab/RNIap/PurchasingServiceProxyAmazonImpl.kt new file mode 100644 index 000000000..f63c9bd6b --- /dev/null +++ b/android/src/amazon/java/com/dooboolab/RNIap/PurchasingServiceProxyAmazonImpl.kt @@ -0,0 +1,33 @@ +package com.dooboolab.RNIap + +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.RequestId + +class PurchasingServiceProxyAmazonImpl : PurchasingServiceProxy { + override fun registerListener(var0: Context?, var1: PurchasingListener?) { + return PurchasingService.registerListener(var0, var1) + } + + override fun getUserData(): RequestId { + return PurchasingService.getUserData() + } + + override fun purchase(var0: String?): RequestId { + return PurchasingService.purchase(var0) + } + + override fun getProductData(var0: Set?): RequestId { + return PurchasingService.getProductData(var0) + } + + override fun getPurchaseUpdates(var0: Boolean): RequestId { + return PurchasingService.getPurchaseUpdates(var0) + } + + override fun notifyFulfillment(var0: String?, var1: FulfillmentResult?) { + return PurchasingService.notifyFulfillment(var0, var1) + } +} diff --git a/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonListener.kt b/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonListener.kt index e75fa94f4..b9615a797 100644 --- a/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonListener.kt +++ b/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonListener.kt @@ -2,8 +2,6 @@ package com.dooboolab.RNIap import android.util.Log import com.amazon.device.iap.PurchasingListener -import com.amazon.device.iap.PurchasingService -import com.amazon.device.iap.model.Product import com.amazon.device.iap.model.ProductDataResponse import com.amazon.device.iap.model.ProductType import com.amazon.device.iap.model.PurchaseResponse @@ -18,26 +16,21 @@ import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter import java.lang.NumberFormatException -import java.util.ArrayList val ProductType.typeString: String get() = if (this == ProductType.ENTITLED || this == ProductType.CONSUMABLE) "inapp" else "subs" -class RNIapAmazonListener(private val reactContext: ReactContext) : PurchasingListener { - private val skus: MutableList +class RNIapAmazonListener( + private val reactContext: ReactContext, + private val purchasingService: PurchasingServiceProxy +) : PurchasingListener { override fun onProductDataResponse(response: ProductDataResponse) { - val requestId = response.requestId.toString() when (response.requestStatus) { ProductDataResponse.RequestStatus.SUCCESSFUL -> { val productData = response.productData - val unavailableSkus = response.unavailableSkus val items = WritableNativeArray() for ((_, product) in productData) { - if (!skus.contains(product)) { - skus.add(product) - } - var priceNumber: Number = 0.00 val priceString = product.price try { @@ -107,7 +100,7 @@ class RNIapAmazonListener(private val reactContext: ReactContext) : PurchasingLi availableItems.pushMap(promiseItem) } if (response.hasMore()) { - PurchasingService.getPurchaseUpdates(false) + purchasingService.getPurchaseUpdates(false) } else { if (purchases.size > 0 && promiseItem != null) { PromiseUtils @@ -197,13 +190,8 @@ class RNIapAmazonListener(private val reactContext: ReactContext) : PurchasingLi override fun onPurchaseResponse(response: PurchaseResponse) { val requestId = response.requestId.toString() val userId = response.userData.userId - val status = response.requestStatus - // Info for potential error reporting - val debugMessage: String? - var errorCode = PromiseUtils.E_UNKNOWN - val error = Arguments.createMap() - when (status) { + when (response.requestStatus) { PurchaseResponse.RequestStatus.SUCCESSFUL -> { val receipt = response.receipt val userData = response.userData @@ -218,8 +206,9 @@ class RNIapAmazonListener(private val reactContext: ReactContext) : PurchasingLi ) } PurchaseResponse.RequestStatus.ALREADY_PURCHASED -> { - debugMessage = "You already own this item." - errorCode = PromiseUtils.E_ALREADY_OWNED + val error = Arguments.createMap() + val debugMessage = "You already own this item." + val errorCode = PromiseUtils.E_ALREADY_OWNED error.putInt("responseCode", 0) error.putString("debugMessage", debugMessage) error.putString("code", errorCode) @@ -234,9 +223,10 @@ class RNIapAmazonListener(private val reactContext: ReactContext) : PurchasingLi ) } PurchaseResponse.RequestStatus.FAILED -> { - debugMessage = + val error = Arguments.createMap() + val debugMessage = "An unknown or unexpected error has occurred. Please try again later." - errorCode = PromiseUtils.E_UNKNOWN + val errorCode = PromiseUtils.E_UNKNOWN error.putInt("responseCode", 0) error.putString("debugMessage", debugMessage) error.putString("code", errorCode) @@ -251,8 +241,9 @@ class RNIapAmazonListener(private val reactContext: ReactContext) : PurchasingLi ) } PurchaseResponse.RequestStatus.INVALID_SKU -> { - debugMessage = "That item is unavailable." - errorCode = PromiseUtils.E_ITEM_UNAVAILABLE + val error = Arguments.createMap() + val debugMessage = "That item is unavailable." + val errorCode = PromiseUtils.E_ITEM_UNAVAILABLE error.putInt("responseCode", 0) error.putString("debugMessage", debugMessage) error.putString("code", errorCode) @@ -267,8 +258,9 @@ class RNIapAmazonListener(private val reactContext: ReactContext) : PurchasingLi ) } PurchaseResponse.RequestStatus.NOT_SUPPORTED -> { - debugMessage = "This feature is not available on your device." - errorCode = PromiseUtils.E_SERVICE_ERROR + val error = Arguments.createMap() + val debugMessage = "This feature is not available on your device." + val errorCode = PromiseUtils.E_SERVICE_ERROR error.putInt("responseCode", 0) error.putString("debugMessage", debugMessage) error.putString("code", errorCode) @@ -314,11 +306,8 @@ class RNIapAmazonListener(private val reactContext: ReactContext) : PurchasingLi ) } } - fun clear() { - skus.clear() - } - private fun sendEvent( + fun sendEvent( reactContext: ReactContext, eventName: String, params: WritableMap? @@ -344,8 +333,4 @@ class RNIapAmazonListener(private val reactContext: ReactContext) : PurchasingLi private const val E_USER_DATA_RESPONSE_NOT_SUPPORTED = "E_USER_DATA_RESPONSE_NOT_SUPPORTED" const val TAG = "RNIapAmazonListener" } - - init { - skus = ArrayList() - } } diff --git a/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt b/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt index f1c76ac56..6c8a5ded4 100644 --- a/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt +++ b/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt @@ -1,9 +1,11 @@ package com.dooboolab.RNIap +import android.os.Handler +import android.os.Looper import android.util.Log import com.amazon.device.drm.LicensingService import com.amazon.device.drm.model.LicenseResponse -import com.amazon.device.iap.PurchasingService +import com.amazon.device.iap.PurchasingListener import com.amazon.device.iap.model.FulfillmentResult import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise @@ -11,14 +13,17 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.module.annotations.ReactModule @ReactModule(name = RNIapAmazonModule.TAG) -class RNIapAmazonModule(reactContext: ReactApplicationContext) : +class RNIapAmazonModule( + reactContext: ReactApplicationContext, + private val purchasingService: PurchasingServiceProxy = PurchasingServiceProxyAmazonImpl(), + private val handler: Handler = Handler(Looper.getMainLooper()), + private val amazonListener: PurchasingListener = RNIapAmazonListener(reactContext, purchasingService) +) : ReactContextBaseJavaModule(reactContext) { var hasListener = false - private var amazonListener: RNIapAmazonListener? = null override fun getName(): String { return TAG } @@ -26,20 +31,19 @@ class RNIapAmazonModule(reactContext: ReactApplicationContext) : @ReactMethod fun initConnection(promise: Promise) { val context = reactApplicationContext - val amazonListener = RNIapAmazonListener(context) - this.amazonListener = amazonListener - UiThreadUtil.runOnUiThread { + + handler.postDelayed({ try { - PurchasingService.registerListener(context.applicationContext, amazonListener) + purchasingService.registerListener(context.applicationContext, amazonListener) hasListener = true // Prefetch user and purchases as per Amazon SDK documentation: - PurchasingService.getUserData() - PurchasingService.getPurchaseUpdates(false) + purchasingService.getUserData() + purchasingService.getPurchaseUpdates(false) promise.safeResolve(true) } catch (e: Exception) { promise.safeReject("Error initializing Amazon appstore sdk", e) } - } + }, 0L) } @ReactMethod @@ -84,14 +88,13 @@ class RNIapAmazonModule(reactContext: ReactApplicationContext) : @ReactMethod fun endConnection(promise: Promise) { PromiseUtils.rejectAllPendingPromises() - amazonListener?.clear() hasListener = false promise.resolve(true) } @ReactMethod fun getUser(promise: Promise) { - val requestId = PurchasingService.getUserData() + val requestId = purchasingService.getUserData() PromiseUtils.addPromiseForKey(PROMISE_GET_USER_DATA, promise) } @@ -114,13 +117,13 @@ class RNIapAmazonModule(reactContext: ReactApplicationContext) : ii++ } PromiseUtils.addPromiseForKey(PROMISE_GET_PRODUCT_DATA, promise) - val requestId = PurchasingService.getProductData(productSkus) + val requestId = purchasingService.getProductData(productSkus) } @ReactMethod fun getAvailableItems(promise: Promise) { PromiseUtils.addPromiseForKey(PROMISE_QUERY_AVAILABLE_ITEMS, promise) - PurchasingService.getPurchaseUpdates(true) + purchasingService.getPurchaseUpdates(true) } @ReactMethod @@ -129,7 +132,7 @@ class RNIapAmazonModule(reactContext: ReactApplicationContext) : promise: Promise ) { PromiseUtils.addPromiseForKey(PROMISE_BUY_ITEM, promise) - val requestId = PurchasingService.purchase(sku) + val requestId = purchasingService.purchase(sku) } @ReactMethod @@ -138,7 +141,7 @@ class RNIapAmazonModule(reactContext: ReactApplicationContext) : developerPayLoad: String?, promise: Promise ) { - PurchasingService.notifyFulfillment(token, FulfillmentResult.FULFILLED) + purchasingService.notifyFulfillment(token, FulfillmentResult.FULFILLED) promise.resolve(true) } @@ -148,13 +151,13 @@ class RNIapAmazonModule(reactContext: ReactApplicationContext) : developerPayLoad: String?, promise: Promise ) { - PurchasingService.notifyFulfillment(token, FulfillmentResult.FULFILLED) + purchasingService.notifyFulfillment(token, FulfillmentResult.FULFILLED) promise.resolve(true) } private fun sendUnconsumedPurchases(promise: Promise) { PromiseUtils.addPromiseForKey(PROMISE_QUERY_PURCHASES, promise) - PurchasingService.getPurchaseUpdates(false) + purchasingService.getPurchaseUpdates(false) } @ReactMethod @@ -189,8 +192,8 @@ class RNIapAmazonModule(reactContext: ReactApplicationContext) : */ override fun onHostResume() { if (hasListener) { - PurchasingService.getUserData() - PurchasingService.getPurchaseUpdates(false) + purchasingService.getUserData() + purchasingService.getPurchaseUpdates(false) } } override fun onHostPause() {} diff --git a/android/src/testAmazon/java/com/dooboolab/RNIap/RNIapAmazonModuleTest.kt b/android/src/testAmazon/java/com/dooboolab/RNIap/RNIapAmazonModuleTest.kt new file mode 100644 index 000000000..dd656d560 --- /dev/null +++ b/android/src/testAmazon/java/com/dooboolab/RNIap/RNIapAmazonModuleTest.kt @@ -0,0 +1,151 @@ +package com.dooboolab.RNIap + +import android.os.Handler +import com.amazon.device.iap.model.PurchaseResponse +import com.amazon.device.iap.model.Receipt +import com.amazon.device.iap.model.RequestId +import com.amazon.device.iap.model.UserData +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableMap +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.util.* + +class RNIapAmazonModuleTest { + + @MockK + lateinit var context: ReactApplicationContext + + @RelaxedMockK + lateinit var purchasingServiceProxy: PurchasingServiceProxy + + @MockK + lateinit var mainThreadHandler: Handler + + private lateinit var listener: RNIapAmazonListener + + private lateinit var module: RNIapAmazonModule + + @Before + fun setUp() { + MockKAnnotations.init(this, relaxUnitFun = true) + listener = spyk(RNIapAmazonListener(context, purchasingServiceProxy)) + module = RNIapAmazonModule(context, purchasingServiceProxy, mainThreadHandler, listener) + } + + @Test + fun `initConnection should resolve to true`() { + every { context.applicationContext } returns mockk() + + val promise = mockk(relaxed = true) + val slot = slot() + every { mainThreadHandler.postDelayed(capture(slot), any()) } answers { slot.captured.run(); true } + module.initConnection(promise) + verify(exactly = 0) { promise.reject(any(), any()) } + verify { promise.resolve(true) } + verify { purchasingServiceProxy.registerListener(any(), any()) } + } + + @Test + fun `Purchase Item`() { + val purchaseResponse = mockk() { + every { requestId } returns RequestId.fromString("0") + + every { requestStatus } returns PurchaseResponse.RequestStatus.SUCCESSFUL + val mReceipt = mockk(relaxed = true) { + every { sku } returns "mySku" + every { purchaseDate } returns Date() + every { receiptId } returns "rId" + } + every { receipt } returns mReceipt + val mUserData = mockk(relaxed = true) { + every { userId } returns "uid1" + } + every { userData } returns mUserData + } + + every { listener.sendEvent(any(), any(), any()) } just Runs + + every { purchasingServiceProxy.purchase(any()) } answers { + listener.onPurchaseResponse( + purchaseResponse + ); RequestId.fromString("0") + } + + val itemsMap = mockk(relaxed = true) { + every { getString("productId") } returns "mySku" + } + mockkStatic(Arguments::class) + + every { Arguments.createMap() } returns itemsMap + + val promise = mockk(relaxed = true) + + module.buyItemByType("mySku", promise) + verify(exactly = 0) { promise.reject(any(), any()) } + val response = slot() + verify { promise.resolve(capture(response)) } + assertEquals("mySku", response.captured.getString("productId")) + verify { listener.sendEvent(any(), "purchase-updated", any()) } + verify(exactly = 0) { purchasingServiceProxy.getPurchaseUpdates(false) } + } + +// @Test +// fun `initConnection Play Services not available on device should reject`() { +// } +// +// @Test +// fun `initConnection start new connection fails`() { +// } +// +// @Test +// fun `endConnection resolves`() { +// } +// +// @Test +// fun `flushFailedPurchasesCachedAsPending resolves to false if no pending purchases`() { +// } +// +// @Test +// fun `flushFailedPurchasesCachedAsPending resolves to true if pending purchases`() { +// } +// +// @Test +// fun getItemsByType() { +// } +// +// @Test +// fun getAvailableItemsByType() { +// } +// +// @Test +// fun getPurchaseHistoryByType() { +// } +// +// +// @Test +// fun acknowledgePurchase() { +// } +// +// @Test +// fun consumeProduct() { +// } +// +// @Test +// fun onPurchasesUpdated() { +// } +}