diff --git a/presentation/src/main/java/org/cryptomator/presentation/CryptomatorApp.kt b/presentation/src/main/java/org/cryptomator/presentation/CryptomatorApp.kt index 3b0811747..40f1a9f1d 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/CryptomatorApp.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/CryptomatorApp.kt @@ -30,7 +30,6 @@ import org.cryptomator.presentation.service.PendingCallbackQueue import org.cryptomator.presentation.service.ProductInfo import org.cryptomator.presentation.service.PurchaseRevokedToastObserver import org.cryptomator.presentation.service.RestoreOutcome -import org.cryptomator.presentation.service.RestoreOutcomeDialogObserver import org.cryptomator.util.FlavorConfig import org.cryptomator.util.NoOpActivityLifecycleCallbacks import org.cryptomator.util.SharedPreferencesHandler @@ -53,11 +52,6 @@ class CryptomatorApp : MultiDexApplication(), HasComponent @Volatile private var iapBillingServiceBinder: IapBillingService.Binder? = null - fun restorePurchasesAndStore() { - val handler = SharedPreferencesHandler(applicationContext()) - restorePurchases { outcome -> handler.setPendingRestoreOutcome(outcome.kind.name) } - } - private val pendingProductDetailsCallbacks = PendingCallbackQueue>() override fun onCreate() { @@ -87,7 +81,6 @@ class CryptomatorApp : MultiDexApplication(), HasComponent registerActivityLifecycleCallbacks(serviceNotifier) if (FlavorConfig.isFreemiumFlavor) { registerActivityLifecycleCallbacks(PurchaseRevokedToastObserver(sharedPreferencesHandler)) - registerActivityLifecycleCallbacks(RestoreOutcomeDialogObserver(sharedPreferencesHandler)) } AppCompatDelegate.setDefaultNightMode(sharedPreferencesHandler.screenStyleMode) cleanupCache() diff --git a/presentation/src/main/java/org/cryptomator/presentation/licensing/LicenseStateOrchestrator.kt b/presentation/src/main/java/org/cryptomator/presentation/licensing/LicenseStateOrchestrator.kt index dd3f1c76e..259f95666 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/licensing/LicenseStateOrchestrator.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/licensing/LicenseStateOrchestrator.kt @@ -14,7 +14,6 @@ class LicenseStateOrchestrator( interface Callback { fun onLicenseStateChanged(uiState: LicenseEnforcer.LicenseUiState) fun onSubscriptionActivatedFirstTime() {} - fun onSubscriptionUpgradedToLifetime() {} } private val licenseChangeListener = Consumer { _ -> updateState() } @@ -41,9 +40,6 @@ class LicenseStateOrchestrator( val wasSubOnly = wasSubscriptionOnly wasSubscriptionOnly = nowSubOnly - if (wasSubOnly && uiState.hasLifetimeLicense && uiState.hasRunningSubscription) { - callback.onSubscriptionUpgradedToLifetime() - } if (!wasSubOnly && nowSubOnly) { callback.onSubscriptionActivatedFirstTime() } diff --git a/presentation/src/main/java/org/cryptomator/presentation/service/RestoreOutcome.kt b/presentation/src/main/java/org/cryptomator/presentation/service/RestoreOutcome.kt index 3b0a5a508..03367bee7 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/service/RestoreOutcome.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/service/RestoreOutcome.kt @@ -9,22 +9,10 @@ sealed interface RestoreOutcome { data object RESTORED : RestoreOutcome data object NOTHING_TO_RESTORE : RestoreOutcome data class FAILED(val cause: Throwable? = null) : RestoreOutcome - - /** Stable identifier for persisting a pending outcome via SharedPreferences (see [RestoreOutcomeDialogObserver]). */ - val kind: Kind - get() = when (this) { - RESTORED -> Kind.RESTORED - NOTHING_TO_RESTORE -> Kind.NOTHING_TO_RESTORE - is FAILED -> Kind.FAILED - } - - enum class Kind { RESTORED, NOTHING_TO_RESTORE, FAILED } } -fun RestoreOutcome.Kind.toDialogFragment(): DialogFragment = when (this) { - RestoreOutcome.Kind.RESTORED -> RestoreSuccessfulDialog() - RestoreOutcome.Kind.NOTHING_TO_RESTORE -> NoFullVersionDialog() - RestoreOutcome.Kind.FAILED -> RestoreFailedDialog() +fun RestoreOutcome.toDialogFragment(): DialogFragment = when (this) { + RestoreOutcome.RESTORED -> RestoreSuccessfulDialog() + RestoreOutcome.NOTHING_TO_RESTORE -> NoFullVersionDialog() + is RestoreOutcome.FAILED -> RestoreFailedDialog() } - -fun RestoreOutcome.toDialogFragment(): DialogFragment = kind.toDialogFragment() diff --git a/presentation/src/main/java/org/cryptomator/presentation/service/RestoreOutcomeDialogObserver.kt b/presentation/src/main/java/org/cryptomator/presentation/service/RestoreOutcomeDialogObserver.kt deleted file mode 100644 index a20a7bb29..000000000 --- a/presentation/src/main/java/org/cryptomator/presentation/service/RestoreOutcomeDialogObserver.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.cryptomator.presentation.service - -import android.app.Activity -import androidx.fragment.app.FragmentActivity -import org.cryptomator.util.NoOpActivityLifecycleCallbacks -import org.cryptomator.util.SharedPreferencesHandler -import timber.log.Timber - -class RestoreOutcomeDialogObserver( - private val sharedPreferencesHandler: SharedPreferencesHandler -) : NoOpActivityLifecycleCallbacks() { - - override fun onActivityResumed(activity: Activity) { - val kindName = sharedPreferencesHandler.pendingRestoreOutcome() - if (kindName.isEmpty()) { - return - } - sharedPreferencesHandler.clearPendingRestoreOutcome() - val kind = runCatching { RestoreOutcome.Kind.valueOf(kindName) }.getOrNull() - if (kind == null) { - Timber.tag("RestoreOutcomeDialogObserver").w("Invalid pending outcome %s; cleared", kindName) - return - } - (activity as? FragmentActivity)?.let { - kind.toDialogFragment().show(it.supportFragmentManager, "RestoreOutcome") - } - } -} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BaseActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BaseActivity.kt index fc725e351..0b45840e5 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BaseActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/BaseActivity.kt @@ -232,6 +232,10 @@ abstract class BaseActivity(val bindingFactory: (LayoutInflate override fun context(): Context = this override fun showDialog(dialog: DialogFragment) { + if (isFinishing || isDestroyed || supportFragmentManager.isStateSaved) { + Timber.tag("BaseActivity").i("Skipping showDialog for %s; activity not in a state to commit fragments", dialog.javaClass.simpleName) + return + } closeDialog() currentDialog = dialog dialog.show(supportFragmentManager, ACTIVE_DIALOG) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/LicenseCheckActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/LicenseCheckActivity.kt index 9694a3737..74efb0093 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/LicenseCheckActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/LicenseCheckActivity.kt @@ -13,7 +13,6 @@ import org.cryptomator.presentation.licensing.LicenseEnforcer import org.cryptomator.presentation.licensing.LicenseStateOrchestrator import org.cryptomator.presentation.presenter.LicenseCheckPresenter import org.cryptomator.presentation.ui.activity.view.LicenseView -import org.cryptomator.presentation.ui.dialog.CancelSubscriptionReminderDialog import org.cryptomator.presentation.ui.dialog.EnterLicenseDialog import org.cryptomator.presentation.ui.dialog.LicenseConfirmationDialog import org.cryptomator.presentation.ui.layout.LicenseContentViewBinder @@ -52,10 +51,6 @@ class LicenseCheckActivity : BaseActivity(ActivityL override fun onSubscriptionActivatedFirstTime() { finish() } - - override fun onSubscriptionUpgradedToLifetime() { - showDialog(CancelSubscriptionReminderDialog.newInstance()) - } }, priceLoader = { licenseContentViewBinder.loadAndBindPrices(application as CryptomatorApp) } ) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CancelSubscriptionReminderDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CancelSubscriptionReminderDialog.kt deleted file mode 100644 index cbc6fc111..000000000 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CancelSubscriptionReminderDialog.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.cryptomator.presentation.ui.dialog - -import android.content.Intent -import android.net.Uri -import androidx.appcompat.app.AlertDialog -import org.cryptomator.generator.Dialog -import org.cryptomator.presentation.R -import org.cryptomator.presentation.databinding.DialogCancelSubscriptionReminderBinding -import org.cryptomator.presentation.service.ProductInfo - -@Dialog -class CancelSubscriptionReminderDialog : BaseInformationalDialog(DialogCancelSubscriptionReminderBinding::inflate) { - - override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog = builder - .setTitle(R.string.dialog_cancel_subscription_reminder_title) - .setPositiveButton(getString(R.string.dialog_cancel_subscription_reminder_manage_button)) { _, _ -> - val url = "https://play.google.com/store/account/subscriptions?sku=${ProductInfo.PRODUCT_YEARLY_SUBSCRIPTION}&package=${requireContext().packageName}" - requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) - } - .setNegativeButton(getString(R.string.dialog_cancel_subscription_reminder_close_button), null) - .dismissOnBackKey() - .create() - - companion object { - fun newInstance() = CancelSubscriptionReminderDialog() - } -} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/WelcomeLicenseFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/WelcomeLicenseFragment.kt index bed3b9db7..023372a29 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/WelcomeLicenseFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/WelcomeLicenseFragment.kt @@ -6,6 +6,7 @@ import org.cryptomator.presentation.CryptomatorApp import org.cryptomator.presentation.R import org.cryptomator.presentation.databinding.FragmentWelcomeLicenseBinding import org.cryptomator.presentation.licensing.LicenseEnforcer +import org.cryptomator.presentation.ui.activity.WelcomeActivity import org.cryptomator.presentation.ui.layout.LicenseContentViewBinder import org.cryptomator.util.FlavorConfig @@ -45,7 +46,7 @@ class WelcomeLicenseFragment : BaseFragment(Fragm licenseContentViewBinder.bindInitialIapLayout() licenseContentViewBinder.bindLegalLinks() licenseContentViewBinder.bindPurchaseButtons( - activity = requireActivity(), + activity = activity(), app = app, onTrialClicked = { listener?.onStartTrial() } ) @@ -81,4 +82,7 @@ class WelcomeLicenseFragment : BaseFragment(Fragm } listener?.onLicenseTextChanged(license) } + + private fun activity(): WelcomeActivity = this.activity as WelcomeActivity + } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/layout/LicenseContentViewBinder.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/layout/LicenseContentViewBinder.kt index 818765112..17c79dc42 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/layout/LicenseContentViewBinder.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/layout/LicenseContentViewBinder.kt @@ -1,6 +1,5 @@ package org.cryptomator.presentation.ui.layout -import android.app.Activity import android.content.Intent import android.net.Uri import android.view.View @@ -9,7 +8,10 @@ import org.cryptomator.presentation.R import org.cryptomator.presentation.databinding.ViewLicenseCheckContentBinding import org.cryptomator.presentation.licensing.LicenseEnforcer import org.cryptomator.presentation.service.ProductInfo +import org.cryptomator.presentation.service.RestoreOutcome import org.cryptomator.presentation.service.resolveProductPrices +import org.cryptomator.presentation.service.toDialogFragment +import org.cryptomator.presentation.ui.activity.BaseActivity import java.lang.ref.WeakReference /** Shared visibility-toggling logic for the license check content included layout. */ @@ -78,7 +80,7 @@ class LicenseContentViewBinder( /** Wires trial, subscription, lifetime, and restore button click listeners. */ fun bindPurchaseButtons( - activity: Activity, + activity: BaseActivity<*>, app: CryptomatorApp, onTrialClicked: () -> Unit ) { @@ -90,10 +92,20 @@ class LicenseContentViewBinder( app.launchPurchaseFlow(WeakReference(activity), ProductInfo.PRODUCT_FULL_VERSION) } binding.tvRestorePurchase.setOnClickListener { - app.restorePurchasesAndStore() + binding.tvRestorePurchase.isEnabled = false + app.restorePurchases { outcome -> + binding.root.post { + binding.tvRestorePurchase.isEnabled = true + showRestoreOutcome(activity, outcome) + } + } } } + private fun showRestoreOutcome(activity: BaseActivity<*>, outcome: RestoreOutcome) { + activity.showDialog(outcome.toDialogFragment()) + } + /** Queries product details and updates price buttons on the UI thread. */ fun loadAndBindPrices(app: CryptomatorApp) { app.queryProductDetails { products -> @@ -180,6 +192,7 @@ class LicenseContentViewBinder( val text: String? = when { lockedAction != null -> context.getString(lockedAction.headerMessageRes) uiState.hasSubscriptionUpgradeHint -> context.getString(R.string.screen_license_check_subscription_upgrade_hint) + uiState.hasCancelSubscriptionHint -> context.getString(R.string.screen_license_check_cancel_subscription_hint, context.getString(R.string.screen_settings_manage_subscription)) uiState.trialState.isExpired && !uiState.hasPaidLicense -> context.getString(R.string.screen_license_check_trial_expired_info) else -> null } @@ -190,3 +203,6 @@ class LicenseContentViewBinder( private val LicenseEnforcer.LicenseUiState.hasSubscriptionUpgradeHint: Boolean get() = hasRunningSubscription && !hasLifetimeLicense + +private val LicenseEnforcer.LicenseUiState.hasCancelSubscriptionHint: Boolean + get() = hasRunningSubscription && hasLifetimeLicense diff --git a/presentation/src/main/res/layout/dialog_cancel_subscription_reminder.xml b/presentation/src/main/res/layout/dialog_cancel_subscription_reminder.xml deleted file mode 100644 index 426e3a27b..000000000 --- a/presentation/src/main/res/layout/dialog_cancel_subscription_reminder.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index f6b133dce..ff55f2dc6 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -91,6 +91,7 @@ Trial expires: %s Your trial has expired. You have an active subscription. Buy the lifetime license and cancel your subscription to switch. + You now have a lifetime license. To avoid future charges, cancel your yearly subscription via the %1$s option in Settings. Welcome Stay notified Allow notifications so Cryptomator can inform you about background activity, auto uploads, or other important events. @@ -619,11 +620,6 @@ We couldn\u0027t reach the Play Store. Please check your connection and try again. @string/dialog_unable_to_share_positive_button - Remember to cancel your subscription - You now have a lifetime license. To avoid future charges, please cancel your yearly subscription in the Google Play Store. - Manage Subscription - Close - User setup required To proceed, please complete the steps required in your Hub user profile. Go to Profile diff --git a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt index 1e0cc129a..2cb4caf8e 100644 --- a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt +++ b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt @@ -246,18 +246,6 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen setPurchaseRevokedState(pending = false, reason = "") } - fun pendingRestoreOutcome(): String { - return defaultSharedPreferences.getValue(PENDING_RESTORE_OUTCOME, "") - } - - fun setPendingRestoreOutcome(kind: String) { - defaultSharedPreferences.setValue(PENDING_RESTORE_OUTCOME, kind) - } - - fun clearPendingRestoreOutcome() { - defaultSharedPreferences.setValue(PENDING_RESTORE_OUTCOME, "") - } - fun hasRunningSubscription(): Boolean { return defaultSharedPreferences.getValue(HAS_RUNNING_SUBSCRIPTION, false) } @@ -415,7 +403,6 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen private const val HAS_RUNNING_SUBSCRIPTION = "hasRunningSubscription" private const val PURCHASE_REVOKED_PENDING = "purchaseRevokedPending" private const val PURCHASE_REVOKED_REASON = "purchaseRevokedReason" - private const val PENDING_RESTORE_OUTCOME = "pendingRestoreOutcome" const val DEBUG_MODE = "debugMode" const val DISABLE_APP_WHEN_OBSCURED = "disableAppWhenObscured" const val SECURE_SCREEN = "secureScreen"