diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-05-29 16:45:18 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-05-29 16:45:18 -0300 |
| commit | 7b349fd6d977aaeee3ee1ccc3943eb891b3efe22 (patch) | |
| tree | 6328562b470712ce0213af3786b382410fd8b5c7 | |
| parent | 92979541abdc20125d0248902f99ee781e930dc6 (diff) | |
| parent | a308f5f522405472c1b64d30f55e4b9acac6469e (diff) | |
| download | mullvadvpn-7b349fd6d977aaeee3ee1ccc3943eb891b3efe22.tar.xz mullvadvpn-7b349fd6d977aaeee3ee1ccc3943eb891b3efe22.zip | |
Merge branch 'account-screen-payment-buttons'
6 files changed, 174 insertions, 20 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 8821b77ca7..3e49683704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ Line wrap the file at 100 chars. Th ## [Unreleased] +### Added +#### Android +- Add buttons to buy credit and redeem voucher in Account screen. + ### Changed #### Android - Show the remaining account time in the Settings screen in days if it's less than 3 months. diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt index 4231ff1e40..f63ca84f58 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt @@ -8,6 +8,13 @@ import net.mullvad.talpid.util.EventNotifier import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat +// Number of retry attempts to check for a changed expiry before giving up. +// Current value will force the cache to keep fetching for about four minutes or until a new expiry +// value is received. +// This is only used if the expiry was invalidated and fetching a new expiry returns the same value +// as before the invalidation. +const val MAX_INVALIDATED_RETRIES = 7 + class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsListener) { companion object { public val EXPIRY_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss z") @@ -27,6 +34,8 @@ class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsList onAccountExpiryChange.notify(value) } + private var oldAccountExpiry: DateTime? = null + val onAccountNumberChange = EventNotifier<String?>(null) val onAccountExpiryChange = EventNotifier<DateTime?>(null) @@ -46,8 +55,9 @@ class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsList val result = daemon.getAccountData(account) if (result is GetAccountDataResult.Ok) { - handleNewExpiry(account, result.accountData.expiry) - break + if (handleNewExpiry(account, result.accountData.expiry, retryAttempt)) { + break + } } else if (result is GetAccountDataResult.InvalidAccount) { break } @@ -60,6 +70,15 @@ class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsList } } + fun invalidateAccountExpiry(accountExpiryToInvalidate: DateTime) { + synchronized(this) { + if (accountExpiry == accountExpiryToInvalidate) { + oldAccountExpiry = accountExpiryToInvalidate + fetchAccountExpiry() + } + } + } + fun onDestroy() { settingsListener.accountNumberNotifier.unsubscribe(this) jobTracker.cancelAllJobs() @@ -74,11 +93,26 @@ class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsList } } - private fun handleNewExpiry(accountNumberUsedForFetch: String, expiryString: String) { + private fun handleNewExpiry( + accountNumberUsedForFetch: String, + expiryString: String, + retryAttempt: Int + ): Boolean { synchronized(this) { - if (accountNumber === accountNumberUsedForFetch) { - accountExpiry = DateTime.parse(expiryString, EXPIRY_FORMAT) + if (accountNumber !== accountNumberUsedForFetch) { + return true + } + + val newAccountExpiry = DateTime.parse(expiryString, EXPIRY_FORMAT) + + if (newAccountExpiry != oldAccountExpiry || retryAttempt >= MAX_INVALIDATED_RETRIES) { + accountExpiry = newAccountExpiry + oldAccountExpiry = null + + return true } + + return false } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt index 3787d2bafe..f7606ea737 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt @@ -7,8 +7,11 @@ import android.view.View import android.view.ViewGroup import java.text.DateFormat import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.ui.widget.Button import net.mullvad.mullvadvpn.ui.widget.CopyableInformationView import net.mullvad.mullvadvpn.ui.widget.InformationView +import net.mullvad.mullvadvpn.ui.widget.UrlButton import org.joda.time.DateTime class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) { @@ -16,8 +19,30 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) { private val timeStyle = DateFormat.SHORT private val expiryFormatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle) + private var oldAccountExpiry: DateTime? = null + + private var currentAccountExpiry: DateTime? = null + set(value) { + field = value + + synchronized(this) { + if (value != oldAccountExpiry) { + oldAccountExpiry = null + } + } + } + + private var hasConnectivity = true + set(value) { + field = value + buyCreditButton.setEnabled(value) + redeemVoucherButton.setEnabled(value) + } + private lateinit var accountExpiryView: InformationView private lateinit var accountNumberView: CopyableInformationView + private lateinit var buyCreditButton: Button + private lateinit var redeemVoucherButton: Button override fun onSafelyCreateView( inflater: LayoutInflater, @@ -30,7 +55,21 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) { parentActivity.onBackPressed() } - view.findViewById<View>(R.id.logout).setOnClickListener { logout() } + buyCreditButton = view.findViewById<UrlButton>(R.id.buy_credit).apply { + prepare(daemon, jobTracker) { + checkForAddedTime() + } + } + + redeemVoucherButton = view.findViewById<Button>(R.id.redeem_voucher).apply { + setOnClickAction("redeem", jobTracker) { + showRedeemVoucherDialog() + } + } + + view.findViewById<Button>(R.id.logout).setOnClickAction("logout", jobTracker) { + logout() + } accountNumberView = view.findViewById<CopyableInformationView>(R.id.account_number).apply { displayFormatter = { rawAccountNumber -> addSpacesToAccountNumber(rawAccountNumber) } @@ -50,9 +89,22 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) { accountCache.onAccountExpiryChange.subscribe(this) { accountExpiry -> jobTracker.newUiJob("updateAccountExpiry") { + currentAccountExpiry = accountExpiry updateAccountExpiry(accountExpiry) } } + + connectionProxy.onUiStateChange.subscribe(this) { uiState -> + jobTracker.newUiJob("updateHasConnectivity") { + hasConnectivity = uiState is TunnelState.Connected || + uiState is TunnelState.Disconnected || + (uiState is TunnelState.Error && !uiState.errorState.isBlocking) + } + } + + oldAccountExpiry?.let { expiry -> + accountCache.invalidateAccountExpiry(expiry) + } } override fun onSafelyPause() { @@ -60,6 +112,13 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) { accountCache.onAccountExpiryChange.unsubscribe(this) } + private fun checkForAddedTime() { + currentAccountExpiry?.let { expiry -> + oldAccountExpiry = expiry + accountCache.invalidateAccountExpiry(expiry) + } + } + private fun updateAccountExpiry(accountExpiry: DateTime?) { if (accountExpiry != null) { accountExpiryView.information = expiryFormatter.format(accountExpiry.toDate()) @@ -69,14 +128,22 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) { } } - private fun logout() { + private fun showRedeemVoucherDialog() { + val transaction = fragmentManager?.beginTransaction() + + transaction?.addToBackStack(null) + + RedeemVoucherDialogFragment().show(transaction, null) + } + + private suspend fun logout() { clearAccountNumber() clearBackStack() goToLoginScreen() } - private fun clearAccountNumber() { - jobTracker.newBackgroundJob("clearAccountNumber") { + private suspend fun clearAccountNumber() { + jobTracker.runOnBackground { daemon.setAccount(null) } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt index d583684ad1..1a300e1fec 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt @@ -15,10 +15,12 @@ import android.widget.EditText import android.widget.TextView import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.VoucherSubmissionResult +import net.mullvad.mullvadvpn.service.AccountCache import net.mullvad.mullvadvpn.service.MullvadDaemon import net.mullvad.mullvadvpn.ui.widget.Button import net.mullvad.mullvadvpn.util.JobTracker import net.mullvad.mullvadvpn.util.SegmentedInputFormatter +import org.joda.time.DateTime const val FULL_VOUCHER_CODE_LENGTH = "XXXX-XXXX-XXXX-XXXX".length @@ -29,6 +31,9 @@ class RedeemVoucherDialogFragment : DialogFragment() { private lateinit var errorMessage: TextView private lateinit var voucherInput: EditText + private var accountCache: AccountCache? = null + private var accountExpiry: DateTime? = null + private var accountExpiryListener: Int? = null private var redeemButton: Button? = null private var daemon: MullvadDaemon? = null @@ -50,6 +55,15 @@ class RedeemVoucherDialogFragment : DialogFragment() { parentActivity.serviceNotifier.subscribe(this) { connection -> daemon = connection?.daemon + + accountCache?.onAccountExpiryChange?.unsubscribe(this@RedeemVoucherDialogFragment) + + accountCache = connection?.accountCache?.apply { + onAccountExpiryChange + .subscribe(this@RedeemVoucherDialogFragment) { newAccountExpiry -> + accountExpiry = newAccountExpiry + } + } } } @@ -112,6 +126,10 @@ class RedeemVoucherDialogFragment : DialogFragment() { override fun onDetach() { parentActivity.serviceNotifier.unsubscribe(this) + accountExpiryListener?.let { id -> + accountCache?.onAccountExpiryChange?.unsubscribe(id) + } + super.onDetach() } @@ -129,11 +147,7 @@ class RedeemVoucherDialogFragment : DialogFragment() { } when (result) { - is VoucherSubmissionResult.Ok -> { - if (result.submission.timeAdded > 0) { - dismiss() - } - } + is VoucherSubmissionResult.Ok -> handleAddedTime(result.submission.timeAdded) is VoucherSubmissionResult.InvalidVoucher -> showError(R.string.invalid_voucher) is VoucherSubmissionResult.VoucherAlreadyUsed -> { showError(R.string.voucher_already_used) @@ -142,6 +156,16 @@ class RedeemVoucherDialogFragment : DialogFragment() { } } + private fun handleAddedTime(timeAdded: Long) { + if (timeAdded > 0) { + accountExpiry?.let { oldAccountExpiry -> + accountCache?.invalidateAccountExpiry(oldAccountExpiry) + } + + dismiss() + } + } + private fun showError(message: Int) { errorMessage.apply { setText(message) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt index 3215e6f616..c2a7839748 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt @@ -45,7 +45,12 @@ class UrlButton : Button { super.showSpinner = true } - fun prepare(daemon: MullvadDaemon, jobTracker: JobTracker, jobName: String = "fetchUrl") { + fun prepare( + daemon: MullvadDaemon, + jobTracker: JobTracker, + jobName: String = "fetchUrl", + extraOnClickAction: (suspend () -> Unit)? = null + ) { synchronized(this) { super.setEnabled(shouldEnable) @@ -55,6 +60,7 @@ class UrlButton : Button { super.setEnabled(false) context.startActivity(buildIntent(jobTracker)) + extraOnClickAction?.invoke() super.setEnabled(true) } diff --git a/android/src/main/res/layout/account.xml b/android/src/main/res/layout/account.xml index fb18b09c04..6356d3ae70 100644 --- a/android/src/main/res/layout/account.xml +++ b/android/src/main/res/layout/account.xml @@ -54,10 +54,29 @@ android:paddingVertical="12dp" mullvad:description="@string/paid_until" mullvad:whenMissing="hide" /> - <Button android:id="@+id/logout" - android:layout_marginTop="12dp" - android:layout_marginHorizontal="24dp" - android:text="@string/log_out" - style="@style/RedButton" /> + <net.mullvad.mullvadvpn.ui.widget.UrlButton android:id="@+id/buy_credit" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:layout_marginHorizontal="24dp" + mullvad:showSpinner="true" + mullvad:url="@string/account_url" + mullvad:withToken="true" + mullvad:text="@string/buy_more_credit" + mullvad:buttonColor="green" /> + <net.mullvad.mullvadvpn.ui.widget.Button android:id="@+id/redeem_voucher" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginHorizontal="24dp" + mullvad:text="@string/redeem_voucher" + mullvad:buttonColor="green" /> + <net.mullvad.mullvadvpn.ui.widget.Button android:id="@+id/logout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginHorizontal="24dp" + mullvad:text="@string/log_out" + mullvad:buttonColor="red" /> </LinearLayout> </LinearLayout> |
