From e676f324f1cea7bb85255522896c859e8f97799e Mon Sep 17 00:00:00 2001 From: Varsha <102332078+varsha888@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:15:49 -0700 Subject: [PATCH] Add new handling to encourage the user to save their wallet recovery phrase. This only effects those who have opted in to payments and have a non-zero balance. --- .../securesms/keyvalue/PaymentsValues.kt | 50 +++--- .../backup/PaymentsRecoveryStartFragment.java | 158 +++++++++++++++--- .../payments/backup/RecoveryPhraseStates.java | 9 + ...PaymentsRecoveryPhraseConfirmFragment.java | 27 +-- .../PaymentsRecoveryPhraseFragment.java | 2 +- .../confirm/ConfirmPaymentFragment.java | 4 +- .../preferences/PaymentsHomeAdapter.java | 2 +- .../preferences/PaymentsHomeFragment.java | 41 ++++- .../preferences/PaymentsHomeViewModel.java | 2 +- .../payments/preferences/model/InfoCard.java | 6 +- .../viewholder/InfoCardViewHolder.java | 2 +- .../AdvancedPinPreferenceFragment.java | 3 +- .../main/res/navigation/payments_backup.xml | 24 ++- .../res/navigation/payments_preferences.xml | 20 +++ app/src/main/res/values/strings.xml | 27 ++- .../signalservice/api/payments/Money.java | 7 + 16 files changed, 308 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/payments/backup/RecoveryPhraseStates.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt index 8af205db1e..6fa68d7db5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt @@ -38,6 +38,7 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa private const val PAYMENTS_CURRENT_CURRENCY = "payments_current_currency" private const val DEFAULT_CURRENCY_CODE = "GBP" private const val USER_CONFIRMED_MNEMONIC = "mob_payments_user_confirmed_mnemonic" + private const val USER_CONFIRMED_MNEMONIC_LARGE_BALANCE = "mob_payments_user_confirmed_mnemonic_large_balance" private const val SHOW_ABOUT_MOBILE_COIN_INFO_CARD = "mob_payments_show_about_mobile_coin_info_card" private const val SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD = "mob_payments_show_adding_to_your_wallet_info_card" private const val SHOW_CASHING_OUT_INFO_CARD = "mob_payments_show_cashing_out_info_card" @@ -46,6 +47,7 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa private const val PAYMENT_LOCK_ENABLED = "mob_payments_payment_lock_enabled" private const val PAYMENT_LOCK_TIMESTAMP = "mob_payments_payment_lock_timestamp" private const val PAYMENT_LOCK_SKIP_COUNT = "mob_payments_payment_lock_skip_count" + private const val SHOW_SAVE_RECOVERY_PHRASE = "mob_show_save_recovery_phrase" private val LARGE_BALANCE_THRESHOLD = Money.mobileCoin(BigDecimal.valueOf(500)) @@ -53,18 +55,17 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa const val MOB_PAYMENTS_ENABLED = "mob_payments_enabled" } - var paymentLock - get() = getBoolean(PAYMENT_LOCK_ENABLED, false) - set(enabled) = putBoolean(PAYMENT_LOCK_ENABLED, enabled) - - var paymentLockTimestamp - get() = getLong(PAYMENT_LOCK_TIMESTAMP, 0) - set(timestamp) = putLong(PAYMENT_LOCK_TIMESTAMP, timestamp) - - var paymentLockSkipCount - get() = getInteger(PAYMENT_LOCK_SKIP_COUNT, 0) - set(count) = putInteger(PAYMENT_LOCK_SKIP_COUNT, count) + var paymentLock: Boolean by booleanValue(PAYMENT_LOCK_ENABLED, false) + var paymentLockTimestamp: Long by longValue(PAYMENT_LOCK_TIMESTAMP, 0) + var paymentLockSkipCount: Int by integerValue(PAYMENT_LOCK_SKIP_COUNT, 0) + var showSaveRecoveryPhrase: Boolean by booleanValue(SHOW_SAVE_RECOVERY_PHRASE, true) + var userConfirmedMnemonic + get() = getBoolean(USER_CONFIRMED_MNEMONIC, false) + private set(value) = putBoolean(USER_CONFIRMED_MNEMONIC, value) + private var userConfirmedMnemonicLargeBalance + get() = getBoolean(USER_CONFIRMED_MNEMONIC_LARGE_BALANCE, false) + set(value) = putBoolean(USER_CONFIRMED_MNEMONIC_LARGE_BALANCE, value) private val liveCurrentCurrency: MutableLiveData by lazy { MutableLiveData(currentCurrency()) } private val liveMobileCoinLedger: MutableLiveData by lazy { MutableLiveData(mobileCoinLatestFullLedger()) } private val liveMobileCoinBalance: LiveData by lazy { Transformations.map(liveMobileCoinLedger) { obj: MobileCoinLedgerWrapper -> obj.balance } } @@ -79,20 +80,25 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa PAYMENTS_CURRENT_CURRENCY, DEFAULT_CURRENCY_CODE, USER_CONFIRMED_MNEMONIC, + USER_CONFIRMED_MNEMONIC_LARGE_BALANCE, SHOW_ABOUT_MOBILE_COIN_INFO_CARD, SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD, SHOW_CASHING_OUT_INFO_CARD, SHOW_RECOVERY_PHRASE_INFO_CARD, - SHOW_UPDATE_PIN_INFO_CARD + SHOW_UPDATE_PIN_INFO_CARD, + PAYMENT_LOCK_ENABLED, + PAYMENT_LOCK_TIMESTAMP, + PAYMENT_LOCK_SKIP_COUNT, + SHOW_SAVE_RECOVERY_PHRASE ) } - fun userConfirmedMnemonic(): Boolean { - return store.getBoolean(USER_CONFIRMED_MNEMONIC, false) - } - - fun setUserConfirmedMnemonic(userConfirmedMnemonic: Boolean) { - store.beginWrite().putBoolean(USER_CONFIRMED_MNEMONIC, userConfirmedMnemonic).commit() + fun confirmMnemonic(confirmed: Boolean) { + if (userHasLargeBalance()) { + userConfirmedMnemonicLargeBalance = confirmed + } else { + userConfirmedMnemonic = confirmed + } } /** @@ -219,11 +225,11 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa return store.getBoolean(SHOW_CASHING_OUT_INFO_CARD, true) } - fun showRecoveryPhraseInfoCard(): Boolean { + fun isMnemonicConfirmed(): Boolean { return if (userHasLargeBalance()) { - store.getBoolean(SHOW_CASHING_OUT_INFO_CARD, true) + userConfirmedMnemonicLargeBalance } else { - false + userConfirmedMnemonic } } @@ -323,7 +329,7 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa val existingEntropy = paymentsEntropy.bytes if (Arrays.equals(existingEntropy, entropyFromMnemonic)) { setMobileCoinPaymentsEnabled(true) - setUserConfirmedMnemonic(true) + confirmMnemonic(true) return WalletRestoreResult.ENTROPY_UNCHANGED } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java index 4d1e71ec7e..52783129b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java @@ -5,20 +5,35 @@ import android.view.View; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; +import androidx.biometric.BiometricManager; +import androidx.biometric.BiometricPrompt; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.BiometricDeviceAuthentication; +import org.thoughtcrime.securesms.BiometricDeviceLockContract; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.navigation.SafeNavigation; import org.thoughtcrime.securesms.util.views.LearnMoreTextView; import org.whispersystems.signalservice.api.payments.PaymentsConstants; +import kotlin.Unit; + public class PaymentsRecoveryStartFragment extends Fragment { - private final OnBackPressed onBackPressed = new OnBackPressed(); + private static final String TAG = Log.tag(PaymentsRecoveryStartFragment.class); + + private ActivityResultLauncher activityResultLauncher; + private boolean finishOnConfirm; public PaymentsRecoveryStartFragment() { super(R.layout.payments_recovery_start_fragment); @@ -26,13 +41,16 @@ public class PaymentsRecoveryStartFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - Toolbar toolbar = view.findViewById(R.id.payments_recovery_start_fragment_toolbar); - TextView title = view.findViewById(R.id.payments_recovery_start_fragment_title); - LearnMoreTextView message = view.findViewById(R.id.payments_recovery_start_fragment_message); - TextView startButton = view.findViewById(R.id.payments_recovery_start_fragment_start); - TextView pasteButton = view.findViewById(R.id.payments_recovery_start_fragment_paste); + Toolbar toolbar = view.findViewById(R.id.payments_recovery_start_fragment_toolbar); + TextView title = view.findViewById(R.id.payments_recovery_start_fragment_title); + LearnMoreTextView message = view.findViewById(R.id.payments_recovery_start_fragment_message); + TextView startButton = view.findViewById(R.id.payments_recovery_start_fragment_start); + TextView pasteButton = view.findViewById(R.id.payments_recovery_start_fragment_paste); + PaymentsRecoveryStartFragmentArgs args = PaymentsRecoveryStartFragmentArgs.fromBundle(requireArguments()); + RecoveryPhraseStates state = args.getRecoveryPhraseState(); + OnBackPressed onBackPressed = new OnBackPressed(state); - PaymentsRecoveryStartFragmentArgs args = PaymentsRecoveryStartFragmentArgs.fromBundle(requireArguments()); + finishOnConfirm = args.getFinishOnConfirm(); if (args.getIsRestore()) { title.setText(R.string.PaymentsRecoveryStartFragment__enter_recovery_phrase); @@ -43,38 +61,138 @@ public class PaymentsRecoveryStartFragment extends Fragment { pasteButton.setVisibility(View.VISIBLE); pasteButton.setOnClickListener(v -> SafeNavigation.safeNavigate(Navigation.findNavController(v), PaymentsRecoveryStartFragmentDirections.actionPaymentsRecoveryStartToPaymentsRecoveryPaste())); } else { - title.setText(R.string.PaymentsRecoveryStartFragment__view_recovery_phrase); - message.setText(getString(R.string.PaymentsRecoveryStartFragment__your_balance_will_automatically_restore, PaymentsConstants.MNEMONIC_LENGTH)); + title.setText(getTitle(state)); + message.setText(getDescription(state)); message.setLink(getString(R.string.PaymentsRecoveryStartFragment__learn_more__view)); - startButton.setOnClickListener(v -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), PaymentsRecoveryStartFragmentDirections.actionPaymentsRecoveryStartToPaymentsRecoveryPhrase(args.getFinishOnConfirm()))); + startButton.setOnClickListener(v -> { + if (state == RecoveryPhraseStates.FROM_PAYMENTS_MENU_WITH_MNEMONIC_CONFIRMED && ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure() && SignalStore.paymentsValues().getPaymentLock()) { + BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo + .Builder() + .setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS) + .setTitle(requireContext().getString(R.string.BiometricDeviceAuthentication__signal)) + .setConfirmationRequired(false) + .build(); + BiometricDeviceAuthentication biometricAuth = new BiometricDeviceAuthentication(BiometricManager.from(requireActivity()), + new BiometricPrompt(requireActivity(), new BiometricAuthenticationListener()), + promptInfo); + biometricAuth.authenticate(requireContext(), true, this::showConfirmDeviceCredentialIntent); + } else { + goToRecoveryPhrase(); + } + }); startButton.setText(R.string.PaymentsRecoveryStartFragment__start); pasteButton.setVisibility(View.GONE); } - toolbar.setNavigationOnClickListener(v -> { - if (args.getFinishOnConfirm()) { - requireActivity().finish(); - } else { - Navigation.findNavController(requireView()).popBackStack(); + message.setLearnMoreVisible(true); + toolbar.setNavigationOnClickListener(v -> onBackPressed(state)); + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), onBackPressed); + activityResultLauncher = registerForActivityResult(new BiometricDeviceLockContract(), result -> { + if (result == BiometricDeviceAuthentication.AUTHENTICATED) { + goToRecoveryPhrase(); } }); + } - if (args.getFinishOnConfirm()) { - requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), onBackPressed); + private Unit showConfirmDeviceCredentialIntent() { + activityResultLauncher.launch(getString(R.string.BiometricDeviceAuthentication__signal)); + return Unit.INSTANCE; + } + + private String getTitle(RecoveryPhraseStates state) { + String title; + + switch (state) { + case FROM_PAYMENTS_MENU_WITH_MNEMONIC_NOT_CONFIRMED: + case FROM_INFO_CARD_WITH_MNEMONIC_NOT_CONFIRMED: + case FIRST_TIME_NON_ZERO_BALANCE_WITH_MNEMONIC_NOT_CONFIRMED: + title = getString(R.string.PaymentsRecoveryStartFragment__save_recovery_phrase); + break; + default: + title = getString(R.string.PaymentsRecoveryStartFragment__view_recovery_phrase); } + return title; + } - message.setLearnMoreVisible(true); + private String getDescription(RecoveryPhraseStates state) { + String description; + + switch (state) { + case FROM_INFO_CARD_WITH_MNEMONIC_NOT_CONFIRMED: + description = getString(R.string.PaymentsRecoveryStartFragment__time_to_save); + break; + case FIRST_TIME_NON_ZERO_BALANCE_WITH_MNEMONIC_NOT_CONFIRMED: + description = getString(R.string.PaymentsRecoveryStartFragment__got_balance); + break; + default: + description = getString(R.string.PaymentsRecoveryStartFragment__your_balance_will_automatically_restore, PaymentsConstants.MNEMONIC_LENGTH); + } + return description; + } + + private void goToRecoveryPhrase() { + PaymentsRecoveryStartFragmentArgs args = PaymentsRecoveryStartFragmentArgs.fromBundle(requireArguments()); + SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), PaymentsRecoveryStartFragmentDirections.actionPaymentsRecoveryStartToPaymentsRecoveryPhrase(args.getFinishOnConfirm())); + } + + private void onBackPressed(RecoveryPhraseStates state) { + if (state == RecoveryPhraseStates.FIRST_TIME_NON_ZERO_BALANCE_WITH_MNEMONIC_NOT_CONFIRMED || + state == RecoveryPhraseStates.FROM_INFO_CARD_WITH_MNEMONIC_NOT_CONFIRMED) + { + showSkipRecoveryDialog(); + } else { + goBack(); + } + } + + private void goBack() { + if (finishOnConfirm) { + requireActivity().finish(); + } else { + Navigation.findNavController(requireView()).popBackStack(); + } + } + + private void showSkipRecoveryDialog() { + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.PaymentsRecoveryStartFragment__continue_without_saving) + .setMessage(R.string.PaymentsRecoveryStartFragment__your_recovery_phrase) + .setPositiveButton(R.string.PaymentsRecoveryStartFragment__skip_recovery_phrase, (d, w) -> goBack()) + .setNegativeButton(R.string.PaymentsRecoveryStartFragment__cancel, null) + .show(); } private class OnBackPressed extends OnBackPressedCallback { + RecoveryPhraseStates state; - public OnBackPressed() { + public OnBackPressed(RecoveryPhraseStates state) { super(true); + this.state = state; } @Override public void handleOnBackPressed() { - requireActivity().finish(); + onBackPressed(state); + } + } + + private class BiometricAuthenticationListener extends BiometricPrompt.AuthenticationCallback { + + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errorString) { + Log.w(TAG, "Authentication error: " + errorCode); + onAuthenticationFailed(); + } + + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + Log.i(TAG, "onAuthenticationSucceeded"); + goToRecoveryPhrase(); + } + + @Override + public void onAuthenticationFailed() { + Log.w(TAG, "Unable to authenticate payment lock"); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/RecoveryPhraseStates.java b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/RecoveryPhraseStates.java new file mode 100644 index 0000000000..acd318263c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/RecoveryPhraseStates.java @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.payments.backup; + +public enum RecoveryPhraseStates { + FROM_PAYMENTS_MENU_WITH_MNEMONIC_CONFIRMED, + FROM_PAYMENTS_MENU_WITH_MNEMONIC_NOT_CONFIRMED, + FROM_INFO_CARD_WITH_MNEMONIC_NOT_CONFIRMED, + FIRST_TIME_NON_ZERO_BALANCE_WITH_MNEMONIC_NOT_CONFIRMED, + NONE +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/confirm/PaymentsRecoveryPhraseConfirmFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/confirm/PaymentsRecoveryPhraseConfirmFragment.java index 7b66093632..6006509932 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/confirm/PaymentsRecoveryPhraseConfirmFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/confirm/PaymentsRecoveryPhraseConfirmFragment.java @@ -30,10 +30,12 @@ public class PaymentsRecoveryPhraseConfirmFragment extends Fragment { /** * The minimum number of characters required to show an error mark. */ - private static final int ERROR_THRESHOLD = 1; + private static final int ERROR_THRESHOLD = 1; + public static final String RECOVERY_PHRASE_CONFIRMED = "recovery_phrase_confirmed"; + public static final String REQUEST_KEY_RECOVERY_PHRASE = "org.thoughtcrime.securesms.payments.backup.confirm.RECOVERY_PHRASE"; - private Drawable validWordCheckMark; - private Drawable invalidWordX; + private Drawable validWordCheckMark; + private Drawable invalidWordX; public PaymentsRecoveryPhraseConfirmFragment() { super(R.layout.payments_recovery_phrase_confirm_fragment); @@ -41,13 +43,13 @@ public class PaymentsRecoveryPhraseConfirmFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - Toolbar toolbar = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_toolbar); - EditText word1 = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_word_1); - EditText word2 = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_word_2); - View seePhraseAgain = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_see_again); - View done = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_done); - TextInputLayout wordWrapper1 = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_word1_wrapper); - TextInputLayout wordWrapper2 = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_word2_wrapper); + Toolbar toolbar = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_toolbar); + EditText word1 = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_word_1); + EditText word2 = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_word_2); + View seePhraseAgain = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_see_again); + View done = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_done); + TextInputLayout wordWrapper1 = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_word1_wrapper); + TextInputLayout wordWrapper2 = view.findViewById(R.id.payments_recovery_phrase_confirm_fragment_word2_wrapper); PaymentsRecoveryPhraseConfirmFragmentArgs args = PaymentsRecoveryPhraseConfirmFragmentArgs.fromBundle(requireArguments()); @@ -65,7 +67,7 @@ public class PaymentsRecoveryPhraseConfirmFragment extends Fragment { word2.addTextChangedListener(new AfterTextChanged(e -> viewModel.validateWord2(e.toString()))); seePhraseAgain.setOnClickListener(v -> Navigation.findNavController(requireView()).popBackStack()); done.setOnClickListener(v -> { - SignalStore.paymentsValues().setUserConfirmedMnemonic(true); + SignalStore.paymentsValues().confirmMnemonic(true); ViewUtil.hideKeyboard(requireContext(), view); Toast.makeText(requireContext(), R.string.PaymentRecoveryPhraseConfirmFragment__recovery_phrase_confirmed, Toast.LENGTH_SHORT).show(); @@ -73,6 +75,9 @@ public class PaymentsRecoveryPhraseConfirmFragment extends Fragment { requireActivity().setResult(Activity.RESULT_OK); requireActivity().finish(); } else { + Bundle result = new Bundle(); + result.putBoolean(RECOVERY_PHRASE_CONFIRMED, true); + getParentFragmentManager().setFragmentResult(REQUEST_KEY_RECOVERY_PHRASE, result); Navigation.findNavController(view).popBackStack(R.id.paymentsHome, false); } }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/phrase/PaymentsRecoveryPhraseFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/phrase/PaymentsRecoveryPhraseFragment.java index 7ad0f6d7e1..ff759c8dd7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/phrase/PaymentsRecoveryPhraseFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/phrase/PaymentsRecoveryPhraseFragment.java @@ -83,7 +83,7 @@ public class PaymentsRecoveryPhraseFragment extends Fragment { if (args.getFinishOnConfirm()) { requireActivity().finish(); } else { - toolbar.setNavigationOnClickListener(t -> Navigation.findNavController(view).popBackStack(R.id.paymentsHome, false)); + Navigation.findNavController(view).popBackStack(R.id.paymentsHome, false); } }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java index 708a4caf53..bac3bcf42b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java @@ -143,7 +143,7 @@ public class ConfirmPaymentFragment extends BottomSheetDialogFragment { BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo .Builder() .setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS) - .setTitle(requireContext().getString(R.string.ConfirmPaymentFragment__unlock_to_send_payment)) + .setTitle(requireContext().getString(R.string.BiometricDeviceAuthentication__signal)) .setConfirmationRequired(false) .build(); biometricAuth = new BiometricDeviceAuthentication(BiometricManager.from(requireActivity()), @@ -239,7 +239,7 @@ public class ConfirmPaymentFragment extends BottomSheetDialogFragment { } public Unit showConfirmDeviceCredentialIntent() { - activityResultLauncher.launch(getString(R.string.ConfirmPaymentFragment__unlock_to_send_payment)); + activityResultLauncher.launch(getString(R.string.BiometricDeviceAuthentication__signal)); return Unit.INSTANCE; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeAdapter.java index e6391993b6..9534217ca5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeAdapter.java @@ -35,7 +35,7 @@ public class PaymentsHomeAdapter extends MappingAdapter { default void onRestorePaymentsAccount() {} default void onSeeAll(@NonNull PaymentType paymentType) {} default void onPaymentItem(@NonNull PaymentItem model) {} - default void onInfoCardDismissed() {} + default void onInfoCardDismissed(InfoCard.Type type) {} default void onViewRecoveryPhrase() {} default void onUpdatePin() {} } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java index 52544a5731..c7788ce644 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.payments.preferences; import android.app.AlertDialog; -import android.graphics.Color; import android.os.Bundle; import android.view.MenuItem; import android.view.View; @@ -33,6 +32,9 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.payments.FiatMoneyUtil; import org.thoughtcrime.securesms.payments.MoneyView; +import org.thoughtcrime.securesms.payments.backup.RecoveryPhraseStates; +import org.thoughtcrime.securesms.payments.backup.confirm.PaymentsRecoveryPhraseConfirmFragment; +import org.thoughtcrime.securesms.payments.preferences.model.InfoCard; import org.thoughtcrime.securesms.payments.preferences.model.PaymentItem; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.SpanUtil; @@ -121,6 +123,12 @@ public class PaymentsHomeFragment extends LoggingFragment { viewModel = new ViewModelProvider(this, new PaymentsHomeViewModel.Factory()).get(PaymentsHomeViewModel.class); + getParentFragmentManager().setFragmentResultListener(PaymentsRecoveryPhraseConfirmFragment.REQUEST_KEY_RECOVERY_PHRASE, this, (requestKey, result) -> { + if (result.getBoolean(PaymentsRecoveryPhraseConfirmFragment.RECOVERY_PHRASE_CONFIRMED)) { + viewModel.updateStore(); + } + }); + viewModel.getList().observe(getViewLifecycleOwner(), list -> { boolean hadPaymentItems = Stream.of(adapter.getCurrentList()).anyMatch(model -> model instanceof PaymentItem); @@ -139,7 +147,17 @@ public class PaymentsHomeFragment extends LoggingFragment { } header.setVisibility(enabled ? View.VISIBLE : View.GONE); }); - viewModel.getBalance().observe(getViewLifecycleOwner(), balance::setMoney); + + viewModel.getBalance().observe(getViewLifecycleOwner(), balanceAmount -> { + balance.setMoney(balanceAmount); + if (SignalStore.paymentsValues().getShowSaveRecoveryPhrase() && + !SignalStore.paymentsValues().getUserConfirmedMnemonic() && + !balanceAmount.isEqualOrLessThanZero()) { + SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), PaymentsHomeFragmentDirections.actionPaymentsHomeToPaymentsBackup().setRecoveryPhraseState(RecoveryPhraseStates.FIRST_TIME_NON_ZERO_BALANCE_WITH_MNEMONIC_NOT_CONFIRMED)); + SignalStore.paymentsValues().setShowSaveRecoveryPhrase(false); + } + }); + viewModel.getExchange().observe(getViewLifecycleOwner(), amount -> { if (amount != null) { exchange.setText(FiatMoneyUtil.format(getResources(), amount)); @@ -251,7 +269,10 @@ public class PaymentsHomeFragment extends LoggingFragment { viewModel.deactivatePayments(); return true; } else if (item.getItemId() == R.id.payments_home_fragment_menu_view_recovery_phrase) { - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_paymentsHome_to_paymentsBackup); + SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), + PaymentsHomeFragmentDirections.actionPaymentsHomeToPaymentsBackup().setRecoveryPhraseState(SignalStore.paymentsValues().isMnemonicConfirmed() ? + RecoveryPhraseStates.FROM_PAYMENTS_MENU_WITH_MNEMONIC_CONFIRMED : + RecoveryPhraseStates.FROM_PAYMENTS_MENU_WITH_MNEMONIC_NOT_CONFIRMED)); return true; } else if (item.getItemId() == R.id.payments_home_fragment_menu_help) { startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.PAYMENT_INDEX)); @@ -303,8 +324,11 @@ public class PaymentsHomeFragment extends LoggingFragment { } @Override - public void onInfoCardDismissed() { - viewModel.onInfoCardDismissed(); + public void onInfoCardDismissed(InfoCard.Type type) { + viewModel.updateStore(); + if (type == InfoCard.Type.RECORD_RECOVERY_PHASE) { + showSaveRecoveryPhrase(); + } } @Override @@ -314,7 +338,12 @@ public class PaymentsHomeFragment extends LoggingFragment { @Override public void onViewRecoveryPhrase() { - SafeNavigation.safeNavigate(NavHostFragment.findNavController(PaymentsHomeFragment.this), R.id.action_paymentsHome_to_paymentsBackup); + showSaveRecoveryPhrase(); + } + + private void showSaveRecoveryPhrase() { + SafeNavigation.safeNavigate(NavHostFragment.findNavController(PaymentsHomeFragment.this), + PaymentsHomeFragmentDirections.actionPaymentsHomeToPaymentsBackup().setRecoveryPhraseState(RecoveryPhraseStates.FROM_INFO_CARD_WITH_MNEMONIC_NOT_CONFIRMED)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java index 9a47c96a4c..7cbde5424f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java @@ -188,7 +188,7 @@ public class PaymentsHomeViewModel extends ViewModel { return state.updatePayments(paymentItems, payments.size()); } - public void onInfoCardDismissed() { + public void updateStore() { store.update(s -> s); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/model/InfoCard.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/model/InfoCard.java index 13b46d15f0..bd5cb6c6c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/model/InfoCard.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/model/InfoCard.java @@ -78,10 +78,10 @@ public class InfoCard implements MappingModel { List infoCards = new ArrayList<>(Type.values().length); PaymentsValues paymentsValues = SignalStore.paymentsValues(); - if (paymentsValues.showRecoveryPhraseInfoCard()) { - infoCards.add(new InfoCard(R.string.payment_info_card_record_recovery_phrase, + if (!paymentsValues.isMnemonicConfirmed()) { + infoCards.add(new InfoCard(R.string.payment_info_card_save_recovery_phrase, R.string.payment_info_card_your_recovery_phrase_gives_you, - R.string.payment_info_card_record_your_phrase, + R.string.payment_info_card_save_your_phrase, R.drawable.ic_payments_info_card_restore_80, Type.RECORD_RECOVERY_PHASE, paymentsValues::dismissRecoveryPhraseInfoCard)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/viewholder/InfoCardViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/viewholder/InfoCardViewHolder.java index 05714809a3..48533e0833 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/viewholder/InfoCardViewHolder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/viewholder/InfoCardViewHolder.java @@ -55,7 +55,7 @@ public class InfoCardViewHolder extends MappingViewHolder { .setPositiveButton(R.string.payment_info_card_hide, (dialog, which) -> { model.dismiss(); dialog.dismiss(); - callbacks.onInfoCardDismissed(); + callbacks.onInfoCardDismissed(model.getType()); }) .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()) .show(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinPreferenceFragment.java index 66b71d393c..311ec4660f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinPreferenceFragment.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.preferences; import android.content.Intent; -import android.graphics.Color; import android.os.Bundle; import androidx.annotation.Nullable; @@ -79,7 +78,7 @@ public class AdvancedPinPreferenceFragment extends ListSummaryPreferenceFragment .setCancelable(true) .setPositiveButton(android.R.string.ok, (d, which) -> d.dismiss()) .show(); - } else if (!enabled && SignalStore.paymentsValues().mobileCoinPaymentsEnabled() && !SignalStore.paymentsValues().userConfirmedMnemonic()) { + } else if (!enabled && SignalStore.paymentsValues().mobileCoinPaymentsEnabled() && !SignalStore.paymentsValues().getUserConfirmedMnemonic()) { new AlertDialog.Builder(requireContext()) .setTitle(R.string.ApplicationPreferencesActivity_record_payments_recovery_phrase) .setMessage(R.string.ApplicationPreferencesActivity_before_you_can_disable_your_pin) diff --git a/app/src/main/res/navigation/payments_backup.xml b/app/src/main/res/navigation/payments_backup.xml index 184cccb046..c83939beab 100644 --- a/app/src/main/res/navigation/payments_backup.xml +++ b/app/src/main/res/navigation/payments_backup.xml @@ -17,8 +17,7 @@ app:enterAnim="@anim/fragment_open_enter" app:exitAnim="@anim/fragment_close_exit" app:popEnterAnim="@anim/fragment_close_enter" - app:popExitAnim="@anim/fragment_close_exit" - app:popUpTo="@id/paymentsHome" /> + app:popExitAnim="@anim/fragment_close_exit" /> + + + + + + + + + + + + + + + + + Payment failed Payment will continue processing Invalid recipient - - Unlock to Send Payment Failed to show payment lock @@ -3010,6 +3008,11 @@ This person has not activated payments Unable to request a network fee. To continue this payment tap okay to try again. + + + Signal + + %1$s at %2$s @@ -3744,9 +3747,11 @@ You can cash out MobileCoin anytime on an exchange that supports MobileCoin. Just make a transfer to your account at that exchange. Hide this card? Hide - Record recovery phrase + + Save recovery phrase Your recovery phrase gives you another way to restore your payments account. - Record your phrase + + Save your phrase Update your PIN With a high balance, you may want to update to an alphanumeric PIN to add more protection to your account. Update PIN @@ -3770,12 +3775,26 @@ Recovery phrase View recovery phrase + + Save recovery phrase Enter recovery phrase Your balance will automatically restore when you reinstall Signal if you confirm your Signal PIN. You can also restore your balance using a recovery phrase, which is a %1$d-word phrase unique to you. Write it down and store it in a safe place. + + You’ve got a balance! Time to save your recovery phrase—a 24-word key you can use to restore your balance. + + Time to save your recovery phrase—a 24-word key you can use to restore your balance. Learn more Your recovery phrase is a %1$d-word phrase unique to you. Use this phrase to restore your balance. Start Enter manually Paste from clipboard + + Continue Without Saving? + + Your recovery phrase lets you restore your balance in a worst-case scenario. We strongly recommend you save it. + + Skip Recovery Phrase + + Cancel Paste recovery phrase diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/payments/Money.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/payments/Money.java index c940c00c67..bdac371dc1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/payments/Money.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/payments/Money.java @@ -68,6 +68,8 @@ public abstract class Money { public abstract boolean isNegative(); + public abstract boolean isEqualOrLessThanZero(); + public abstract Money negate(); public abstract Money abs(); @@ -147,6 +149,11 @@ public abstract class Money { return amount.signum() == -1; } + @Override + public boolean isEqualOrLessThanZero() { + return amount != null && amount.compareTo(BigInteger.ZERO) <= 0; + } + @Override public MobileCoin negate() { return new MobileCoin(amount.negate());