diff --git a/library/src/main/java/com/billing/BillingService.java b/library/src/main/java/com/billing/BillingService.java deleted file mode 100644 index ac50395..0000000 --- a/library/src/main/java/com/billing/BillingService.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.billing; - -import android.app.Activity; - -import androidx.annotation.CallSuper; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public abstract class BillingService { - - private List purchaseServiceListeners; - private List subscriptionServiceListeners; - - @SuppressWarnings("WeakerAccess") - public BillingService() { - purchaseServiceListeners = new ArrayList<>(); - subscriptionServiceListeners = new ArrayList<>(); - } - - public void addPurchaseListener(PurchaseServiceListener purchaseServiceListener) { - purchaseServiceListeners.add(purchaseServiceListener); - } - - public void removePurchaseListener(PurchaseServiceListener purchaseServiceListener) { - purchaseServiceListeners.remove(purchaseServiceListener); - } - - public void addSubscriptionListener(SubscriptionServiceListener subscriptionServiceListener) { - subscriptionServiceListeners.add(subscriptionServiceListener); - } - - public void removeSubscriptionListener(SubscriptionServiceListener subscriptionServiceListener) { - subscriptionServiceListeners.remove(subscriptionServiceListener); - } - - /** - * @param sku - product specificator - * @param isRestore - a flag indicating whether it's a fresh purchase or restored product - */ - public void productOwned(String sku, boolean isRestore) { - for (PurchaseServiceListener purchaseServiceListener : purchaseServiceListeners) { - if (isRestore) { - purchaseServiceListener.onProductRestored(sku); - } else { - purchaseServiceListener.onProductPurchased(sku); - } - } - } - - /** - * @param sku - subscription specificator - * @param isRestore - a flag indicating whether it's a fresh purchase or restored subscription - */ - public void subscriptionOwned(String sku, boolean isRestore) { - for (SubscriptionServiceListener subscriptionServiceListener : subscriptionServiceListeners) { - if (isRestore) { - subscriptionServiceListener.onSubscriptionRestored(sku); - } else { - subscriptionServiceListener.onSubscriptionPurchased(sku); - } - } - } - - public void updatePrices(Map iapkeyPrices) { - for (BillingServiceListener billingServiceListener : purchaseServiceListeners) { - billingServiceListener.onPricesUpdated(iapkeyPrices); - } - - for (BillingServiceListener billingServiceListener : subscriptionServiceListeners) { - billingServiceListener.onPricesUpdated(iapkeyPrices); - } - } - - public abstract void init(String key); - - public abstract void buy(Activity activity, String sku, int id); - - public abstract void subscribe(Activity activity, String sku, int id); - - public abstract void unsubscribe(Activity activity, String sku, int id); - - public abstract void enableDebugLogging(boolean enable); - - @CallSuper - public void close() { - subscriptionServiceListeners.clear(); - purchaseServiceListeners.clear(); - } -} diff --git a/library/src/main/java/com/billing/BillingService.kt b/library/src/main/java/com/billing/BillingService.kt new file mode 100644 index 0000000..fd49e73 --- /dev/null +++ b/library/src/main/java/com/billing/BillingService.kt @@ -0,0 +1,99 @@ +package com.billing + +import android.app.Activity +import android.os.Handler +import android.os.Looper +import androidx.annotation.CallSuper + +abstract class BillingService { + + private val purchaseServiceListeners: MutableList = mutableListOf() + private val subscriptionServiceListeners: MutableList = mutableListOf() + + fun addPurchaseListener(purchaseServiceListener: PurchaseServiceListener) { + purchaseServiceListeners.add(purchaseServiceListener) + } + + fun removePurchaseListener(purchaseServiceListener: PurchaseServiceListener) { + purchaseServiceListeners.remove(purchaseServiceListener) + } + + fun addSubscriptionListener(subscriptionServiceListener: SubscriptionServiceListener) { + subscriptionServiceListeners.add(subscriptionServiceListener) + } + + fun removeSubscriptionListener(subscriptionServiceListener: SubscriptionServiceListener) { + subscriptionServiceListeners.remove(subscriptionServiceListener) + } + + /** + * @param sku - product specificator + * @param isRestore - a flag indicating whether it's a fresh purchase or restored product + */ + fun productOwned(sku: String?, isRestore: Boolean) { + findUiHandler().post { + productOwnedInternal(sku, isRestore) + } + } + + fun productOwnedInternal(sku: String?, isRestore: Boolean) { + for (purchaseServiceListener in purchaseServiceListeners) { + if (isRestore) { + purchaseServiceListener.onProductRestored(sku) + } else { + purchaseServiceListener.onProductPurchased(sku) + } + } + } + + /** + * @param sku - subscription specificator + * @param isRestore - a flag indicating whether it's a fresh purchase or restored subscription + */ + fun subscriptionOwned(sku: String, isRestore: Boolean) { + findUiHandler().post { + subscriptionOwnedInternal(sku, isRestore) + } + } + + fun subscriptionOwnedInternal(sku: String, isRestore: Boolean) { + for (subscriptionServiceListener in subscriptionServiceListeners) { + if (isRestore) { + subscriptionServiceListener.onSubscriptionRestored(sku) + } else { + subscriptionServiceListener.onSubscriptionPurchased(sku) + } + } + } + + fun updatePrices(iapkeyPrices: Map) { + findUiHandler().post { + updatePricesInternal(iapkeyPrices) + } + } + + fun updatePricesInternal(iapkeyPrices: Map) { + for (billingServiceListener in purchaseServiceListeners) { + billingServiceListener.onPricesUpdated(iapkeyPrices) + } + for (billingServiceListener in subscriptionServiceListeners) { + billingServiceListener.onPricesUpdated(iapkeyPrices) + } + } + + abstract fun init(key: String?) + abstract fun buy(activity: Activity, sku: String) + abstract fun subscribe(activity: Activity, sku: String) + abstract fun unsubscribe(activity: Activity, sku: String) + abstract fun enableDebugLogging(enable: Boolean) + + @CallSuper + open fun close() { + subscriptionServiceListeners.clear() + purchaseServiceListeners.clear() + } +} + +fun findUiHandler(): Handler { + return Handler(Looper.getMainLooper()) +} \ No newline at end of file diff --git a/library/src/main/java/com/billing/amazon/AmazonBillingService.kt b/library/src/main/java/com/billing/amazon/AmazonBillingService.kt index 5f156b7..87addec 100644 --- a/library/src/main/java/com/billing/amazon/AmazonBillingService.kt +++ b/library/src/main/java/com/billing/amazon/AmazonBillingService.kt @@ -11,7 +11,7 @@ class AmazonBillingService(val context: Context, private val iapKeys: List() - override fun init(key: String) { + override fun init(key: String?) { decodedKey = key mBillingClient = BillingClient.newBuilder(context).setListener(this).enablePendingPurchases().build() @@ -51,7 +51,7 @@ class GoogleBillingService2(val context: Context, private val inAppSkuKeys: List } } - override fun buy(activity: Activity, sku: String, id: Int) { + override fun buy(activity: Activity, sku: String) { if (!sku.isSkuReady()) { log("buy. Google billing service is not ready yet.") return @@ -60,7 +60,7 @@ class GoogleBillingService2(val context: Context, private val inAppSkuKeys: List launchBillingFlow(activity, sku, BillingClient.SkuType.INAPP) } - override fun subscribe(activity: Activity, sku: String, id: Int) { + override fun subscribe(activity: Activity, sku: String) { if (!sku.isSkuReady()) { log("buy. Google billing service is not ready yet.") return @@ -77,7 +77,7 @@ class GoogleBillingService2(val context: Context, private val inAppSkuKeys: List } } - override fun unsubscribe(activity: Activity, sku: String, id: Int) { + override fun unsubscribe(activity: Activity, sku: String) { try { val intent = Intent() intent.action = Intent.ACTION_VIEW @@ -165,7 +165,8 @@ class GoogleBillingService2(val context: Context, private val inAppSkuKeys: List } private fun isSignatureValid(purchase: Purchase): Boolean { - return Security.verifyPurchase(decodedKey, purchase.originalJson, purchase.signature) + val key = decodedKey ?: return true + return Security.verifyPurchase(key, purchase.originalJson, purchase.signature) } /** diff --git a/library/src/main/java/com/eggheadgames/inapppayments/IAPManager.java b/library/src/main/java/com/eggheadgames/inapppayments/IAPManager.java deleted file mode 100644 index 5833b71..0000000 --- a/library/src/main/java/com/eggheadgames/inapppayments/IAPManager.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.eggheadgames.inapppayments; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; - -import com.billing.BillingService; -import com.billing.PurchaseServiceListener; -import com.billing.SubscriptionServiceListener; -import com.billing.amazon.AmazonBillingService; -import com.billing.google.GoogleBillingService2; - -import java.util.ArrayList; -import java.util.List; - -//Public front-end for IAP functionality. - -public class IAPManager { - - public static final int BUILD_TARGET_GOOGLE = 0; - public static final int BUILD_TARGET_AMAZON = 1; - - @SuppressLint("StaticFieldLeak") - private static BillingService billingService; - - /** - * @param context - application context - * @param buildTarget - IAPManager.BUILD_TARGET_GOOGLE or IAPManager.BUILD_TARGET_AMAZON - * @param iapKeys - list of sku for purchases - * @param subscriptionKeys - list of sku for subscriptions - */ - public static void build(Context context, int buildTarget, List iapKeys, List subscriptionKeys) { - Context applicationContext = context.getApplicationContext(); - Context contextLocal = applicationContext == null ? context : applicationContext; - - //Build-specific initializations - if (buildTarget == BUILD_TARGET_GOOGLE) { - billingService = new GoogleBillingService2(contextLocal, iapKeys, subscriptionKeys); - } else if (buildTarget == BUILD_TARGET_AMAZON) { - List keys = new ArrayList<>(); - keys.addAll(iapKeys); - keys.addAll(subscriptionKeys); - billingService = new AmazonBillingService(contextLocal, keys); - } - } - - public static void init(String key, boolean enableLogging) { - billingService.init(key); - billingService.enableDebugLogging(enableLogging); - } - - public static void addPurchaseListener(PurchaseServiceListener purchaseServiceListener) { - billingService.addPurchaseListener(purchaseServiceListener); - } - - public static void removePurchaseListener(PurchaseServiceListener purchaseServiceListener) { - billingService.removePurchaseListener(purchaseServiceListener); - } - - public static void addSubscriptionListener(SubscriptionServiceListener subscriptionServiceListener) { - billingService.addSubscriptionListener(subscriptionServiceListener); - } - - public static void removeSubscriptionListener(SubscriptionServiceListener subscriptionServiceListener) { - billingService.removeSubscriptionListener(subscriptionServiceListener); - } - - public static void buy(Activity activity, String sku, int id) { - billingService.buy(activity, sku, id); - } - - public static void subscribe(Activity activity, String sku, int id) { - billingService.subscribe(activity, sku, id); - } - - public static void unsubscribe(Activity activity, String sku, int id) { - billingService.unsubscribe(activity, sku, id); - } - - public static void destroy() { - billingService.close(); - } - - public static BillingService getBillingService() { - return billingService; - } -} diff --git a/library/src/main/java/com/eggheadgames/inapppayments/IAPManager.kt b/library/src/main/java/com/eggheadgames/inapppayments/IAPManager.kt new file mode 100644 index 0000000..2fd741c --- /dev/null +++ b/library/src/main/java/com/eggheadgames/inapppayments/IAPManager.kt @@ -0,0 +1,98 @@ +package com.eggheadgames.inapppayments + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import com.billing.BillingService +import com.billing.PurchaseServiceListener +import com.billing.SubscriptionServiceListener +import com.billing.amazon.AmazonBillingService +import com.billing.google.GoogleBillingService2 +import java.util.* + +//Public front-end for IAP functionality. +object IAPManager { + const val BUILD_TARGET_GOOGLE = 0 + const val BUILD_TARGET_AMAZON = 1 + + @SuppressLint("StaticFieldLeak") + private var mBillingService: BillingService? = null + + /** + * @param context - application context + * @param buildTarget - IAPManager.BUILD_TARGET_GOOGLE or IAPManager.BUILD_TARGET_AMAZON + * @param iapKeys - list of sku for purchases + * @param subscriptionKeys - list of sku for subscriptions + */ + @JvmStatic + fun build(context: Context, buildTarget: Int, iapKeys: List, subscriptionKeys: List = emptyList()) { + val contextLocal = context.applicationContext ?: context + + //Build-specific initializations + if (buildTarget == BUILD_TARGET_GOOGLE) { + mBillingService = GoogleBillingService2(contextLocal, iapKeys, subscriptionKeys) + + } else if (buildTarget == BUILD_TARGET_AMAZON) { + val keys: MutableList = ArrayList() + keys.addAll(iapKeys) + keys.addAll(subscriptionKeys) + mBillingService = AmazonBillingService(contextLocal, keys) + } + } + + /** + * Initialize billing service. + * + * @param key - key to verify purchase messages. Currently valid only for Google Billing. Leave empty if you want to skip verification + */ + fun init(key: String? = null, enableLogging: Boolean = false) { + getBillingService().init(key) + getBillingService().enableDebugLogging(enableLogging) + } + + @JvmStatic + fun addPurchaseListener(purchaseServiceListener: PurchaseServiceListener) { + getBillingService().addPurchaseListener(purchaseServiceListener) + } + + @JvmStatic + fun removePurchaseListener(purchaseServiceListener: PurchaseServiceListener) { + getBillingService().removePurchaseListener(purchaseServiceListener) + } + + @JvmStatic + fun addSubscriptionListener(subscriptionServiceListener: SubscriptionServiceListener) { + getBillingService().addSubscriptionListener(subscriptionServiceListener) + } + + @JvmStatic + fun removeSubscriptionListener(subscriptionServiceListener: SubscriptionServiceListener) { + getBillingService().removeSubscriptionListener(subscriptionServiceListener) + } + + @JvmStatic + fun buy(activity: Activity, sku: String) { + getBillingService().buy(activity, sku) + } + + @JvmStatic + fun subscribe(activity: Activity, sku: String) { + getBillingService().subscribe(activity, sku) + } + + fun unsubscribe(activity: Activity, sku: String) { + getBillingService().unsubscribe(activity, sku) + } + + @JvmStatic + fun destroy() { + getBillingService().close() + } + + @JvmStatic + fun getBillingService(): BillingService { + return mBillingService ?: let { + throw RuntimeException("Call IAPManager.build to initialize billing service") + } + } +} \ No newline at end of file diff --git a/library/src/test/java/com/eggheadgames/inapppayments/BillingTest.java b/library/src/test/java/com/eggheadgames/inapppayments/BillingTest.java index fa14237..1f802d4 100644 --- a/library/src/test/java/com/eggheadgames/inapppayments/BillingTest.java +++ b/library/src/test/java/com/eggheadgames/inapppayments/BillingTest.java @@ -31,7 +31,7 @@ public void onProductOwnedEvent_eachRegisteredListenerShouldBeTriggered() throws PurchaseServiceListener secondListener = Mockito.spy(PurchaseServiceListener.class); IAPManager.addPurchaseListener(secondListener); - IAPManager.getBillingService().productOwned(TestConstants.TEST_SKU, false); + IAPManager.getBillingService().productOwnedInternal(TestConstants.TEST_SKU, false); Mockito.verify(firstListener, Mockito.times(1)).onProductPurchased(TestConstants.TEST_SKU); Mockito.verify(secondListener, Mockito.times(1)).onProductPurchased(TestConstants.TEST_SKU); @@ -51,7 +51,7 @@ public void onProductDetailsFetched_eachRegisteredListenerShouldBeTriggered() { products.put(TestConstants.TEST_SKU, TestConstants.TEST_PRODUCT); products.put(TestConstants.TEST_SKU_1, TestConstants.TEST_PRODUCT); - IAPManager.getBillingService().updatePrices(products); + IAPManager.getBillingService().updatePricesInternal(products); Mockito.verify(firstListener, Mockito.times(1)).onPricesUpdated(products); Mockito.verify(secondListener, Mockito.times(1)).onPricesUpdated(products); @@ -69,7 +69,7 @@ public void onIapManagerInteraction_onlyRegisteredListenersShouldBeTriggered() { IAPManager.removePurchaseListener(firstListener); - IAPManager.getBillingService().productOwned(TestConstants.TEST_SKU, false); + IAPManager.getBillingService().productOwnedInternal(TestConstants.TEST_SKU, false); Mockito.verify(firstListener, Mockito.never()).onProductPurchased(Mockito.anyString()); Mockito.verify(secondListener, Mockito.times(1)).onProductPurchased(TestConstants.TEST_SKU); @@ -83,7 +83,7 @@ public void onProductPurchase_purchaseCallbackShouldBeTriggered() { PurchaseServiceListener listener = Mockito.spy(PurchaseServiceListener.class); IAPManager.addPurchaseListener(listener); - IAPManager.getBillingService().productOwned(TestConstants.TEST_SKU, false); + IAPManager.getBillingService().productOwnedInternal(TestConstants.TEST_SKU, false); Mockito.verify(listener, Mockito.times(1)).onProductPurchased(TestConstants.TEST_SKU); } @@ -95,7 +95,7 @@ public void onProductRestore_restoreCallbackShouldBeTriggered() { PurchaseServiceListener listener = Mockito.spy(PurchaseServiceListener.class); IAPManager.addPurchaseListener(listener); - IAPManager.getBillingService().productOwned(TestConstants.TEST_SKU, true); + IAPManager.getBillingService().productOwnedInternal(TestConstants.TEST_SKU, true); Mockito.verify(listener, Mockito.times(1)).onProductRestored(TestConstants.TEST_SKU); } @@ -107,7 +107,7 @@ public void onSubscriptionPurchase_purchaseCallbackShouldBeTriggered() { SubscriptionServiceListener listener = Mockito.spy(SubscriptionServiceListener.class); IAPManager.addSubscriptionListener(listener); - IAPManager.getBillingService().subscriptionOwned(TestConstants.TEST_SKU, false); + IAPManager.getBillingService().subscriptionOwnedInternal(TestConstants.TEST_SKU, false); Mockito.verify(listener, Mockito.times(1)).onSubscriptionPurchased(TestConstants.TEST_SKU); } @@ -119,7 +119,7 @@ public void onSubscriptionRestore_restoreCallbackShouldBeTriggered() { SubscriptionServiceListener listener = Mockito.spy(SubscriptionServiceListener.class); IAPManager.addSubscriptionListener(listener); - IAPManager.getBillingService().subscriptionOwned(TestConstants.TEST_SKU, true); + IAPManager.getBillingService().subscriptionOwnedInternal(TestConstants.TEST_SKU, true); Mockito.verify(listener, Mockito.times(1)).onSubscriptionRestored(TestConstants.TEST_SKU); }