From 15b2c6b3d604b8f46fd90d2bce26047e0f5ac094 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 3 May 2019 14:37:30 +0200 Subject: [PATCH] first draft of IAP implementation --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 6 + .../android/PokerAnalyticsApplication.kt | 5 +- .../android/ui/activity/BillingActivity.kt | 36 ++++++ .../ui/fragment/SubscriptionFragment.kt | 68 +++++++++++ .../android/util/billing/AppGuard.kt | 110 ++++++++++++++++++ app/src/main/res/layout/activity_billing.xml | 15 +++ .../main/res/layout/fragment_subscription.xml | 19 +++ app/src/main/res/values/strings.xml | 1 + 9 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt create mode 100644 app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt create mode 100644 app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt create mode 100644 app/src/main/res/layout/activity_billing.xml create mode 100644 app/src/main/res/layout/fragment_subscription.xml diff --git a/app/build.gradle b/app/build.gradle index c54b385d..8ee8c89d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,6 +87,9 @@ dependencies { // Places implementation 'com.google.android.libraries.places:places:1.1.0' + // Billing / Subscriptions + implementation 'com.android.billingclient:billing:1.2.2' + // Firebase implementation 'com.google.firebase:firebase-core:16.0.8' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c2989ed..123dffcf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + + + diff --git a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt index 33b16f46..5ca7c7f0 100644 --- a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt +++ b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt @@ -26,6 +26,9 @@ class PokerAnalyticsApplication : Application() { super.onCreate() UserDefaults.init(this) + // AppGuard / Billing services +// AppGuard.load(this.applicationContext) + // Realm Realm.init(this) val realmConfiguration = RealmConfiguration.Builder() @@ -52,7 +55,7 @@ class PokerAnalyticsApplication : Application() { if (BuildConfig.DEBUG) { Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") - this.createFakeSessions() +// this.createFakeSessions() } Patcher.patchBreaks() diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt new file mode 100644 index 00000000..9a313130 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt @@ -0,0 +1,36 @@ +package net.pokeranalytics.android.ui.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity +import net.pokeranalytics.android.util.billing.AppGuard + +enum class IAPProducts(var identifier: String) { + PRO("pro") +} + +class BillingActivity : PokerAnalyticsActivity() { + + companion object { + fun newInstance(context: Context) { + val intent = Intent(context, BillingActivity::class.java) + context.startActivity(intent) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_billing) + } + + override fun onResume() { + super.onResume() + + AppGuard.restorePurchases() + } + +} + + diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt new file mode 100644 index 00000000..b7a8cdcc --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt @@ -0,0 +1,68 @@ +package net.pokeranalytics.android.ui.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.SkuDetails +import com.android.billingclient.api.SkuDetailsResponseListener +import kotlinx.android.synthetic.main.fragment_subscription.* +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.IAPProducts +import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment +import net.pokeranalytics.android.util.billing.AppGuard + +class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListener { + + var selectedProduct: SkuDetails? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (!AppGuard.requestProducts(this)) { + Toast.makeText(requireContext(), R.string.billingclient_unavailable, Toast.LENGTH_LONG).show() + } + + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_subscription, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initUI() + + } + + // SkuDetailsResponseListener + override fun onSkuDetailsResponse(responseCode: Int, skuDetailsList: MutableList?) { + + if (responseCode == BillingClient.BillingResponse.OK && skuDetailsList != null) { + selectedProduct = skuDetailsList.first { it.sku == IAPProducts.PRO.identifier } + updatePurchaseButtonState() + } + + } + + private fun initUI() { + + this.purchase.setOnClickListener { + + this.selectedProduct?.let { + AppGuard.initiatePurchase(this.requireActivity(), it) + } ?: run { + throw IllegalStateException("Attempt to initiate purchase while no product has been chosen") + } + } + + } + + private fun updatePurchaseButtonState() { + this.purchase.isEnabled = (this.selectedProduct != null) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt new file mode 100644 index 00000000..4d1e16d4 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt @@ -0,0 +1,110 @@ +package net.pokeranalytics.android.util.billing + +import android.app.Activity +import android.content.Context +import com.android.billingclient.api.* +import net.pokeranalytics.android.ui.activity.IAPProducts + +object AppGuard : PurchasesUpdatedListener { + + lateinit private var billingClient: BillingClient + + private var billingClientAvailable: Boolean = false + + fun load(context: Context) { + + billingClient = BillingClient.newBuilder(context).setListener(this).build() + + this.restorePurchases() + + billingClient.startConnection(object : BillingClientStateListener { + override fun onBillingSetupFinished(@BillingClient.BillingResponse billingResponseCode: Int) { + if (billingResponseCode == BillingClient.BillingResponse.OK) { + // The BillingClient is ready. You can query purchases here. + + billingClientAvailable = true + + } + } + override fun onBillingServiceDisconnected() { + billingClientAvailable = false + + // Try to restart the connection on the next request to + // Google Play by calling the startConnection() method. + } + }) + + } + + fun restorePurchases() { + // Automatically checks for purchases (when switching devices for example) + val purchasesResult= + billingClient.queryPurchases(BillingClient.SkuType.SUBS) + purchasesResult.purchasesList.forEach { + this.handlePurchase(it) + } + } + + fun requestProducts(listener: SkuDetailsResponseListener) : Boolean { + + if (this.billingClientAvailable) { + + val skuList = ArrayList() + skuList.add(IAPProducts.PRO.identifier) + val params = SkuDetailsParams.newBuilder() + params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS) + + billingClient.querySkuDetailsAsync(params.build(), listener) + return true + } + return false + } + + fun initiatePurchase(activity: Activity, skuDetails: SkuDetails) { + + val flowParams = BillingFlowParams.newBuilder() + .setSkuDetails(skuDetails) + .build() + val responseCode = billingClient.launchBillingFlow(activity, flowParams) + + } + + // PurchasesUpdatedListener + + override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList?) { + + if (responseCode == BillingClient.BillingResponse.OK && purchases != null) { + for (purchase in purchases) { + handlePurchase(purchase) + } + } else if (responseCode == BillingClient.BillingResponse.USER_CANCELED) { + // Handle an error caused by a user cancelling the purchase flow. + } else { + // Handle any other error codes. + } + + } + + private fun handlePurchase(purchase: Purchase) { + + val token = purchase.purchaseToken + val oj = purchase.originalJson + + if (this.verifyPurchase(purchase)) { + when (purchase.sku) { + IAPProducts.PRO.identifier -> { + + } + else -> {} + } + } else { + // invalid purchase + } + + } + + private fun verifyPurchase(purchase: Purchase) : Boolean { + return true + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_billing.xml b/app/src/main/res/layout/activity_billing.xml new file mode 100644 index 00000000..08d1f651 --- /dev/null +++ b/app/src/main/res/layout/activity_billing.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_subscription.xml b/app/src/main/res/layout/fragment_subscription.xml new file mode 100644 index 00000000..90b15285 --- /dev/null +++ b/app/src/main/res/layout/fragment_subscription.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 73666f07..26f159bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Initial Value Can\'t show because there is less than two values to display! The object you\'re trying to access is invalid + The billing service is unavailable at the moment. Please check your internet connection and retry later. Address Naming suggestions