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.
fork-5.53.8
Varsha 2022-09-21 11:15:49 -07:00 zatwierdzone przez Cody Henthorne
rodzic c6bfdeb4b0
commit e676f324f1
16 zmienionych plików z 308 dodań i 76 usunięć

Wyświetl plik

@ -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<Currency> by lazy { MutableLiveData(currentCurrency()) }
private val liveMobileCoinLedger: MutableLiveData<MobileCoinLedgerWrapper> by lazy { MutableLiveData(mobileCoinLatestFullLedger()) }
private val liveMobileCoinBalance: LiveData<Balance> 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
}
}

Wyświetl plik

@ -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<String> 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");
}
}
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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);
}
});

Wyświetl plik

@ -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);
}
});

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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() {}
}

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -78,10 +78,10 @@ public class InfoCard implements MappingModel<InfoCard> {
List<InfoCard> 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));

Wyświetl plik

@ -55,7 +55,7 @@ public class InfoCardViewHolder extends MappingViewHolder<InfoCard> {
.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();

Wyświetl plik

@ -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)

Wyświetl plik

@ -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" />
<action
android:id="@+id/action_paymentsRecoveryStart_to_paymentsRecoveryEntry"
@ -46,6 +45,27 @@
android:defaultValue="false"
app:argType="boolean"
app:nullable="false" />
<argument
android:name="recovery_phrase_state"
android:defaultValue="NONE"
app:argType="org.thoughtcrime.securesms.payments.backup.RecoveryPhraseStates" />
<argument
android:name="first_time_non_zero_balance"
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="info_card"
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="payment_menu"
android:defaultValue="false"
app:argType="boolean" />
</fragment>
<fragment

Wyświetl plik

@ -76,6 +76,26 @@
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="recovery_phrase_state"
android:defaultValue="NONE"
app:argType="org.thoughtcrime.securesms.payments.backup.RecoveryPhraseStates" />
<argument
android:name="first_time_non_zero_balance"
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="info_card"
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="payment_menu"
android:defaultValue="false"
app:argType="boolean" />
</action>
<action

Wyświetl plik

@ -2999,8 +2999,6 @@
<string name="ConfirmPayment__payment_failed">Payment failed</string>
<string name="ConfirmPayment__payment_will_continue_processing">Payment will continue processing</string>
<string name="ConfirmPaymentFragment__invalid_recipient">Invalid recipient</string>
<!-- Biometric/Device authentication prompt title which comes up before sending a payment -->
<string name="ConfirmPaymentFragment__unlock_to_send_payment">Unlock to Send Payment</string>
<!-- Title of a dialog show when we were unable to present the user\'s screenlock before sending a payment -->
<string name="ConfirmPaymentFragment__failed_to_show_payment_lock">Failed to show payment lock</string>
<!-- Body of a dialog show when we were unable to present the user\'s screenlock before sending a payment -->
@ -3010,6 +3008,11 @@
<string name="ConfirmPaymentFragment__this_person_has_not_activated_payments">This person has not activated payments</string>
<string name="ConfirmPaymentFragment__unable_to_request_a_network_fee">Unable to request a network fee. To continue this payment tap okay to try again.</string>
<!-- BiometricDeviceAuthentication -->
<!-- Biometric/Device authentication prompt title -->
<string name="BiometricDeviceAuthentication__signal">Signal</string>
<!-- CurrencyAmountFormatter_s_at_s -->
<string name="CurrencyAmountFormatter_s_at_s">%1$s at %2$s</string>
@ -3744,9 +3747,11 @@
<string name="payment_info_card_you_can_cash_out_mobilecoin">You can cash out MobileCoin anytime on an exchange that supports MobileCoin. Just make a transfer to your account at that exchange.</string>
<string name="payment_info_card_hide_this_card">Hide this card?</string>
<string name="payment_info_card_hide">Hide</string>
<string name="payment_info_card_record_recovery_phrase">Record recovery phrase</string>
<!-- Title of save recovery phrase card -->
<string name="payment_info_card_save_recovery_phrase">Save recovery phrase</string>
<string name="payment_info_card_your_recovery_phrase_gives_you">Your recovery phrase gives you another way to restore your payments account.</string>
<string name="payment_info_card_record_your_phrase">Record your phrase</string>
<!-- Button in save recovery phrase card -->
<string name="payment_info_card_save_your_phrase">Save your phrase</string>
<string name="payment_info_card_update_your_pin">Update your PIN</string>
<string name="payment_info_card_with_a_high_balance">With a high balance, you may want to update to an alphanumeric PIN to add more protection to your account.</string>
<string name="payment_info_card_update_pin">Update PIN</string>
@ -3770,12 +3775,26 @@
<!-- PaymentsRecoveryStartFragment -->
<string name="PaymentsRecoveryStartFragment__recovery_phrase">Recovery phrase</string>
<string name="PaymentsRecoveryStartFragment__view_recovery_phrase">View recovery phrase</string>
<!-- Title in save recovery phrase screen -->
<string name="PaymentsRecoveryStartFragment__save_recovery_phrase">Save recovery phrase</string>
<string name="PaymentsRecoveryStartFragment__enter_recovery_phrase">Enter recovery phrase</string>
<string name="PaymentsRecoveryStartFragment__your_balance_will_automatically_restore">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.</string>
<!-- Description in save recovery phrase screen which shows up when user has non zero balance -->
<string name="PaymentsRecoveryStartFragment__got_balance">Youve got a balance! Time to save your recovery phrase—a 24-word key you can use to restore your balance.</string>
<!-- Description in save recovery phrase screen which shows up when user navigates from info card -->
<string name="PaymentsRecoveryStartFragment__time_to_save">Time to save your recovery phrase—a 24-word key you can use to restore your balance. Learn more</string>
<string name="PaymentsRecoveryStartFragment__your_recovery_phrase_is_a">Your recovery phrase is a %1$d-word phrase unique to you. Use this phrase to restore your balance.</string>
<string name="PaymentsRecoveryStartFragment__start">Start</string>
<string name="PaymentsRecoveryStartFragment__enter_manually">Enter manually</string>
<string name="PaymentsRecoveryStartFragment__paste_from_clipboard">Paste from clipboard</string>
<!-- Alert dialog title which asks before going back if user wants to save recovery phrase -->
<string name="PaymentsRecoveryStartFragment__continue_without_saving">Continue Without Saving?</string>
<!-- Alert dialog description to let user know why recovery phrase needs to be saved -->
<string name="PaymentsRecoveryStartFragment__your_recovery_phrase">Your recovery phrase lets you restore your balance in a worst-case scenario. We strongly recommend you save it.</string>
<!-- Alert dialog option to skip recovery phrase -->
<string name="PaymentsRecoveryStartFragment__skip_recovery_phrase">Skip Recovery Phrase</string>
<!-- Alert dialog option to cancel dialog-->
<string name="PaymentsRecoveryStartFragment__cancel">Cancel</string>
<!-- PaymentsRecoveryPasteFragment -->
<string name="PaymentsRecoveryPasteFragment__paste_recovery_phrase">Paste recovery phrase</string>

Wyświetl plik

@ -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());