|
|
|
package org.tasks.billing
|
|
|
|
|
|
|
|
import android.app.Activity.RESULT_OK
|
|
|
|
import android.app.Dialog
|
|
|
|
import android.content.BroadcastReceiver
|
|
|
|
import android.content.Context
|
|
|
|
import android.content.DialogInterface
|
|
|
|
import android.content.Intent
|
|
|
|
import android.net.Uri
|
|
|
|
import android.os.Bundle
|
|
|
|
import android.text.method.LinkMovementMethod
|
|
|
|
import android.text.style.TextAppearanceSpan
|
|
|
|
import androidx.appcompat.app.AlertDialog
|
|
|
|
import androidx.core.content.ContextCompat
|
|
|
|
import androidx.core.view.isVisible
|
|
|
|
import androidx.fragment.app.DialogFragment
|
|
|
|
import androidx.fragment.app.Fragment
|
|
|
|
import butterknife.ButterKnife
|
|
|
|
import butterknife.OnClick
|
|
|
|
import com.google.android.material.slider.Slider
|
|
|
|
import dagger.hilt.android.AndroidEntryPoint
|
|
|
|
import io.noties.markwon.AbstractMarkwonPlugin
|
|
|
|
import io.noties.markwon.Markwon
|
|
|
|
import io.noties.markwon.MarkwonSpansFactory
|
|
|
|
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
|
|
|
|
import org.commonmark.ext.gfm.strikethrough.Strikethrough
|
|
|
|
import org.tasks.BuildConfig
|
|
|
|
import org.tasks.LocalBroadcastManager
|
|
|
|
import org.tasks.R
|
|
|
|
import org.tasks.analytics.Firebase
|
|
|
|
import org.tasks.databinding.ActivityPurchaseBinding
|
|
|
|
import org.tasks.dialogs.DialogBuilder
|
|
|
|
import org.tasks.locale.Locale
|
|
|
|
import timber.log.Timber
|
|
|
|
import javax.inject.Inject
|
|
|
|
|
|
|
|
|
|
|
|
@AndroidEntryPoint
|
|
|
|
class PurchaseDialog : DialogFragment(), OnPurchasesUpdated {
|
|
|
|
|
|
|
|
interface PurchaseHandler {
|
|
|
|
fun onPurchaseDialogDismissed()
|
|
|
|
}
|
|
|
|
|
|
|
|
private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
|
|
|
override fun onReceive(context: Context, intent: Intent) {
|
|
|
|
setup()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Inject lateinit var inventory: Inventory
|
|
|
|
@Inject lateinit var dialogBuilder: DialogBuilder
|
|
|
|
@Inject lateinit var billingClient: BillingClient
|
|
|
|
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
|
|
|
|
@Inject lateinit var locale: Locale
|
|
|
|
@Inject lateinit var firebase: Firebase
|
|
|
|
|
|
|
|
private lateinit var binding: ActivityPurchaseBinding
|
|
|
|
private lateinit var markwon: Markwon
|
|
|
|
|
|
|
|
private var currentSubscription: Purchase? = null
|
|
|
|
private var priceChanged = false
|
|
|
|
private var nameYourPrice = false
|
|
|
|
|
|
|
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
|
|
binding = ActivityPurchaseBinding.inflate(layoutInflater)
|
|
|
|
ButterKnife.bind(this, binding.root)
|
|
|
|
|
|
|
|
if (savedInstanceState != null) {
|
|
|
|
binding.slider.value = savedInstanceState.getFloat(EXTRA_PRICE)
|
|
|
|
priceChanged = savedInstanceState.getBoolean(EXTRA_PRICE_CHANGED)
|
|
|
|
nameYourPrice = savedInstanceState.getBoolean(EXTRA_NAME_YOUR_PRICE)
|
|
|
|
}
|
|
|
|
|
|
|
|
binding.slider.addOnChangeListener(this::onPriceChanged)
|
|
|
|
binding.slider.setLabelFormatter { "$${it - .01}" }
|
|
|
|
binding.text.movementMethod = LinkMovementMethod.getInstance()
|
|
|
|
|
|
|
|
markwon = Markwon.builder(requireContext())
|
|
|
|
.usePlugin(StrikethroughPlugin.create())
|
|
|
|
.usePlugin(object : AbstractMarkwonPlugin() {
|
|
|
|
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
|
|
|
|
builder.appendFactory(Strikethrough::class.java) { _, _ ->
|
|
|
|
TextAppearanceSpan(requireContext(), R.style.RedText)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.build()
|
|
|
|
|
|
|
|
setWaitScreen(BuildConfig.FLAVOR != "generic")
|
|
|
|
|
|
|
|
return if (BuildConfig.FLAVOR == "generic") {
|
|
|
|
if (isGitHub) {
|
|
|
|
getPurchaseDialog()
|
|
|
|
} else {
|
|
|
|
getMessageDialog(R.string.no_google_play_subscription)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (isGitHub) {
|
|
|
|
getMessageDialog(R.string.insufficient_sponsorship)
|
|
|
|
} else {
|
|
|
|
getPurchaseDialog()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getPurchaseDialog(): AlertDialog =
|
|
|
|
dialogBuilder.newDialog().setView(binding.root).show()
|
|
|
|
|
|
|
|
private fun getMessageDialog(res: Int): AlertDialog =
|
|
|
|
dialogBuilder.newDialog()
|
|
|
|
.setMessage(res)
|
|
|
|
.setPositiveButton(R.string.ok, null)
|
|
|
|
.setNeutralButton(R.string.help) { _, _ ->
|
|
|
|
val url = Uri.parse(getString(R.string.subscription_help_url))
|
|
|
|
startActivity(Intent(Intent.ACTION_VIEW, url))
|
|
|
|
}
|
|
|
|
.show()
|
|
|
|
|
|
|
|
private fun updateText() {
|
|
|
|
var benefits = "### ${getString(when {
|
|
|
|
nameYourPrice -> R.string.name_your_price
|
|
|
|
!inventory.hasPro -> R.string.upgrade_to_pro
|
|
|
|
!inventory.hasTasksSubscription -> R.string.button_upgrade
|
|
|
|
else -> R.string.modify_subscription
|
|
|
|
})}"
|
|
|
|
benefits += if (nameYourPrice) {
|
|
|
|
"""
|
|
|
|
---
|
|
|
|
#### ~~${getString(R.string.upgrade_sync_with_tasks)}~~
|
|
|
|
"""
|
|
|
|
} else {
|
|
|
|
"""
|
|
|
|
---
|
|
|
|
#### ${getString(R.string.upgrade_sync_with_tasks)}
|
|
|
|
* ${getString(R.string.upgrade_open_internet_standards)}
|
|
|
|
* ${getString(R.string.upgrade_privacy)}
|
|
|
|
* [${getString(R.string.upgrade_coming_soon)}](${getString(R.string.help_url_sync)})
|
|
|
|
"""
|
|
|
|
}
|
|
|
|
benefits += if (BuildConfig.FLAVOR == "generic") {
|
|
|
|
"""
|
|
|
|
---
|
|
|
|
**${getString(R.string.upgrade_previous_donors)}** - [${getString(R.string.contact_developer)}](mailto:${getString(R.string.support_email)}) ${getString(R.string.upgrade_previous_donors_contact)}
|
|
|
|
"""
|
|
|
|
} else {
|
|
|
|
"""
|
|
|
|
---
|
|
|
|
#### ${getString(R.string.upgrade_additional_features)}
|
|
|
|
* ${getString(R.string.upgrade_themes)}
|
|
|
|
* [${getString(R.string.davx5)}](${getString(R.string.url_davx5)})
|
|
|
|
* [${getString(R.string.caldav)}](${getString(R.string.url_caldav)})
|
|
|
|
* [${getString(R.string.upgrade_etesync)}](${getString(R.string.url_etesync)})
|
|
|
|
* ${getString(R.string.upgrade_google_tasks)}
|
|
|
|
* ${getString(R.string.upgrade_google_places)}
|
|
|
|
* [${getString(R.string.upgrade_tasker)}](${getString(R.string.url_tasker)})
|
|
|
|
---
|
|
|
|
* ${getString(R.string.upgrade_free_trial)}
|
|
|
|
* ${getString(R.string.upgrade_downgrade)}
|
|
|
|
"""
|
|
|
|
}
|
|
|
|
binding.text.text = markwon.toMarkdown(benefits)
|
|
|
|
}
|
|
|
|
|
|
|
|
@OnClick(R.id.pay_annually)
|
|
|
|
fun subscribeAnnually() {
|
|
|
|
initiatePurchase(false, 30)
|
|
|
|
}
|
|
|
|
|
|
|
|
@OnClick(R.id.pay_monthly)
|
|
|
|
fun subscribeMonthly() {
|
|
|
|
initiatePurchase(true, 3)
|
|
|
|
}
|
|
|
|
|
|
|
|
@OnClick(R.id.sponsor)
|
|
|
|
fun sponsor() {
|
|
|
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_sponsor))))
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun initiatePurchase(isMonthly: Boolean, price: Int) {
|
|
|
|
val newSku = String.format("%s_%02d", if (isMonthly) "monthly" else "annual", price)
|
|
|
|
billingClient.initiatePurchaseFlow(
|
|
|
|
requireActivity(),
|
|
|
|
newSku,
|
|
|
|
BillingClientImpl.TYPE_SUBS,
|
|
|
|
currentSubscription?.sku?.takeIf { it != newSku })
|
|
|
|
billingClient.addPurchaseCallback(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
@OnClick(R.id.pay_other)
|
|
|
|
fun nameYourPrice() {
|
|
|
|
if (isTasksPayment) {
|
|
|
|
dismiss()
|
|
|
|
} else {
|
|
|
|
nameYourPrice = !nameYourPrice
|
|
|
|
setWaitScreen(false)
|
|
|
|
binding.scroll.scrollTo(0, 0)
|
|
|
|
updateSubscribeButton()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onSaveInstanceState(outState: Bundle) {
|
|
|
|
super.onSaveInstanceState(outState)
|
|
|
|
outState.putFloat(EXTRA_PRICE, binding.slider.value)
|
|
|
|
outState.putBoolean(EXTRA_PRICE_CHANGED, priceChanged)
|
|
|
|
outState.putBoolean(EXTRA_NAME_YOUR_PRICE, nameYourPrice)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setWaitScreen(isWaitScreen: Boolean) {
|
|
|
|
Timber.d("setWaitScreen(%s)", isWaitScreen)
|
|
|
|
val generic = BuildConfig.FLAVOR == "generic"
|
|
|
|
binding.sliderContainer.isVisible = !isWaitScreen && nameYourPrice
|
|
|
|
binding.payOther.isVisible = !isWaitScreen
|
|
|
|
binding.payOther.setText(when {
|
|
|
|
nameYourPrice -> R.string.back
|
|
|
|
isTasksPayment -> R.string.cancel
|
|
|
|
else -> R.string.more_options
|
|
|
|
})
|
|
|
|
binding.tasksOrgButtonPanel.isVisible = !isWaitScreen && !generic
|
|
|
|
binding.screenWait.isVisible = isWaitScreen && !generic
|
|
|
|
binding.sponsor.isVisible = generic
|
|
|
|
updateText()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onStart() {
|
|
|
|
super.onStart()
|
|
|
|
localBroadcastManager.registerPurchaseReceiver(purchaseReceiver)
|
|
|
|
billingClient.queryPurchases()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onStop() {
|
|
|
|
super.onStop()
|
|
|
|
localBroadcastManager.unregisterReceiver(purchaseReceiver)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setup() {
|
|
|
|
currentSubscription = inventory.subscription
|
|
|
|
if (!priceChanged) {
|
|
|
|
binding.slider.value =
|
|
|
|
currentSubscription
|
|
|
|
?.subscriptionPrice
|
|
|
|
?.coerceAtMost(25)
|
|
|
|
?.toFloat() ?: 10f
|
|
|
|
}
|
|
|
|
updateSubscribeButton()
|
|
|
|
setWaitScreen(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateSubscribeButton() {
|
|
|
|
val sliderValue = binding.slider.value.toInt()
|
|
|
|
val annualPrice = if (nameYourPrice) sliderValue else 30
|
|
|
|
val monthlyPrice = if (nameYourPrice) sliderValue else 3
|
|
|
|
val constrained = resources.getBoolean(R.bool.width_constrained)
|
|
|
|
val aboveAverage = "${getString(R.string.above_average)} $POPPER"
|
|
|
|
binding.avgAnnual.text = when {
|
|
|
|
!nameYourPrice -> "${getString(
|
|
|
|
R.string.save_percent,
|
|
|
|
((1 - (annualPrice / (12.0 * monthlyPrice))) * 100).toInt()
|
|
|
|
)} $POPPER"
|
|
|
|
sliderValue < firebase.averageSubscription() -> "" //getString(R.string.below_average)
|
|
|
|
else -> aboveAverage
|
|
|
|
}
|
|
|
|
binding.avgAnnual.setTextColor(
|
|
|
|
if (nameYourPrice && sliderValue < firebase.averageSubscription()) {
|
|
|
|
ContextCompat.getColor(requireContext(), R.color.text_secondary)
|
|
|
|
} else {
|
|
|
|
ContextCompat.getColor(requireContext(), R.color.purchase_highlight)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
binding.avgMonthly.setTextColor(
|
|
|
|
ContextCompat.getColor(requireContext(), R.color.purchase_highlight)
|
|
|
|
)
|
|
|
|
binding.avgMonthly.text = aboveAverage
|
|
|
|
binding.payAnnually.let {
|
|
|
|
it.isEnabled = true
|
|
|
|
it.text = getString(
|
|
|
|
if (constrained) R.string.price_per_year_abbreviated else R.string.price_per_year,
|
|
|
|
annualPrice - .01
|
|
|
|
)
|
|
|
|
it.setOnClickListener {
|
|
|
|
initiatePurchase(false, if (nameYourPrice) sliderValue else 30)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
binding.payMonthly.let {
|
|
|
|
it.isEnabled = true
|
|
|
|
it.text = getString(
|
|
|
|
if (constrained) R.string.price_per_month_abbreviated else R.string.price_per_month,
|
|
|
|
monthlyPrice - .01
|
|
|
|
)
|
|
|
|
it.setOnClickListener {
|
|
|
|
initiatePurchase(true, if (nameYourPrice) sliderValue else 3)
|
|
|
|
}
|
|
|
|
it.isVisible = !nameYourPrice || sliderValue < 3
|
|
|
|
}
|
|
|
|
binding.avgMonthly.isVisible = nameYourPrice && binding.payMonthly.isVisible
|
|
|
|
currentSubscription?.let {
|
|
|
|
binding.payMonthly.isEnabled =
|
|
|
|
it.isCanceled || !it.isMonthly || monthlyPrice != it.subscriptionPrice
|
|
|
|
binding.payAnnually.isEnabled =
|
|
|
|
it.isCanceled || it.isMonthly || annualPrice != it.subscriptionPrice
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onPriceChanged(slider: Slider, value: Float, fromUser: Boolean) {
|
|
|
|
if (fromUser) {
|
|
|
|
priceChanged = true
|
|
|
|
}
|
|
|
|
updateSubscribeButton()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onPurchasesUpdated(success: Boolean) {
|
|
|
|
if (success) {
|
|
|
|
dismiss()
|
|
|
|
targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, null)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDismiss(dialog: DialogInterface) {
|
|
|
|
super.onDismiss(dialog)
|
|
|
|
|
|
|
|
activity.takeIf { it is PurchaseHandler }?.let {
|
|
|
|
(it as PurchaseHandler).onPurchaseDialogDismissed()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private val isTasksPayment: Boolean
|
|
|
|
get() = arguments?.getBoolean(EXTRA_TASKS_PAYMENT, false) ?: false
|
|
|
|
|
|
|
|
private val isGitHub: Boolean
|
|
|
|
get() = arguments?.getBoolean(EXTRA_GITHUB, false) ?: false
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
private const val POPPER = "\uD83C\uDF89"
|
|
|
|
private const val EXTRA_PRICE = "extra_price"
|
|
|
|
private const val EXTRA_PRICE_CHANGED = "extra_price_changed"
|
|
|
|
private const val EXTRA_NAME_YOUR_PRICE = "extra_name_your_price"
|
|
|
|
private const val EXTRA_TASKS_PAYMENT = "extra_tasks_payment"
|
|
|
|
private const val EXTRA_GITHUB = "extra_github"
|
|
|
|
|
|
|
|
@JvmStatic
|
|
|
|
val FRAG_TAG_PURCHASE_DIALOG = "frag_tag_purchase_dialog"
|
|
|
|
|
|
|
|
@JvmStatic
|
|
|
|
@JvmOverloads
|
|
|
|
fun newPurchaseDialog(
|
|
|
|
tasksPayment: Boolean = false,
|
|
|
|
github: Boolean = BuildConfig.FLAVOR == "generic"
|
|
|
|
): PurchaseDialog {
|
|
|
|
val dialog = PurchaseDialog()
|
|
|
|
val args = Bundle()
|
|
|
|
args.putBoolean(EXTRA_TASKS_PAYMENT, tasksPayment)
|
|
|
|
args.putBoolean(EXTRA_GITHUB, github)
|
|
|
|
dialog.arguments = args
|
|
|
|
return dialog
|
|
|
|
}
|
|
|
|
|
|
|
|
fun newPurchaseDialog(target: Fragment, rc: Int): PurchaseDialog {
|
|
|
|
val dialog = PurchaseDialog()
|
|
|
|
dialog.setTargetFragment(target, rc)
|
|
|
|
return dialog
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|