Prompt update on MobileCoin enclave failure.

This commit is contained in:
Varsha 2022-11-01 11:33:43 -07:00 committed by Cody Henthorne
parent 2709f0ee0d
commit b38ac44d0f
9 changed files with 181 additions and 19 deletions

View file

@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import android.view.View
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.PlayStoreUtil
/**
* Banner to update app to the latest version because of enclave failure
*/
class EnclaveFailureReminder(context: Context) : Reminder(null,
context.getString(R.string.EnclaveFailureReminder_update_signal)) {
init {
addAction(Action(context.getString(R.string.ExpiredBuildReminder_update_now), R.id.reminder_action_update_now))
okListener = View.OnClickListener { PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context) }
}
override fun isDismissable(): Boolean = false
override fun getImportance(): Importance {
return Importance.TERMINAL
}
}

View file

@ -68,6 +68,7 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa
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 enclaveFailure: MutableLiveData<Boolean> by lazy { MutableLiveData(false) }
private val liveMobileCoinLedger: MutableLiveData<MobileCoinLedgerWrapper> by lazy { MutableLiveData(mobileCoinLatestFullLedger()) }
private val liveMobileCoinBalance: LiveData<Balance> by lazy { Transformations.map(liveMobileCoinLedger) { obj: MobileCoinLedgerWrapper -> obj.balance } }
@ -214,6 +215,15 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa
return liveCurrentCurrency
}
fun setEnclaveFailure(failure: Boolean) {
enclaveFailure.postValue(failure)
}
fun enclaveFailure(): LiveData<Boolean> {
return enclaveFailure
}
fun showAboutMobileCoinInfoCard(): Boolean {
return store.getBoolean(SHOW_ABOUT_MOBILE_COIN_INFO_CARD, true)
}

View file

