Subscriptions update

dev
Laurent 7 years ago
parent d62d6332f7
commit 3958396ce6
  1. 1
      app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt
  2. 9
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  3. 3
      app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt
  4. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/FeedFragment.kt
  5. 69
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt
  6. 2
      app/src/main/java/net/pokeranalytics/android/util/Preferences.kt
  7. 21
      app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt
  8. 33
      app/src/main/res/layout/fragment_subscription.xml
  9. 4
      app/src/main/res/values/strings.xml
  10. 20
      app/src/main/res/values/styles.xml

@ -24,7 +24,6 @@ class BillingActivity : PokerAnalyticsActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
AppGuard.restorePurchases()
} }
} }

@ -7,18 +7,16 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.android.synthetic.main.bottom_sheet_sum.view.*
import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.Currency import net.pokeranalytics.android.model.realm.Currency
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.adapter.HomePagerAdapter import net.pokeranalytics.android.ui.adapter.HomePagerAdapter
import net.pokeranalytics.android.ui.interfaces.FilterHandler import net.pokeranalytics.android.ui.interfaces.FilterHandler
import timber.log.Timber import net.pokeranalytics.android.util.billing.AppGuard
class HomeActivity : PokerAnalyticsActivity() { class HomeActivity : PokerAnalyticsActivity() {
@ -68,6 +66,11 @@ class HomeActivity : PokerAnalyticsActivity() {
return@OnNavigationItemSelectedListener true return@OnNavigationItemSelectedListener true
} }
override fun onResume() {
super.onResume()
AppGuard.requestPurchasesUpdate()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

@ -20,6 +20,7 @@ import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.util.DeviceUtils import net.pokeranalytics.android.util.DeviceUtils
import net.pokeranalytics.android.util.URL import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.billing.AppGuard
import java.io.File import java.io.File
@ -70,7 +71,7 @@ fun PokerAnalyticsActivity.openPlayStorePage() {
// Open email for "Contact us" // Open email for "Contact us"
fun PokerAnalyticsActivity.openContactMail(subjectStringRes: Int, filePath: String?= null) { fun PokerAnalyticsActivity.openContactMail(subjectStringRes: Int, filePath: String?= null) {
val info = "v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}), Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}" val info = "v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) - ${AppGuard.isProUser}, Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}"
val emailIntent = Intent(Intent.ACTION_SEND) val emailIntent = Intent(Intent.ACTION_SEND)

@ -183,7 +183,7 @@ class FeedFragment : RealmFragment(), RowRepresentableDelegate {
private fun createNewSession(isTournament: Boolean) { private fun createNewSession(isTournament: Boolean) {
// if (!AppGuard.isProUser) { // && !BuildConfig.DEBUG // if (!AppGuard.isProUser) { // && !BuildConfig.DEBUG
// Toast.makeText(context, "Please subscribe!", Toast.LENGTH_LONG).show() //// Toast.makeText(context, "Please subscribe!", Toast.LENGTH_LONG).show()
// BillingActivity.newInstance(requireContext()) // BillingActivity.newInstance(requireContext())
// return // return
// } // }

@ -1,23 +1,31 @@
package net.pokeranalytics.android.ui.fragment package net.pokeranalytics.android.ui.fragment
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.TypefaceSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.content.res.ResourcesCompat
import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.SkuDetails import com.android.billingclient.api.SkuDetails
import com.android.billingclient.api.SkuDetailsResponseListener import com.android.billingclient.api.SkuDetailsResponseListener
import kotlinx.android.synthetic.main.fragment_subscription.* import kotlinx.android.synthetic.main.fragment_subscription.*
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseDelegate import net.pokeranalytics.android.util.billing.PurchaseDelegate
class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListener, PurchaseDelegate { class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListener, PurchaseDelegate {
var selectedProduct: SkuDetails? = null private var selectedProduct: SkuDetails? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -25,7 +33,6 @@ class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListene
if (!AppGuard.requestProducts(this)) { if (!AppGuard.requestProducts(this)) {
Toast.makeText(requireContext(), R.string.billingclient_unavailable, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.billingclient_unavailable, Toast.LENGTH_LONG).show()
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -36,21 +43,34 @@ class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListene
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initUI() initUI()
} }
// SkuDetailsResponseListener private fun initUI() {
override fun onSkuDetailsResponse(responseCode: Int, skuDetailsList: MutableList<SkuDetails>?) {
if (responseCode == BillingClient.BillingResponse.OK && skuDetailsList != null) { val upgradeString = requireContext().getString(R.string.pro_upgrade)
selectedProduct = skuDetailsList.first { it.sku == IAPProducts.PRO.identifier }
updatePurchaseButtonState()
}
} if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
private fun initUI() { val ssb = SpannableStringBuilder(upgradeString)
val indexOfLastSpace = upgradeString.lastIndexOf(" ")
val end = upgradeString.chars().count().toInt()
val lightTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_light)
val boldTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_bold)
if (lightTypeFace != null && boldTypeFace != null) {
ssb.setSpan(TypefaceSpan(lightTypeFace), 0, indexOfLastSpace, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
ssb.setSpan(TypefaceSpan(boldTypeFace), indexOfLastSpace, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
}
this.title.text = ssb
} else {
this.title.text = upgradeString
}
this.purchase.isEnabled = false
this.purchase.setOnClickListener { this.purchase.setOnClickListener {
this.selectedProduct?.let { this.selectedProduct?.let {
@ -59,16 +79,37 @@ class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListene
throw IllegalStateException("Attempt to initiate purchase while no product has been chosen") throw IllegalStateException("Attempt to initiate purchase while no product has been chosen")
} }
} }
}
// SkuDetailsResponseListener
override fun onSkuDetailsResponse(responseCode: Int, skuDetailsList: MutableList<SkuDetails>?) {
if (responseCode == BillingClient.BillingResponse.OK && skuDetailsList != null) {
selectedProduct = skuDetailsList.first { it.sku == IAPProducts.PRO.identifier }
updateUI()
}
} }
private fun updatePurchaseButtonState() { private fun updateUI() {
this.purchase.isEnabled = (this.selectedProduct != null)
this.selectedProduct?.let {
this.purchase.isEnabled = true
val perYearString = requireContext().getString(R.string.year_subscription)
val formattedPrice = it.price + " / " + perYearString
this.price.text = formattedPrice
}
} }
// PurchaseDelegate // PurchaseDelegate
override fun purchaseDidSucceed() { override fun purchaseDidSucceed(purchase: Purchase) {
// record purchase in preferences for troubleshooting / verification
val purchaseInfos = listOf(purchase.sku, purchase.orderId, purchase.purchaseToken)
Preferences.setString(Preferences.Keys.LATEST_PURCHASE, purchaseInfos.joinToString("/"), requireContext())
this.activity?.finish() this.activity?.finish()
} }

@ -12,7 +12,7 @@ class Preferences {
FIRST_LAUNCH("firstLaunch"), FIRST_LAUNCH("firstLaunch"),
STOP_SHOWING_DISCLAIMER("stopShowingDisclaimer"), STOP_SHOWING_DISCLAIMER("stopShowingDisclaimer"),
ACTIVE_FILTER_ID("ActiveFilterId"), ACTIVE_FILTER_ID("ActiveFilterId"),
LATEST_PURCHASE("latestPurchase")
} }
companion object { companion object {

@ -14,12 +14,14 @@ enum class IAPProducts(var identifier: String) {
} }
interface PurchaseDelegate { interface PurchaseDelegate {
fun purchaseDidSucceed() fun purchaseDidSucceed(purchase: Purchase)
} }
/** /**
* the AppGuard object is in charge of contacting the Billing services to retrieve products, * the AppGuard object is in charge of contacting the Billing services to retrieve products,
* initiating purchases and verifying transactions * initiating purchases and verifying transactions
* Requests performed with the BillingClient must be done while being connected.
* Use executeServiceRequest to ensure this
*/ */
object AppGuard : PurchasesUpdatedListener { object AppGuard : PurchasesUpdatedListener {
@ -56,7 +58,7 @@ object AppGuard : PurchasesUpdatedListener {
billingClient = BillingClient.newBuilder(context).setListener(this).build() billingClient = BillingClient.newBuilder(context).setListener(this).build()
this.startConnection(Runnable { this.startConnection(Runnable {
this.restorePurchases() this.updatePurchases()
}) })
} }
@ -88,10 +90,17 @@ object AppGuard : PurchasesUpdatedListener {
} }
} }
fun requestPurchasesUpdate() {
this.executeServiceRequest(Runnable {
this.updatePurchases()
})
}
/** /**
* Restore purchases * Update the state of subscriptions
* Restore or stop access to IAPs
*/ */
fun restorePurchases() { private fun updatePurchases() {
this.resetPurchases() this.resetPurchases()
// Automatically checks for purchases (when switching devices for example) // Automatically checks for purchases (when switching devices for example)
val purchasesResult = val purchasesResult =
@ -172,8 +181,6 @@ object AppGuard : PurchasesUpdatedListener {
*/ */
private fun handlePurchase(purchase: Purchase) { private fun handlePurchase(purchase: Purchase) {
// val token = purchase.purchaseToken
if (this.verifyValidSignature(purchase.originalJson, purchase.signature)) { if (this.verifyValidSignature(purchase.originalJson, purchase.signature)) {
when (purchase.sku) { when (purchase.sku) {
IAPProducts.PRO.identifier -> { IAPProducts.PRO.identifier -> {
@ -185,7 +192,7 @@ object AppGuard : PurchasesUpdatedListener {
this.isProUser = true this.isProUser = true
this.purchaseDelegate?.let { this.purchaseDelegate?.let {
it.purchaseDidSucceed() it.purchaseDidSucceed(purchase)
this.purchaseDelegate = null this.purchaseDelegate = null
} }
} }

@ -3,8 +3,34 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"> android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_marginTop="24dp"
android:text="@string/pro_upgrade"
style="@style/PokerAnalyticsTheme.TextView.SubscriptionTitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- -->
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/price"
style="@style/PokerAnalyticsTheme.TextView.SubscriptionPrice"
app:layout_constraintBottom_toTopOf="@id/purchase"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="$29.99 / year"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/purchase" android:id="@+id/purchase"
style="@style/PokerAnalyticsTheme.Button" style="@style/PokerAnalyticsTheme.Button"
@ -13,8 +39,9 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_margin="16dp" android:layout_marginLeft="16dp"
android:text="@string/purchase" /> android:layout_marginRight="16dp"
android:layout_marginBottom="42dp"
android:text="@string/pro_purchase" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -11,7 +11,9 @@
<string name="initial_value">Initial Value</string> <string name="initial_value">Initial Value</string>
<string name="less_then_2_values_for_display">Can\'t show because there is less than two values to display!</string> <string name="less_then_2_values_for_display">Can\'t show because there is less than two values to display!</string>
<string name="invalid_object">The object you\'re trying to access is invalid</string> <string name="invalid_object">The object you\'re trying to access is invalid</string>
<string name="billingclient_unavailable">The billing service is unavailable at the moment. Please check your internet connection and retry later.</string> <string name="billingclient_unavailable">The billing services are unavailable at the moment. Please check your internet connection and retry later.</string>
<string name="pro_upgrade">Upgrade to Pro</string>
<string name="pro_purchase">Go Pro</string>
<string name="address">Address</string> <string name="address">Address</string>
<string name="suggestions">Naming suggestions</string> <string name="suggestions">Naming suggestions</string>

@ -303,4 +303,24 @@
</style> </style>
<!-- Subscription -->
<style name="PokerAnalyticsTheme.TextView.SubscriptionTitle">
<item name="android:textColor">@color/white</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>
<item name="android:fontFamily">@font/roboto_light</item>
<item name="android:textSize">36sp</item>
</style>
<style name="PokerAnalyticsTheme.TextView.SubscriptionPrice">
<item name="android:textColor">@color/white</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>
<item name="android:fontFamily">@font/roboto_light</item>
<item name="android:textSize">28sp</item>
</style>
</resources> </resources>

Loading…
Cancel
Save