Skip to content

Commit ca55054

Browse files
committed
Enhance switching from subscription to lifetime
1 parent ab02f09 commit ca55054

9 files changed

Lines changed: 130 additions & 37 deletions

File tree

presentation/src/main/java/org/cryptomator/presentation/licensing/LicenseEnforcer.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,25 @@ class LicenseEnforcer @Inject constructor(private val sharedPreferencesHandler:
8888
data class LicenseUiState(
8989
val hasWriteAccess: Boolean,
9090
val hasPaidLicense: Boolean,
91+
val hasLifetimeLicense: Boolean,
92+
val hasRunningSubscription: Boolean,
9193
val trialState: TrialState,
9294
val trialExpirationText: String?
9395
)
9496

9597
fun evaluateUiState(context: Context): LicenseUiState {
9698
val trialState = evaluateTrialState()
9799
val paidLicense = hasPaidLicense()
100+
val lifetimeLicense = sharedPreferencesHandler.licenseToken().isNotEmpty()
101+
val runningSubscription = sharedPreferencesHandler.hasRunningSubscription()
98102
val expirationText = if (trialState.isActive || trialState.isExpired) {
99103
context.getString(R.string.screen_license_check_trial_expiration, trialState.formattedExpirationDate)
100104
} else null
101105
return LicenseUiState(
102106
hasWriteAccess = paidLicense || trialState.isActive,
103107
hasPaidLicense = paidLicense,
108+
hasLifetimeLicense = lifetimeLicense,
109+
hasRunningSubscription = runningSubscription,
104110
trialState = trialState,
105111
trialExpirationText = expirationText
106112
)

presentation/src/main/java/org/cryptomator/presentation/licensing/LicenseStateOrchestrator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class LicenseStateOrchestrator(
1414
) {
1515

1616
interface Target {
17-
fun onPurchaseStateChanged(hasWriteAccess: Boolean, hasPaidLicense: Boolean)
17+
fun onPurchaseStateChanged(hasWriteAccess: Boolean, hasPaidLicense: Boolean, hasLifetimeLicense: Boolean, hasRunningSubscription: Boolean)
1818
fun onTrialStateChanged(active: Boolean, expired: Boolean, expirationText: String?)
1919
}
2020

@@ -34,7 +34,7 @@ class LicenseStateOrchestrator(
3434

3535
fun updateState() {
3636
val uiState = licenseEnforcer.evaluateUiState(contextProvider())
37-
target.onPurchaseStateChanged(uiState.hasWriteAccess, uiState.hasPaidLicense)
37+
target.onPurchaseStateChanged(uiState.hasWriteAccess, uiState.hasPaidLicense, uiState.hasLifetimeLicense, uiState.hasRunningSubscription)
3838
target.onTrialStateChanged(uiState.trialState.isActive, uiState.trialState.isExpired, uiState.trialExpirationText)
3939
}
4040
}

presentation/src/main/java/org/cryptomator/presentation/ui/activity/LicenseCheckActivity.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.cryptomator.presentation.presenter.LicenseCheckPresenter
1616
import org.cryptomator.presentation.service.RestoreOutcome
1717
import org.cryptomator.presentation.service.RestoreOutcomeHandler
1818
import org.cryptomator.presentation.ui.activity.view.UpdateLicenseView
19+
import org.cryptomator.presentation.ui.dialog.CancelSubscriptionReminderDialog
1920
import org.cryptomator.presentation.ui.dialog.EnterLicenseDialog
2021
import org.cryptomator.presentation.ui.dialog.LicenseConfirmationDialog
2122
import org.cryptomator.presentation.ui.dialog.NoFullVersionDialog
@@ -34,7 +35,8 @@ class LicenseCheckActivity : BaseActivity<ActivityLicenseCheckBinding>(ActivityL
3435
RestoreSuccessfulDialog.Callback, //
3536
NoFullVersionDialog.Callback, //
3637
RestoreFailedDialog.Callback, //
37-
EnterLicenseDialog.Callback {
38+
EnterLicenseDialog.Callback, //
39+
CancelSubscriptionReminderDialog.Callback {
3840

3941
@Inject
4042
lateinit var licenseCheckPresenter: LicenseCheckPresenter
@@ -46,17 +48,22 @@ class LicenseCheckActivity : BaseActivity<ActivityLicenseCheckBinding>(ActivityL
4648
lateinit var licenseCheckIntent: LicenseCheckIntent
4749

4850
private var lockedAction: LicenseEnforcer.LockedAction? = null
51+
private var wasSubscriptionOnly = false
4952
private val licenseContentViewBinder by lazy { LicenseContentViewBinder(binding.licenseContent, FlavorConfig.isFreemiumFlavor) }
5053

5154
private val orchestrator by lazy {
5255
LicenseStateOrchestrator(
5356
sharedPreferencesHandler, licenseEnforcer, { this },
5457
target = object : LicenseStateOrchestrator.Target {
55-
override fun onPurchaseStateChanged(hasWriteAccess: Boolean, hasPaidLicense: Boolean) {
56-
licenseContentViewBinder.bindPurchaseState(hasWriteAccess, hasPaidLicense)
58+
override fun onPurchaseStateChanged(hasWriteAccess: Boolean, hasPaidLicense: Boolean, hasLifetimeLicense: Boolean, hasRunningSubscription: Boolean) {
59+
if (hasLifetimeLicense && hasRunningSubscription && wasSubscriptionOnly) {
60+
showDialog(CancelSubscriptionReminderDialog.newInstance())
61+
}
62+
wasSubscriptionOnly = hasRunningSubscription && !hasLifetimeLicense
63+
licenseContentViewBinder.bindPurchaseState(hasWriteAccess, hasPaidLicense, hasLifetimeLicense, hasRunningSubscription, hasLockedActionHeader = lockedAction != null)
5764
}
5865
override fun onTrialStateChanged(active: Boolean, expired: Boolean, expirationText: String?) {
59-
licenseContentViewBinder.bindTrialState(active, expired, expirationText, hasLockedActionHeader = lockedAction != null)
66+
licenseContentViewBinder.bindTrialState(active, expired, expirationText, hasLockedActionHeader = lockedAction != null, hasSubscriptionUpgradeHint = wasSubscriptionOnly)
6067
}
6168
},
6269
priceLoader = { licenseContentViewBinder.loadAndBindPrices(application as CryptomatorApp) }
@@ -180,4 +187,5 @@ class LicenseCheckActivity : BaseActivity<ActivityLicenseCheckBinding>(ActivityL
180187
override fun onRestoreSuccessfulDialogFinished() = Unit
181188
override fun onNoFullVersionDialogFinished() = Unit
182189
override fun onRestoreFailedDialogFinished() = Unit
190+
override fun onCancelSubscriptionReminderDismissed() = Unit
183191
}

presentation/src/main/java/org/cryptomator/presentation/ui/activity/WelcomeActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>(ActivityWelcomeBind
6262
LicenseStateOrchestrator(
6363
sharedPreferencesHandler, licenseEnforcer, { this },
6464
target = object : LicenseStateOrchestrator.Target {
65-
override fun onPurchaseStateChanged(hasWriteAccess: Boolean, hasPaidLicense: Boolean) {
65+
override fun onPurchaseStateChanged(hasWriteAccess: Boolean, hasPaidLicense: Boolean, hasLifetimeLicense: Boolean, hasRunningSubscription: Boolean) {
6666
if (!this@WelcomeActivity::pagerAdapter.isInitialized) {
6767
return
6868
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.cryptomator.presentation.ui.dialog
2+
3+
import android.content.DialogInterface
4+
import android.content.Intent
5+
import android.net.Uri
6+
import android.view.KeyEvent
7+
import androidx.appcompat.app.AlertDialog
8+
import org.cryptomator.generator.Dialog
9+
import org.cryptomator.presentation.R
10+
import org.cryptomator.presentation.databinding.DialogCancelSubscriptionReminderBinding
11+
import org.cryptomator.presentation.service.ProductInfo
12+
13+
@Dialog
14+
class CancelSubscriptionReminderDialog : BaseDialog<CancelSubscriptionReminderDialog.Callback, DialogCancelSubscriptionReminderBinding>(DialogCancelSubscriptionReminderBinding::inflate) {
15+
16+
interface Callback {
17+
18+
fun onCancelSubscriptionReminderDismissed()
19+
}
20+
21+
public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog {
22+
builder //
23+
.setTitle(R.string.dialog_cancel_subscription_reminder_title) //
24+
.setPositiveButton(getString(R.string.dialog_cancel_subscription_reminder_manage_button)) { _: DialogInterface, _: Int ->
25+
val url = "https://play.google.com/store/account/subscriptions?sku=${ProductInfo.PRODUCT_YEARLY_SUBSCRIPTION}&package=${requireContext().packageName}"
26+
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
27+
callback?.onCancelSubscriptionReminderDismissed()
28+
}
29+
.setNegativeButton(getString(R.string.dialog_cancel_subscription_reminder_close_button)) { _: DialogInterface, _: Int -> callback?.onCancelSubscriptionReminderDismissed() }
30+
.setOnKeyListener { _, keyCode, _ ->
31+
if (keyCode == KeyEvent.KEYCODE_BACK) {
32+
dialog?.dismiss()
33+
callback?.onCancelSubscriptionReminderDismissed()
34+
true
35+
} else {
36+
false
37+
}
38+
}
39+
return builder.create()
40+
}
41+
42+
public override fun setupView() {
43+
super.onStart()
44+
val dialog = dialog as AlertDialog?
45+
dialog?.setCanceledOnTouchOutside(false)
46+
}
47+
48+
companion object {
49+
50+
fun newInstance(): CancelSubscriptionReminderDialog {
51+
return CancelSubscriptionReminderDialog()
52+
}
53+
}
54+
}

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ import androidx.preference.Preference
1414
import androidx.preference.PreferenceCategory
1515
import androidx.preference.SwitchPreference
1616
import org.cryptomator.presentation.BuildConfig
17-
import org.cryptomator.presentation.CryptomatorApp
1817
import org.cryptomator.presentation.R
1918
import org.cryptomator.presentation.intent.Intents
2019
import org.cryptomator.presentation.licensing.LicenseEnforcer
2120
import org.cryptomator.presentation.presenter.ContextHolder
2221
import org.cryptomator.presentation.service.PhotoContentJob
2322
import org.cryptomator.presentation.service.ProductInfo
24-
import org.cryptomator.presentation.service.resolveProductPrices
2523
import org.cryptomator.presentation.ui.activity.AutoUploadChooseVaultActivity
2624
import org.cryptomator.presentation.ui.activity.BiometricAuthSettingsActivity
2725
import org.cryptomator.presentation.ui.activity.CloudSettingsActivity
@@ -37,12 +35,11 @@ import org.cryptomator.util.FlavorConfig
3735
import org.cryptomator.util.SharedPreferencesHandler
3836
import org.cryptomator.util.SharedPreferencesHandler.Companion.CRYPTOMATOR_VARIANTS
3937
import org.cryptomator.util.file.LruFileCacheUtil
40-
import java.util.function.Consumer
4138
import java.lang.Boolean.FALSE
4239
import java.lang.Boolean.TRUE
4340
import java.lang.String.format
44-
import java.lang.ref.WeakReference
4541
import java.text.DecimalFormat
42+
import java.util.function.Consumer
4643
import kotlin.math.log10
4744
import timber.log.Timber
4845

@@ -245,25 +242,10 @@ class SettingsFragment : PreferenceFragmentCompatLayout() {
245242
key = UPGRADE_LIFETIME_KEY
246243
title = getString(R.string.screen_settings_upgrade_to_lifetime)
247244
setOnPreferenceClickListener {
248-
val app = requireActivity().application as CryptomatorApp
249-
app.launchPurchaseFlow(WeakReference(requireActivity()), ProductInfo.PRODUCT_FULL_VERSION)
245+
Intents.licenseCheckIntent().startActivity(activity() as ContextHolder)
250246
true
251247
}
252248
})
253-
val app = requireActivity().application as CryptomatorApp
254-
app.queryProductDetails { products ->
255-
val prices = products.resolveProductPrices()
256-
activity?.runOnUiThread {
257-
if (!isAdded) {
258-
return@runOnUiThread
259-
}
260-
category.findPreference<Preference>(UPGRADE_LIFETIME_KEY)?.let { pref ->
261-
if (!prices.lifetimePrice.isNullOrEmpty()) {
262-
pref.summary = prices.lifetimePrice
263-
}
264-
}
265-
}
266-
}
267249
}
268250
} else {
269251
category.findPreference<Preference>(UPGRADE_LIFETIME_KEY)?.let {

presentation/src/main/java/org/cryptomator/presentation/ui/layout/LicenseContentViewBinder.kt

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,32 @@ class LicenseContentViewBinder(
129129
}
130130

131131
/** Updates purchase-related view visibility based on license state. */
132-
fun bindPurchaseState(unlocked: Boolean, hasPaidLicense: Boolean) {
132+
fun bindPurchaseState(unlocked: Boolean, hasPaidLicense: Boolean, hasLifetimeLicense: Boolean = false, hasRunningSubscription: Boolean = false, hasLockedActionHeader: Boolean = false) {
133133
if (isFreemiumFlavor) {
134-
binding.purchaseOptionsGroup.visibility = if (hasPaidLicense) View.GONE else View.VISIBLE
135-
binding.tvRestorePurchase.visibility = if (hasPaidLicense) View.GONE else View.VISIBLE
136-
if (hasPaidLicense) {
137-
binding.tvInfoText.visibility = View.GONE
138-
binding.tvTrialStatusBadge.visibility = View.GONE
139-
binding.tvTrialExpiration.visibility = View.GONE
134+
when {
135+
hasLifetimeLicense -> {
136+
binding.purchaseOptionsGroup.visibility = View.GONE
137+
binding.tvRestorePurchase.visibility = View.GONE
138+
binding.tvInfoText.visibility = View.GONE
139+
binding.tvTrialStatusBadge.visibility = View.GONE
140+
binding.tvTrialExpiration.visibility = View.GONE
141+
}
142+
hasRunningSubscription -> {
143+
binding.purchaseOptionsGroup.visibility = View.VISIBLE
144+
binding.tvRestorePurchase.visibility = View.GONE
145+
binding.rowTrial.visibility = View.GONE
146+
binding.dividerTrialSubscription.visibility = View.GONE
147+
binding.rowSubscription.visibility = View.GONE
148+
binding.dividerSubscriptionLifetime.visibility = View.GONE
149+
if (!hasLockedActionHeader) {
150+
binding.tvInfoText.visibility = View.VISIBLE
151+
binding.tvInfoText.text = context.getString(R.string.screen_license_check_subscription_upgrade_hint)
152+
}
153+
}
154+
else -> {
155+
binding.purchaseOptionsGroup.visibility = View.VISIBLE
156+
binding.tvRestorePurchase.visibility = View.VISIBLE
157+
}
140158
}
141159
} else {
142160
binding.btnPurchase.isEnabled = !unlocked
@@ -150,7 +168,7 @@ class LicenseContentViewBinder(
150168
}
151169

152170
/** Updates trial-related view visibility based on trial state. */
153-
fun bindTrialState(active: Boolean, expired: Boolean, expirationText: String?, hasLockedActionHeader: Boolean = false) {
171+
fun bindTrialState(active: Boolean, expired: Boolean, expirationText: String?, hasLockedActionHeader: Boolean = false, hasSubscriptionUpgradeHint: Boolean = false) {
154172
if (active || expired) {
155173
binding.trialButtonGroup.visibility = View.GONE
156174
binding.tvTrialStatusBadge.visibility = View.VISIBLE
@@ -160,10 +178,10 @@ class LicenseContentViewBinder(
160178
)
161179
binding.tvTrialExpiration.visibility = View.VISIBLE
162180
binding.tvTrialExpiration.text = expirationText
163-
if (expired && !hasLockedActionHeader) {
181+
if (expired && !hasLockedActionHeader && !hasSubscriptionUpgradeHint) {
164182
binding.tvInfoText.visibility = View.VISIBLE
165183
binding.tvInfoText.text = context.getString(R.string.screen_license_check_trial_expired_info)
166-
} else if (!hasLockedActionHeader) {
184+
} else if (!hasLockedActionHeader && !hasSubscriptionUpgradeHint) {
167185
binding.tvInfoText.visibility = View.GONE
168186
}
169187
} else {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:layout_width="match_parent"
4+
android:layout_height="match_parent">
5+
6+
<RelativeLayout
7+
android:layout_width="match_parent"
8+
android:layout_height="match_parent"
9+
android:padding="@dimen/activity_vertical_margin">
10+
11+
<TextView
12+
android:id="@+id/tv_cancel_subscription_reminder"
13+
android:layout_width="wrap_content"
14+
android:layout_height="wrap_content"
15+
android:text="@string/dialog_cancel_subscription_reminder_message" />
16+
17+
</RelativeLayout>
18+
19+
</androidx.core.widget.NestedScrollView>

presentation/src/main/res/values/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
<string name="screen_license_check_trial_status_expired">Expired</string>
9191
<string name="screen_license_check_trial_active_info">Trial expires: %s</string>
9292
<string name="screen_license_check_trial_expired_info">Your trial has expired.</string>
93+
<string name="screen_license_check_subscription_upgrade_hint">You have an active subscription. Buy the lifetime license and cancel your subscription to switch.</string>
9394
<string name="screen_welcome_title">Welcome</string>
9495
<string name="screen_welcome_notifications_title">Stay notified</string>
9596
<string name="screen_welcome_notifications_message">Allow notifications so Cryptomator can inform you about background activity, auto uploads, or other important events.</string>
@@ -618,6 +619,11 @@
618619
<string name="dialog_restore_failed_message">We couldn\u0027t reach the Play Store. Please check your connection and try again.</string>
619620
<string name="dialog_restore_failed_positive_button" translatable="false">@string/dialog_unable_to_share_positive_button</string>
620621

622+
<string name="dialog_cancel_subscription_reminder_title">Remember to cancel your subscription</string>
623+
<string name="dialog_cancel_subscription_reminder_message">You now have a lifetime license. To avoid future charges, please cancel your yearly subscription in the Google Play Store.</string>
624+
<string name="dialog_cancel_subscription_reminder_manage_button">Manage Subscription</string>
625+
<string name="dialog_cancel_subscription_reminder_close_button">Close</string>
626+
621627
<string name="dialog_hub_user_setup_required_title">User setup required</string>
622628
<string name="dialog_hub_user_setup_required_hint">To proceed, please complete the steps required in your Hub user profile.</string>
623629
<string name="dialog_hub_user_setup_required_neutral_button">Go to Profile</string>

0 commit comments

Comments
 (0)