mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add support for biometric auth for payments.
This commit is contained in:
@@ -14,8 +14,11 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
@@ -26,31 +29,42 @@ import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
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.payments.CanNotSendPaymentDialog;
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil;
|
||||
import org.thoughtcrime.securesms.payments.Payee;
|
||||
import org.thoughtcrime.securesms.payments.preferences.PaymentsHomeFragmentDirections;
|
||||
import org.thoughtcrime.securesms.payments.preferences.RecipientHasNotEnabledPaymentsDialog;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
import org.whispersystems.signalservice.api.payments.FormatterOptions;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ConfirmPaymentFragment extends BottomSheetDialogFragment {
|
||||
import kotlin.Unit;
|
||||
|
||||
private ConfirmPaymentViewModel viewModel;
|
||||
private final Runnable dismiss = () -> {
|
||||
public class ConfirmPaymentFragment extends BottomSheetDialogFragment {
|
||||
private static final String TAG = Log.tag(ConfirmPaymentFragment.class);
|
||||
private ConfirmPaymentViewModel viewModel;
|
||||
private ActivityResultLauncher<String> activityResultLauncher;
|
||||
private BiometricDeviceAuthentication biometricAuth;
|
||||
private final Runnable dismiss = () ->
|
||||
{
|
||||
dismissAllowingStateLoss();
|
||||
|
||||
if (ConfirmPaymentFragmentArgs.fromBundle(requireArguments()).getFinishOnConfirm()) {
|
||||
requireActivity().setResult(Activity.RESULT_OK);
|
||||
requireActivity().finish();
|
||||
} else {
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_directly_to_paymentsHome);
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), PaymentsHomeFragmentDirections.actionDirectlyToPaymentsHome(!isPaymentLockEnabled(requireContext())));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,6 +100,12 @@ public class ConfirmPaymentFragment extends BottomSheetDialogFragment {
|
||||
ConfirmPaymentAdapter adapter = new ConfirmPaymentAdapter(new Callbacks());
|
||||
list.setAdapter(adapter);
|
||||
|
||||
activityResultLauncher = registerForActivityResult(new BiometricDeviceLockContract(), result -> {
|
||||
if (result == BiometricDeviceAuthentication.AUTHENTICATED) {
|
||||
viewModel.confirmPayment();
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getState().observe(getViewLifecycleOwner(), state -> adapter.submitList(createList(state)));
|
||||
viewModel.isPaymentDone().observe(getViewLifecycleOwner(), isDone -> {
|
||||
if (isDone) {
|
||||
@@ -117,6 +137,16 @@ public class ConfirmPaymentFragment extends BottomSheetDialogFragment {
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo
|
||||
.Builder()
|
||||
.setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS)
|
||||
.setTitle(requireContext().getString(R.string.ConfirmPaymentFragment__unlock_to_send_payment))
|
||||
.setConfirmationRequired(false)
|
||||
.build();
|
||||
biometricAuth = new BiometricDeviceAuthentication(BiometricManager.from(requireActivity()),
|
||||
new BiometricPrompt(requireActivity(), new BiometricAuthenticationListener()),
|
||||
promptInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,6 +155,12 @@ public class ConfirmPaymentFragment extends BottomSheetDialogFragment {
|
||||
ThreadUtil.cancelRunnableOnMain(dismiss);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
biometricAuth.cancelAuthentication();
|
||||
}
|
||||
|
||||
private @NonNull MappingModelList createList(@NonNull ConfirmPaymentState state) {
|
||||
MappingModelList list = new MappingModelList();
|
||||
FormatterOptions options = FormatterOptions.defaults();
|
||||
@@ -170,11 +206,48 @@ public class ConfirmPaymentFragment extends BottomSheetDialogFragment {
|
||||
return spannable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private boolean isPaymentLockEnabled(Context context) {
|
||||
return SignalStore.paymentsValues().getPaymentLock() && ServiceUtil.getKeyguardManager(context).isKeyguardSecure();
|
||||
}
|
||||
|
||||
private class Callbacks implements ConfirmPaymentAdapter.Callbacks {
|
||||
|
||||
@Override
|
||||
public void onConfirmPayment() {
|
||||
setCancelable(false);
|
||||
if (isPaymentLockEnabled(requireContext())) {
|
||||
biometricAuth.authenticate(requireContext(), true, this::showConfirmDeviceCredentialIntent);
|
||||
} else {
|
||||
viewModel.confirmPayment();
|
||||
}
|
||||
}
|
||||
|
||||
public Unit showConfirmDeviceCredentialIntent() {
|
||||
activityResultLauncher.launch(getString(R.string.ConfirmPaymentFragment__unlock_to_send_payment));
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
private class BiometricAuthenticationListener extends BiometricPrompt.AuthenticationCallback {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errorString) {
|
||||
Log.w(TAG, "Authentication error: " + errorCode);
|
||||
if (errorCode != BiometricPrompt.ERROR_CANCELED && errorCode != BiometricPrompt.ERROR_USER_CANCELED) {
|
||||
onAuthenticationFailed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
Log.i(TAG, "onAuthenticationSucceeded");
|
||||
viewModel.confirmPayment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
Log.w(TAG, "Unable to authenticate payment lock");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ConfirmPaymentState {
|
||||
amount,
|
||||
note,
|
||||
amount.toZero(),
|
||||
FeeStatus.NOT_SET,
|
||||
FeeStatus.STILL_LOADING,
|
||||
null,
|
||||
Status.CONFIRM,
|
||||
null);
|
||||
|
||||
@@ -38,7 +38,11 @@ import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class PaymentsHomeFragment extends LoggingFragment {
|
||||
private static final int DAYS_UNTIL_REPROMPT_PAYMENT_LOCK = 30;
|
||||
private static final int MAX_PAYMENT_LOCK_SKIP_COUNT = 2;
|
||||
|
||||
private static final String TAG = Log.tag(PaymentsHomeFragment.class);
|
||||
|
||||
@@ -50,6 +54,34 @@ public class PaymentsHomeFragment extends LoggingFragment {
|
||||
super(R.layout.payments_home_fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
long paymentLockTimestamp = SignalStore.paymentsValues().getPaymentLockTimestamp();
|
||||
boolean enablePaymentLock = PaymentsHomeFragmentArgs.fromBundle(getArguments()).getEnablePaymentLock();
|
||||
boolean showPaymentLock = SignalStore.paymentsValues().getPaymentLockSkipCount() < MAX_PAYMENT_LOCK_SKIP_COUNT &&
|
||||
(System.currentTimeMillis() >= paymentLockTimestamp);
|
||||
|
||||
if (enablePaymentLock && showPaymentLock) {
|
||||
long waitUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(DAYS_UNTIL_REPROMPT_PAYMENT_LOCK);
|
||||
|
||||
SignalStore.paymentsValues().setPaymentLockTimestamp(waitUntil);
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.PaymentsHomeFragment__turn_on))
|
||||
.setMessage(getString(R.string.PaymentsHomeFragment__add_an_additional_layer))
|
||||
.setPositiveButton(R.string.PaymentsHomeFragment__enable, (dialog, which) ->
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), PaymentsHomeFragmentDirections.actionPaymentsHomeToPrivacySettings(true)))
|
||||
.setNegativeButton(R.string.PaymentsHomeFragment__not_now, (dialog, which) -> setSkipCount())
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void setSkipCount() {
|
||||
int skipCount = SignalStore.paymentsValues().getPaymentLockSkipCount();
|
||||
SignalStore.paymentsValues().setPaymentLockSkipCount(++skipCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
Toolbar toolbar = view.findViewById(R.id.payments_home_fragment_toolbar);
|
||||
|
||||
Reference in New Issue
Block a user