diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/EnclaveFailureReminder.kt b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/EnclaveFailureReminder.kt new file mode 100644 index 000000000..40f5f3f08 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/EnclaveFailureReminder.kt @@ -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 + } +} 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 fd5a328da..534eea852 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt @@ -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 by lazy { MutableLiveData(currentCurrency()) } + private val enclaveFailure: MutableLiveData by lazy { MutableLiveData(false) } private val liveMobileCoinLedger: MutableLiveData by lazy { MutableLiveData(mobileCoinLatestFullLedger()) } private val liveMobileCoinBalance: LiveData 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 { + return enclaveFailure + } + + fun showAboutMobileCoinInfoCard(): Boolean { return store.getBoolean(SHOW_ABOUT_MOBILE_COIN_INFO_CARD, true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/Wallet.java b/app/src/main/java/org/thoughtcrime/securesms/payments/Wallet.java index bd956198e..85f16fd08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/Wallet.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/Wallet.java @@ -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)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/create/CreatePaymentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/create/CreatePaymentFragment.java index 5030bab0b..8ae933154 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/create/CreatePaymentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/create/CreatePaymentFragment.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/create/CreatePaymentViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/payments/create/CreatePaymentViewModel.java index 1c24d3d85..c127ce5b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/create/CreatePaymentViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/create/CreatePaymentViewModel.java @@ -45,6 +45,7 @@ public class CreatePaymentViewModel extends ViewModel { private final LiveData isValidAmount; private final Store inputState; private final LiveData isPaymentsSupportedByPayee; + private final LiveData enclaveFailure; private final PayeeParcelable payee; private final MutableLiveData 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 getEnclaveFailure() { + return enclaveFailure; + } + @NonNull LiveData getInputState() { return inputState.getStateLiveData(); } 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 22e162d93..573b5d4e3 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 @@ -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 = 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); 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 66bc21d47..7b5f0ac6b 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 @@ -51,6 +51,7 @@ public class PaymentsHomeViewModel extends ViewModel { private final LiveData exchange; private final SingleLiveEvent paymentStateEvents; private final SingleLiveEvent errorEnablingPayments; + private final LiveData 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 liveExchangeRate = LiveDataUtil.combineLatest(SignalStore.paymentsValues().liveCurrentCurrency(), @@ -109,6 +110,14 @@ public class PaymentsHomeViewModel extends ViewModel { return errorEnablingPayments; } + @NonNull LiveData getEnclaveFailure() { + return enclaveFailure; + } + + @NonNull boolean isEnclaveFailurePresent() { + return Boolean.TRUE.equals(getEnclaveFailure().getValue()); + } + @NonNull LiveData getList() { return list; } diff --git a/app/src/main/res/layout/payments_home_fragment.xml b/app/src/main/res/layout/payments_home_fragment.xml index 5c211f652..df2ea455d 100644 --- a/app/src/main/res/layout/payments_home_fragment.xml +++ b/app/src/main/res/layout/payments_home_fragment.xml @@ -1,5 +1,6 @@ - + + + + + android:layout_height="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/banner_barrier" /> + 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" /> - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96599a797..b3c117dbb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2155,6 +2155,12 @@ Device no longer registered This is likely because you registered your phone number with Signal on a different device. Tap to re-register. + + + Update Signal to continue using payments. Your balance may not be up-to-date. + + Update now + To answer the call, give Signal access to your microphone. To answer the call from %s, give Signal access to your microphone. @@ -3018,6 +3024,14 @@ Turn On Not Now + + Update required + + An update is required to continue sending and receiving payments, and to view your up-to-date payment balance. + + Cancel + + Update now