@ -198,7 +198,7 @@ public final class Wallet {
.setHighestBlock(MobileCoinLedger.Block.newBuilder()
.setBlockNumber(highestBlockIndex.longValue())
.setTimestamp(highestBlockTimeStamp));
SignalStore.paymentsValues().setEnclaveFailure(false);
return new MobileCoinLedgerWrapper(builder.build());
} catch (InvalidFogResponse e) {
Log.w(TAG, "Problem getting ledger", e);
@ -211,6 +211,7 @@ public final class Wallet {
}
throw new IOException(e);
} catch (AttestationException e) {
SignalStore.paymentsValues().setEnclaveFailure(true);
Log.w(TAG, "Attestation problem getting ledger", e);
throw new IOException(e);
} catch (Uint64RangeException e) {
@ -233,12 +234,18 @@ public final class Wallet {
BigInteger picoMob = amount.requireMobileCoin().toPicoMobBigInteger();
AccountSnapshot accountSnapshot = getCachedAccountSnapshot();
Amount minimumFee = getCachedMinimumTxFee();
Money.MobileCoin money;
if (accountSnapshot != null && minimumFee != null) {
return Money.picoMobileCoin(accountSnapshot.estimateTotalFee(Amount.ofMOB(picoMob), minimumFee).getValue());
money = Money.picoMobileCoin(accountSnapshot.estimateTotalFee(Amount.ofMOB(picoMob), minimumFee).getValue());
} else {
return Money.picoMobileCoin(mobileCoinClient.estimateTotalFee(Amount.ofMOB(picoMob)).getValue());
money = Money.picoMobileCoin(mobileCoinClient.estimateTotalFee(Amount.ofMOB(picoMob)).getValue());
}
} catch (InvalidFogResponse | AttestationException | InsufficientFundsException e) {
SignalStore.paymentsValues().setEnclaveFailure(false);
return money;
} catch (AttestationException e) {
SignalStore.paymentsValues().setEnclaveFailure(true);
return Money.MobileCoin.ZERO;
} catch (InvalidFogResponse | InsufficientFundsException e) {
Log.w(TAG, "Failed to get fee", e);
return Money.MobileCoin.ZERO;
} catch (NetworkException | FogSyncException e) {
@ -288,22 +295,31 @@ public final class Wallet {
try {
Receipt receipt = Receipt.fromBytes(receiptBytes);
Receipt.Status status = mobileCoinClient.getReceiptStatus(receipt);
ReceivedTransactionStatus txStatus = null;
switch (status) {
case UNKNOWN:
Log.w(TAG, "Unknown received Transaction Status");
return ReceivedTransactionStatus.inProgress();
txStatus = ReceivedTransactionStatus.inProgress();
break;
case FAILED:
return ReceivedTransactionStatus.failed();
txStatus = ReceivedTransactionStatus.failed();
break;
case RECEIVED:
final Amount amount = receipt.getAmountData(account);
return ReceivedTransactionStatus.complete(Money.picoMobileCoin(amount.getValue()), status.getBlockIndex().longValue());
txStatus = ReceivedTransactionStatus.complete(Money.picoMobileCoin(amount.getValue()), status.getBlockIndex().longValue());
break;
default:
throw new IllegalStateException("Unknown Transaction Status: " + status);
}
SignalStore.paymentsValues().setEnclaveFailure(false);
if (txStatus == null) throw new IllegalStateException("Unknown Transaction Status: " + status);
return txStatus;
} catch (SerializationException | InvalidFogResponse | InvalidReceiptException e) {
Log.w(TAG, e);
return ReceivedTransactionStatus.failed();
} catch (NetworkException | AttestationException e) {
} catch (NetworkException e) {
throw new IOException(e);
} catch (AttestationException e) {
SignalStore.paymentsValues().setEnclaveFailure(true);
throw new IOException(e);
} catch (AmountDecoderException e) {
Log.w(TAG, "Failed to decode amount", e);
@ -322,11 +338,16 @@ public final class Wallet {
if (defragmentFirst) {
try {
defragmentFees = defragment(amount, results);
SignalStore.paymentsValues().setEnclaveFailure(false);
} catch (InsufficientFundsException e) {
Log.w(TAG, "Insufficient funds", e);
results.add(TransactionSubmissionResult.failure(TransactionSubmissionResult.ErrorCode.INSUFFICIENT_FUNDS, true));
return;
} catch (TimeoutException | InvalidTransactionException | InvalidFogResponse | AttestationException | TransactionBuilderException | NetworkException | FogReportException | FogSyncException e) {
} catch (AttestationException e) {
results.add(TransactionSubmissionResult.failure(TransactionSubmissionResult.ErrorCode.GENERIC_FAILURE, true));
SignalStore.paymentsValues().setEnclaveFailure(true);
return;
} catch (TimeoutException | InvalidTransactionException | InvalidFogResponse | TransactionBuilderException | NetworkException | FogReportException | FogSyncException e) {
Log.w(TAG, "Defragment failed", e);
results.add(TransactionSubmissionResult.failure(TransactionSubmissionResult.ErrorCode.GENERIC_FAILURE, true));
return;
@ -358,6 +379,7 @@ public final class Wallet {
Amount.ofMOB(feeMobileCoin.toPicoMobBigInteger()),
TxOutMemoBuilder.createSenderAndDestinationRTHMemoBuilder(account));
}
SignalStore.paymentsValues().setEnclaveFailure(false);
} catch (InsufficientFundsException e) {
Log.w(TAG, "Insufficient funds", e);
results.add(TransactionSubmissionResult.failure(TransactionSubmissionResult.ErrorCode.INSUFFICIENT_FUNDS, false));
@ -378,6 +400,7 @@ public final class Wallet {
} catch (AttestationException e) {
Log.w(TAG, "Attestation problem", e);
results.add(TransactionSubmissionResult.failure(TransactionSubmissionResult.ErrorCode.GENERIC_FAILURE, false));
SignalStore.paymentsValues().setEnclaveFailure(true);
} catch (NetworkException e) {
Log.w(TAG, "Network problem", e);
results.add(TransactionSubmissionResult.failure(TransactionSubmissionResult.ErrorCode.GENERIC_FAILURE, false));
@ -399,6 +422,7 @@ public final class Wallet {
mobileCoinClient.submitTransaction(pendingTransaction.getTransaction());
Log.i(TAG, "Transaction submitted");
results.add(TransactionSubmissionResult.successfullySubmitted(new PaymentTransactionId.MobileCoin(pendingTransaction.getTransaction().toByteArray(), pendingTransaction.getReceipt().toByteArray(), feeMobileCoin)));
SignalStore.paymentsValues().setEnclaveFailure(false);
} catch (NetworkException e) {
Log.w(TAG, "Network problem", e);
results.add(TransactionSubmissionResult.failure(TransactionSubmissionResult.ErrorCode.NETWORK_FAILURE, false));
@ -408,6 +432,7 @@ public final class Wallet {
} catch (AttestationException e) {
Log.w(TAG, "Attestation problem", e);
results.add(TransactionSubmissionResult.failure(TransactionSubmissionResult.ErrorCode.GENERIC_FAILURE, false));
SignalStore.paymentsValues().setEnclaveFailure(true);
} catch (SerializationException e) {
Log.w(TAG, "Serialization problem", e);
results.add(TransactionSubmissionResult.failure(TransactionSubmissionResult.ErrorCode.GENERIC_FAILURE, false));

View file

@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.payments.FiatMoneyUtil;
import org.thoughtcrime.securesms.payments.MoneyView;
import org.thoughtcrime.securesms.payments.preferences.RecipientHasNotEnabledPaymentsDialog;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
@ -144,6 +145,17 @@ public class CreatePaymentFragment extends LoggingFragment {
viewModel.getNote().observe(getViewLifecycleOwner(), this::updateNote);
viewModel.getSpendableBalance().observe(getViewLifecycleOwner(), this::updateBalance);
viewModel.getCanSendPayment().observe(getViewLifecycleOwner(), this::updatePayAmountButtons);
viewModel.getEnclaveFailure().observe(getViewLifecycleOwner(), failure -> {
if (failure) {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.PaymentsHomeFragment__update_required))
.setMessage(getString(R.string.PaymentsHomeFragment__an_update_is_required))
.setPositiveButton(R.string.PaymentsHomeFragment__update_now, (dialog, which) -> { PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext()); })
.setNegativeButton(R.string.PaymentsHomeFragment__cancel, (dialog, which) -> {})
.setCancelable(false)
.show();
}
});
}
private void goBack(View v) {

View file

@ -45,6 +45,7 @@ public class CreatePaymentViewModel extends ViewModel {
private final LiveData<Boolean> isValidAmount;
private final Store<InputState> inputState;
private final LiveData<Boolean> isPaymentsSupportedByPayee;
private final LiveData<Boolean> enclaveFailure;
private final PayeeParcelable payee;
private final MutableLiveData<CharSequence> note;
@ -55,6 +56,7 @@ public class CreatePaymentViewModel extends ViewModel {
this.note = new MutableLiveData<>(note);
this.inputState = new Store<>(new InputState());
this.isValidAmount = LiveDataUtil.combineLatest(spendableBalance, inputState.getStateLiveData(), (b, s) -> validateAmount(b.requireMobileCoin(), s.getMoney().requireMobileCoin()));
this.enclaveFailure = LiveDataUtil.mapDistinct(SignalStore.paymentsValues().enclaveFailure(), isFailure -> isFailure);
if (payee.getPayee().hasRecipientId()) {
isPaymentsSupportedByPayee = LiveDataUtil.mapAsync(new DefaultValueLiveData<>(payee.getPayee().requireRecipientId()), r -> {
@ -92,6 +94,10 @@ public class CreatePaymentViewModel extends ViewModel {
inputState.update(liveExchangeRate, (rate, state) -> updateAmount(ApplicationDependencies.getApplication(), state.updateExchangeRate(rate), AmountKeyboardGlyph.NONE));
}
@NonNull LiveData<Boolean> getEnclaveFailure() {
return enclaveFailure;
}
@NonNull LiveData<InputState> getInputState() {
return inputState.getStateLiveData();
}

View file

@ -26,6 +26,8 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.PaymentPreferencesDirections;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.reminder.EnclaveFailureReminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.help.HelpFragment;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@ -37,8 +39,11 @@ import org.thoughtcrime.securesms.payments.backup.confirm.PaymentsRecoveryPhrase
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.PlayStoreUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import org.thoughtcrime.securesms.util.views.Stub;
import java.util.concurrent.TimeUnit;
@ -95,6 +100,7 @@ public class PaymentsHomeFragment extends LoggingFragment {
View sendMoney = view.findViewById(R.id.button_end_frame);
View refresh = view.findViewById(R.id.payments_home_fragment_header_refresh);
LottieAnimationView refreshAnimation = view.findViewById(R.id.payments_home_fragment_header_refresh_animation);
Stub<ReminderView> reminderView = ViewUtil.findStubById(view, R.id.reminder);
toolbar.setNavigationOnClickListener(v -> {
viewModel.markAllPaymentsSeen();
@ -104,14 +110,18 @@ public class PaymentsHomeFragment extends LoggingFragment {
toolbar.setOnMenuItemClickListener(this::onMenuItemSelected);
addMoney.setOnClickListener(v -> {
if (SignalStore.paymentsValues().getPaymentsAvailability().isSendAllowed()) {
if (viewModel.isEnclaveFailurePresent()) {
showUpdateIsRequiredDialog();
} else if (SignalStore.paymentsValues().getPaymentsAvailability().isSendAllowed()) {
SafeNavigation.safeNavigate(Navigation.findNavController(v), PaymentsHomeFragmentDirections.actionPaymentsHomeToPaymentsAddMoney());
} else {
showPaymentsDisabledDialog();
}
});
sendMoney.setOnClickListener(v -> {
if (SignalStore.paymentsValues().getPaymentsAvailability().isSendAllowed()) {
if (viewModel.isEnclaveFailurePresent()) {
showUpdateIsRequiredDialog();
} else if (SignalStore.paymentsValues().getPaymentsAvailability().isSendAllowed()) {
SafeNavigation.safeNavigate(Navigation.findNavController(v), PaymentsHomeFragmentDirections.actionPaymentsHomeToPaymentRecipientSelectionFragment());
} else {
showPaymentsDisabledDialog();
@ -246,6 +256,20 @@ public class PaymentsHomeFragment extends LoggingFragment {
}
});
viewModel.getEnclaveFailure().observe(getViewLifecycleOwner(), failure -> {
if (failure) {
showUpdateIsRequiredDialog();
reminderView.get().showReminder(new EnclaveFailureReminder(requireContext()));
reminderView.get().setOnActionClickListener(actionId -> {
if (actionId == R.id.reminder_action_update_now) {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
}
});
} else {
reminderView.get().requestDismiss();
}
});
requireActivity().getOnBackPressedDispatcher().addCallback(onBackPressed);
}
@ -261,9 +285,23 @@ public class PaymentsHomeFragment extends LoggingFragment {
onBackPressed.setEnabled(false);
}
private void showUpdateIsRequiredDialog() {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.PaymentsHomeFragment__update_required))
.setMessage(getString(R.string.PaymentsHomeFragment__an_update_is_required))
.setPositiveButton(R.string.PaymentsHomeFragment__update_now, (dialog, which) -> { PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext()); })
.setNegativeButton(R.string.PaymentsHomeFragment__cancel, (dialog, which) -> {})
.setCancelable(false)
.show();
}
private boolean onMenuItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.payments_home_fragment_menu_transfer_to_exchange) {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_paymentsHome_to_paymentsTransfer);
if (viewModel.isEnclaveFailurePresent()) {
showUpdateIsRequiredDialog();
} else {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_paymentsHome_to_paymentsTransfer);
}
return true;
} else if (item.getItemId() == R.id.payments_home_fragment_menu_set_currency) {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_paymentsHome_to_setCurrency);

View file

@ -51,6 +51,7 @@ public class PaymentsHomeViewModel extends ViewModel {
private final LiveData<FiatMoney> exchange;
private final SingleLiveEvent<PaymentStateEvent> paymentStateEvents;
private final SingleLiveEvent<ErrorEnabling> errorEnablingPayments;
private final LiveData<Boolean> enclaveFailure;
private final PaymentsHomeRepository paymentsHomeRepository;
private final CurrencyExchangeRepository currencyExchangeRepository;
@ -72,7 +73,7 @@ public class PaymentsHomeViewModel extends ViewModel {
this.exchangeLoadState = LiveDataUtil.mapDistinct(store.getStateLiveData(), PaymentsHomeState::getExchangeRateLoadState);
this.paymentStateEvents = new SingleLiveEvent<>();
this.errorEnablingPayments = new SingleLiveEvent<>();
this.enclaveFailure = LiveDataUtil.mapDistinct(SignalStore.paymentsValues().enclaveFailure(), isFailure -> isFailure);
this.store.update(paymentsRepository.getRecentPayments(), this::updateRecentPayments);
LiveData<CurrencyExchange.ExchangeRate> liveExchangeRate = LiveDataUtil.combineLatest(SignalStore.paymentsValues().liveCurrentCurrency(),
@ -109,6 +110,14 @@ public class PaymentsHomeViewModel extends ViewModel {
return errorEnablingPayments;
}
@NonNull LiveData<Boolean> getEnclaveFailure() {
return enclaveFailure;
}
@NonNull boolean isEnclaveFailurePresent() {
return Boolean.TRUE.equals(getEnclaveFailure().getValue());
}
@NonNull LiveData<MappingModelList> getList() {
return list;
}

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -13,22 +14,44 @@
android:layout_height="@dimen/signal_m3_toolbar_height"
android:minHeight="@dimen/signal_m3_toolbar_height"
android:theme="?actionBarStyle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_arrow_left_24"
app:title="@string/preferences__payments_beta"
app:titleTextAppearance="@style/Signal.Text.TitleLarge" />
<ViewStub
android:id="@+id/reminder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/reminder"
android:layout="@layout/conversation_list_reminder_view"
app:layout_constraintTop_toBottomOf="@id/payments_home_fragment_toolbar" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/banner_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="reminder" />
<include
layout="@layout/payments_home_fragment_header"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/banner_barrier" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/payments_home_fragment_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
android:scrollIndicators="top|bottom"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/payments_home_fragment_header" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -2155,6 +2155,12 @@
<string name="UnauthorizedReminder_device_no_longer_registered">Device no longer registered</string>
<string name="UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device">This is likely because you registered your phone number with Signal on a different device. Tap to re-register.</string>
<!-- EnclaveFailureReminder -->
<!-- Banner message to update app to use payments -->
<string name="EnclaveFailureReminder_update_signal">Update Signal to continue using payments. Your balance may not be up-to-date.</string>
<!-- Banner button to update now -->
<string name="EnclaveFailureReminder_update_now">Update now</string>
<!-- WebRtcCallActivity -->
<string name="WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone">To answer the call, give Signal access to your microphone.</string>
<string name="WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone">To answer the call from %s, give Signal access to your microphone.</string>
@ -3018,6 +3024,14 @@
<string name="PaymentsHomeFragment__enable">Turn On</string>
<!-- Alert dialog button to not enable payment lock for now -->
<string name="PaymentsHomeFragment__not_now">Not Now</string>
<!-- Alert dialog title which shows up to update app to send payments -->
<string name="PaymentsHomeFragment__update_required">Update required</string>
<!-- Alert dialog description that app update is required to send payments-->
<string name="PaymentsHomeFragment__an_update_is_required">An update is required to continue sending and receiving payments, and to view your up-to-date payment balance.</string>
<!-- Alert dialog button to cancel -->
<string name="PaymentsHomeFragment__cancel">Cancel</string>
<!-- Alert dialog button to update now -->
<string name="PaymentsHomeFragment__update_now">Update now</string>
<!-- PaymentsSecuritySetupFragment -->
<!-- Toolbar title -->