From 5cc7efb4ad022e5e7da07839fa97468619e6d1ad Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 7 May 2019 17:02:53 +0200 Subject: [PATCH] Subscription update --- .../android/PokerAnalyticsApplication.kt | 3 +- .../android/ui/activity/BillingActivity.kt | 4 -- .../android/ui/fragment/FeedFragment.kt | 8 ++- .../android/ui/fragment/SettingsFragment.kt | 2 + .../ui/fragment/SubscriptionFragment.kt | 13 +++- .../ui/view/rowrepresentable/SettingRow.kt | 10 +-- .../android/util/billing/AppGuard.kt | 63 ++++++++++++++++--- 7 files changed, 81 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt index 2ed38ca4..77b51db0 100644 --- a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt +++ b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt @@ -16,6 +16,7 @@ import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.util.FakeDataManager import net.pokeranalytics.android.util.PokerAnalyticsLogs import net.pokeranalytics.android.util.UserDefaults +import net.pokeranalytics.android.util.billing.AppGuard import timber.log.Timber @@ -27,7 +28,7 @@ class PokerAnalyticsApplication : Application() { UserDefaults.init(this) // AppGuard / Billing services -// AppGuard.load(this.applicationContext) + AppGuard.load(this.applicationContext) // Realm Realm.init(this) 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 index 9a313130..359d490b 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt @@ -7,10 +7,6 @@ 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 { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FeedFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/FeedFragment.kt index 978c5cf5..5d167770 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/FeedFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/FeedFragment.kt @@ -182,6 +182,12 @@ class FeedFragment : RealmFragment(), RowRepresentableDelegate { */ private fun createNewSession(isTournament: Boolean) { +// if (!AppGuard.isProUser) { // && !BuildConfig.DEBUG +// Toast.makeText(context, "Please subscribe!", Toast.LENGTH_LONG).show() +// BillingActivity.newInstance(requireContext()) +// return +// } + if (Date().after(betaLimitDate)) { this.showEndOfBetaMessage() return @@ -219,7 +225,7 @@ class FeedFragment : RealmFragment(), RowRepresentableDelegate { * Show end of beta message */ private fun showEndOfBetaMessage() { - Toast.makeText(context, "Beta has ended. Please update with the Google Play version", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Beta has ended. Thanks a lot for your participation! Please update with the Google Play version to continue using the app", Toast.LENGTH_LONG).show() } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt index b396874f..f212b794 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt @@ -28,6 +28,7 @@ import net.pokeranalytics.android.ui.view.rowrepresentable.SettingRow import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.URL import net.pokeranalytics.android.util.UserDefaults +import net.pokeranalytics.android.util.billing.AppGuard import java.util.* @@ -90,6 +91,7 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta override fun stringForRow(row: RowRepresentable): String { return when (row) { + SettingRow.SUBSCRIPTION -> AppGuard.subscriptionStatus(requireContext()) SettingRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else "" SettingRow.CURRENCY -> UserDefaults.currency.symbol else -> "" 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 index b7a8cdcc..e0f14895 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt @@ -10,11 +10,12 @@ 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 +import net.pokeranalytics.android.util.billing.IAPProducts +import net.pokeranalytics.android.util.billing.PurchaseDelegate -class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListener { +class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListener, PurchaseDelegate { var selectedProduct: SkuDetails? = null @@ -53,7 +54,7 @@ class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListene this.purchase.setOnClickListener { this.selectedProduct?.let { - AppGuard.initiatePurchase(this.requireActivity(), it) + AppGuard.initiatePurchase(this.requireActivity(), it, this) } ?: run { throw IllegalStateException("Attempt to initiate purchase while no product has been chosen") } @@ -65,4 +66,10 @@ class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListene this.purchase.isEnabled = (this.selectedProduct != null) } + // PurchaseDelegate + + override fun purchaseDidSucceed() { + this.activity?.finish() + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SettingRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SettingRow.kt index 018896b8..70b497c3 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SettingRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SettingRow.kt @@ -9,6 +9,7 @@ import net.pokeranalytics.android.ui.view.RowViewType enum class SettingRow : RowRepresentable { // About + SUBSCRIPTION, VERSION, RATE_APP, CONTACT_US, @@ -44,7 +45,7 @@ enum class SettingRow : RowRepresentable { val rows = ArrayList() rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.information)) - rows.addAll(arrayListOf(VERSION, RATE_APP, CONTACT_US, BUG_REPORT)) + rows.addAll(arrayListOf(SUBSCRIPTION, VERSION, RATE_APP, CONTACT_US, BUG_REPORT)) rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.follow_us)) rows.addAll(arrayListOf(FOLLOW_US)) @@ -58,8 +59,8 @@ enum class SettingRow : RowRepresentable { resId = R.string.data_management ) ) - rows.addAll(arrayListOf(BANKROLL, GAME, LOCATION, TOURNAMENT_NAME, TOURNAMENT_FEATURE)) -//, TRANSACTION, TRANSACTION_TYPE //TODO add them back + rows.addAll(arrayListOf(BANKROLL, GAME, LOCATION, TOURNAMENT_NAME, TOURNAMENT_FEATURE, TRANSACTION_TYPE)) +//, TRANSACTION, //TODO add them back rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.terms)) rows.addAll(arrayListOf(PRIVACY_POLICY, TERMS_OF_USE, GDPR)) @@ -75,6 +76,7 @@ enum class SettingRow : RowRepresentable { return it.resId } ?: run { return when (this) { + SUBSCRIPTION -> R.string.subscription VERSION -> R.string.version RATE_APP -> R.string.releasenote_rating CONTACT_US -> R.string.contact @@ -94,7 +96,7 @@ enum class SettingRow : RowRepresentable { override val viewType: Int get() { return when (this) { - VERSION -> RowViewType.TITLE_VALUE.ordinal + VERSION, SUBSCRIPTION -> RowViewType.TITLE_VALUE.ordinal LANGUAGE, CURRENCY -> RowViewType.TITLE_VALUE_ARROW.ordinal FOLLOW_US -> RowViewType.ROW_FOLLOW_US.ordinal else -> RowViewType.TITLE_ARROW.ordinal 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 index 4180119a..71dbc5b3 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt @@ -3,10 +3,19 @@ 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 +import net.pokeranalytics.android.R import timber.log.Timber import java.io.IOException +import java.util.* +import kotlin.collections.ArrayList +enum class IAPProducts(var identifier: String) { + PRO("unlimited") +} + +interface PurchaseDelegate { + fun purchaseDidSucceed() +} /** * the AppGuard object is in charge of contacting the Billing services to retrieve products, @@ -14,12 +23,13 @@ import java.io.IOException */ object AppGuard : PurchasesUpdatedListener { - private val BASE_64_ENCODED_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0VuL5bk2w0FNaZIwE3v2857ZxJlo0epIIsVJfVs7Pqh7zE1JN7uGBgZ/r2s5Rn3o0R1Ycqxp832kYg/B0FvlQJ3Ko6IZkoyfJQ3i1zuAcc7NLMxKUMJY8Mc0U6Go2bevjQ54WkvumIdAFWIlMjyuOOFcSZRZr8V7tlq0SYlenkuellQeHIq3V47M/0jlDrEbCFj59hsukN75eGIiafFAxBYO/8L/flkZLik8YyhV1uZTu+KziA0PsbIvXKyN+gCK9UmrscTyM4+hfmRgb74fro67UsEqq2OvmHFUhubPzCZDElOwPeauUDEGeQjJn43iUHZWIcSEVktVB9cFa/0JwIDAQAB" + private const val BASE_64_ENCODED_PUBLIC_KEY = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0VuL5bk2w0FNaZIwE3v2857ZxJlo0epIIsVJfVs7Pqh7zE1JN7uGBgZ/r2s5Rn3o0R1Ycqxp832kYg/B0FvlQJ3Ko6IZkoyfJQ3i1zuAcc7NLMxKUMJY8Mc0U6Go2bevjQ54WkvumIdAFWIlMjyuOOFcSZRZr8V7tlq0SYlenkuellQeHIq3V47M/0jlDrEbCFj59hsukN75eGIiafFAxBYO/8L/flkZLik8YyhV1uZTu+KziA0PsbIvXKyN+gCK9UmrscTyM4+hfmRgb74fro67UsEqq2OvmHFUhubPzCZDElOwPeauUDEGeQjJn43iUHZWIcSEVktVB9cFa/0JwIDAQAB" /** * The Billing Client making requests with Google Billing services */ - lateinit private var billingClient: BillingClient + private lateinit var billingClient: BillingClient /** * Whether the billing client is available @@ -32,6 +42,11 @@ object AppGuard : PurchasesUpdatedListener { var isProUser: Boolean = false private set + /** + * A delegate to notify when the purchase has succeeded + */ + private var purchaseDelegate: PurchaseDelegate? = null + /** * Initialization of AppGuard * Connects to billing services and restores purchases @@ -77,6 +92,7 @@ object AppGuard : PurchasesUpdatedListener { * Restore purchases */ fun restorePurchases() { + this.resetPurchases() // Automatically checks for purchases (when switching devices for example) val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS) @@ -85,6 +101,14 @@ object AppGuard : PurchasesUpdatedListener { } } + /** + * Reset all purchases + * This is done before restoring in order to ensure that subscriptions stops + */ + private fun resetPurchases() { + this.isProUser = false + } + /** * Requests the product descriptions */ @@ -109,7 +133,9 @@ object AppGuard : PurchasesUpdatedListener { /** * Initiates purchase with the product [skuDetails] */ - fun initiatePurchase(activity: Activity, skuDetails: SkuDetails) { + fun initiatePurchase(activity: Activity, skuDetails: SkuDetails, delegate: PurchaseDelegate) { + + this.purchaseDelegate = delegate val flowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) @@ -117,6 +143,7 @@ object AppGuard : PurchasesUpdatedListener { this.executeServiceRequest(Runnable { val responseCode = billingClient.launchBillingFlow(activity, flowParams) + Timber.d("launchBillingFlow returned $responseCode") }) } @@ -141,7 +168,7 @@ object AppGuard : PurchasesUpdatedListener { } /** - * Method called when a purchase has been made + * Method called when a [purchase] has been made */ private fun handlePurchase(purchase: Purchase) { @@ -150,7 +177,17 @@ object AppGuard : PurchasesUpdatedListener { if (this.verifyValidSignature(purchase.originalJson, purchase.signature)) { when (purchase.sku) { IAPProducts.PRO.identifier -> { + + + val date = Date(purchase.purchaseTime) + Timber.d("*** Auto renewing = ${purchase.isAutoRenewing}") + Timber.d("*** purchaseTime = ${date}") + this.isProUser = true + this.purchaseDelegate?.let { + it.purchaseDidSucceed() + this.purchaseDelegate = null + } } else -> { } @@ -162,15 +199,23 @@ object AppGuard : PurchasesUpdatedListener { } /** - * Verifies the validity of a purchase + * Verifies the validity of a purchase with its [signedData] and [signature] */ private fun verifyValidSignature(signedData: String, signature: String): Boolean { - try { - return Security.verifyPurchase(BASE_64_ENCODED_PUBLIC_KEY, signedData, signature) + return try { + Security.verifyPurchase(BASE_64_ENCODED_PUBLIC_KEY, signedData, signature) } catch (e: IOException) { Timber.d("Got an exception trying to validate a purchase: $e") - return false + false } } + /** + * Returns the subscription status of the user, using the [context] + */ + fun subscriptionStatus(context: Context): String { + val resId = if (this.isProUser) R.string.pro_sub_short_title else R.string.none + return context.getString(resId) + } + } \ No newline at end of file