Add support for biometric auth for payments.
This commit is contained in:
parent
716229719a
commit
372f939a67
13 changed files with 390 additions and 100 deletions
|
@ -0,0 +1,80 @@
|
|||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.biometric.BiometricPrompt.PromptInfo
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
|
||||
/**
|
||||
* Authentication using phone biometric (face, fingerprint recognition) or device lock (pattern, pin or passphrase).
|
||||
*/
|
||||
class BiometricDeviceAuthentication(
|
||||
private val biometricManager: BiometricManager,
|
||||
private val biometricPrompt: BiometricPrompt,
|
||||
private val biometricPromptInfo: PromptInfo
|
||||
) {
|
||||
companion object {
|
||||
const val AUTHENTICATED = 1
|
||||
const val NOT_AUTHENTICATED = -1
|
||||
const val TAG: String = "BiometricDeviceAuth"
|
||||
const val BIOMETRIC_AUTHENTICATORS = BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
const val ALLOWED_AUTHENTICATORS = BIOMETRIC_AUTHENTICATORS or BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
}
|
||||
|
||||
fun authenticate(context: Context, force: Boolean, showConfirmDeviceCredentialIntent: () -> Unit): Boolean {
|
||||
val isKeyGuardSecure = ServiceUtil.getKeyguardManager(context).isKeyguardSecure
|
||||
|
||||
if (!isKeyGuardSecure) {
|
||||
Log.w(TAG, "Keyguard not secure...")
|
||||
return false
|
||||
}
|
||||
|
||||
return if (Build.VERSION.SDK_INT != 29 && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
if (force) {
|
||||
Log.i(TAG, "Listening for biometric authentication...")
|
||||
biometricPrompt.authenticate(biometricPromptInfo)
|
||||
} else {
|
||||
Log.i(TAG, "Skipping show system biometric or device lock dialog unless forced")
|
||||
}
|
||||
true
|
||||
} else if (force) {
|
||||
if (force) {
|
||||
Log.i(TAG, "firing intent...")
|
||||
showConfirmDeviceCredentialIntent()
|
||||
} else {
|
||||
Log.i(TAG, "Skipping firing intent unless forced")
|
||||
}
|
||||
true
|
||||
} else {
|
||||
Log.w(TAG, "Not compatible...")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAuthentication() {
|
||||
biometricPrompt.cancelAuthentication()
|
||||
}
|
||||
}
|
||||
|
||||
class BiometricDeviceLockContract : ActivityResultContract<String, Int>() {
|
||||
|
||||
@RequiresApi(api = 21)
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val keyguardManager = ServiceUtil.getKeyguardManager(context)
|
||||
return keyguardManager.createConfirmDeviceCredentialIntent(input, "")
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?) =
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
BiometricDeviceAuthentication.NOT_AUTHENTICATED
|
||||
} else {
|
||||
BiometricDeviceAuthentication.AUTHENTICATED
|
||||
}
|
||||
}
|
|
@ -47,7 +47,6 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricManager.Authenticators;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
|
@ -64,6 +63,8 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
|
|||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* Activity that prompts for a user's passphrase.
|
||||
*
|
||||
|
@ -72,8 +73,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|||
public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
|
||||
private static final String TAG = Log.tag(PassphrasePromptActivity.class);
|
||||
private static final int BIOMETRIC_AUTHENTICATORS = Authenticators.BIOMETRIC_STRONG | Authenticators.BIOMETRIC_WEAK;
|
||||
private static final int ALLOWED_AUTHENTICATORS = BIOMETRIC_AUTHENTICATORS | Authenticators.DEVICE_CREDENTIAL;
|
||||
private static final short AUTHENTICATE_REQUEST_CODE = 1007;
|
||||
private static final String BUNDLE_ALREADY_SHOWN = "bundle_already_shown";
|
||||
public static final String FROM_FOREGROUND = "from_foreground";
|
||||
|
@ -90,9 +89,9 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||
private ImageButton hideButton;
|
||||
private AnimatingToggle visibilityToggle;
|
||||
|
||||
private BiometricManager biometricManager;
|
||||
private BiometricPrompt biometricPrompt;
|
||||
private BiometricPrompt.PromptInfo biometricPromptInfo;
|
||||
private BiometricManager biometricManager;
|
||||
private BiometricPrompt biometricPrompt;
|
||||
private BiometricDeviceAuthentication biometricAuth;
|
||||
|
||||
private boolean authenticated;
|
||||
private boolean hadFailure;
|
||||
|
@ -249,12 +248,12 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
|
||||
biometricManager = BiometricManager.from(this);
|
||||
biometricPrompt = new BiometricPrompt(this, new BiometricAuthenticationListener());
|
||||
biometricPromptInfo = new BiometricPrompt.PromptInfo
|
||||
.Builder()
|
||||
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
|
||||
.setTitle(getString(R.string.PassphrasePromptActivity_unlock_signal))
|
||||
.build();
|
||||
|
||||
BiometricPrompt.PromptInfo biometricPromptInfo = new BiometricPrompt.PromptInfo
|
||||
.Builder()
|
||||
.setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS)
|
||||
.setTitle(getString(R.string.PassphrasePromptActivity_unlock_signal))
|
||||
.build();
|
||||
biometricAuth = new BiometricDeviceAuthentication(biometricManager, biometricPrompt, biometricPromptInfo);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setTitle("");
|
||||
|
||||
|
@ -279,7 +278,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||
private void setLockTypeVisibility() {
|
||||
if (TextSecurePreferences.isScreenLockEnabled(this)) {
|
||||
passphraseAuthContainer.setVisibility(View.GONE);
|
||||
fingerprintPrompt.setVisibility(biometricManager.canAuthenticate(BIOMETRIC_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS ? View.VISIBLE
|
||||
fingerprintPrompt.setVisibility(biometricManager.canAuthenticate(BiometricDeviceAuthentication.BIOMETRIC_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS ? View.VISIBLE
|
||||
: View.GONE);
|
||||
lockScreenButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
|
@ -290,33 +289,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||
}
|
||||
|
||||
private void resumeScreenLock(boolean force) {
|
||||
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
||||
|
||||
assert keyguardManager != null;
|
||||
|
||||
if (!keyguardManager.isKeyguardSecure()) {
|
||||
Log.w(TAG ,"Keyguard not secure...");
|
||||
handleAuthenticated();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT != 29 && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
if (force) {
|
||||
Log.i(TAG, "Listening for biometric authentication...");
|
||||
biometricPrompt.authenticate(biometricPromptInfo);
|
||||
} else {
|
||||
Log.i(TAG, "Skipping show system biometric dialog unless forced");
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (force) {
|
||||
Log.i(TAG, "firing intent...");
|
||||
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
|
||||
startActivityForResult(intent, AUTHENTICATE_REQUEST_CODE);
|
||||
} else {
|
||||
Log.i(TAG, "Skipping firing intent unless forced");
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Not compatible...");
|
||||
if (!biometricAuth.authenticate(getApplicationContext(), force, this::showConfirmDeviceCredentialIntent)) {
|
||||
handleAuthenticated();
|
||||
}
|
||||
}
|
||||
|
@ -332,6 +305,16 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||
body);
|
||||
}
|
||||
|
||||
public Unit showConfirmDeviceCredentialIntent() {
|
||||
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
||||
Intent intent = null;
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
|
||||
}
|
||||
startActivityForResult(intent, AUTHENTICATE_REQUEST_CODE);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
private class PassphraseActionListener implements TextView.OnEditorActionListener {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent keyEvent) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.privacy
|
|||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.TextAppearanceSpan
|
||||
|
@ -16,6 +17,7 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import mobi.upod.timedurationpicker.TimeDurationPicker
|
||||
|
@ -78,9 +80,15 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
|||
val repository = PrivacySettingsRepository()
|
||||
val factory = PrivacySettingsViewModel.Factory(sharedPreferences, repository)
|
||||
viewModel = ViewModelProvider(this, factory)[PrivacySettingsViewModel::class.java]
|
||||
val args: PrivacySettingsFragmentArgs by navArgs()
|
||||
var showPaymentLock = true
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
if (args.showPaymentLock && showPaymentLock) {
|
||||
showPaymentLock = false
|
||||
recyclerView?.scrollToPosition(adapter.itemCount - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,6 +312,23 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
|||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences_app_protection__payments)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__payment_lock),
|
||||
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__payment_lock_require_lock),
|
||||
isChecked = state.paymentLock && ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure,
|
||||
onClick = {
|
||||
if (!ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure) {
|
||||
showGoToPhoneSettings()
|
||||
} else {
|
||||
viewModel.togglePaymentLock()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__advanced),
|
||||
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__signal_message_and_calls),
|
||||
|
@ -314,6 +339,16 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
|||
}
|
||||
}
|
||||
|
||||
private fun showGoToPhoneSettings() {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(getString(R.string.PrivacySettingsFragment__cant_enable_title))
|
||||
setMessage(getString(R.string.PrivacySettingsFragment__cant_enable_description))
|
||||
setPositiveButton(R.string.PaymentsHomeFragment__enable) { _, _ -> startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL)) }
|
||||
setNegativeButton(R.string.PaymentsHomeFragment__not_now) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getScreenLockInactivityTimeoutSummary(timeoutSeconds: Long): String {
|
||||
val hours = TimeUnit.SECONDS.toHours(timeoutSeconds)
|
||||
val minutes = TimeUnit.SECONDS.toMinutes(timeoutSeconds) - hours * 60
|
||||
|
|
|
@ -12,6 +12,7 @@ data class PrivacySettingsState(
|
|||
val screenLockActivityTimeout: Long,
|
||||
val screenSecurity: Boolean,
|
||||
val incognitoKeyboard: Boolean,
|
||||
val paymentLock: Boolean,
|
||||
val isObsoletePasswordEnabled: Boolean,
|
||||
val isObsoletePasswordTimeoutEnabled: Boolean,
|
||||
val obsoletePasswordTimeout: Int,
|
||||
|
|
|
@ -74,6 +74,11 @@ class PrivacySettingsViewModel(
|
|||
refresh()
|
||||
}
|
||||
|
||||
fun togglePaymentLock() {
|
||||
SignalStore.paymentsValues().paymentLock = state.value?.let { !it.paymentLock } ?: false
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setObsoletePasswordTimeoutEnabled(enabled: Boolean) {
|
||||
sharedPreferences.edit().putBoolean(TextSecurePreferences.PASSPHRASE_TIMEOUT_PREF, enabled).apply()
|
||||
refresh()
|
||||
|
@ -97,6 +102,7 @@ class PrivacySettingsViewModel(
|
|||
screenLockActivityTimeout = TextSecurePreferences.getScreenLockTimeout(ApplicationDependencies.getApplication()),
|
||||
screenSecurity = TextSecurePreferences.isScreenSecurityEnabled(ApplicationDependencies.getApplication()),
|
||||
incognitoKeyboard = TextSecurePreferences.isIncognitoKeyboardEnabled(ApplicationDependencies.getApplication()),
|
||||
paymentLock = SignalStore.paymentsValues().paymentLock,
|
||||
seeMyPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode,
|
||||
findMeByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode,
|
||||
isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(ApplicationDependencies.getApplication()),
|
||||
|
|
|
@ -43,6 +43,9 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa
|
|||
private const val SHOW_CASHING_OUT_INFO_CARD = "mob_payments_show_cashing_out_info_card"
|
||||
private const val SHOW_RECOVERY_PHRASE_INFO_CARD = "mob_payments_show_recovery_phrase_info_card"
|
||||
private const val SHOW_UPDATE_PIN_INFO_CARD = "mob_payments_show_update_pin_info_card"
|
||||
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 val LARGE_BALANCE_THRESHOLD = Money.mobileCoin(BigDecimal.valueOf(500))
|
||||
|
||||
|
@ -50,6 +53,18 @@ 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)
|
||||
|
||||
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 } }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_privacySettingsFragment"
|
||||
app:destination="@id/privacySettingsFragment"
|
||||
app:destination="@id/privacy_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
|
@ -297,59 +297,8 @@
|
|||
</fragment>
|
||||
|
||||
<!-- region Privacy -->
|
||||
<fragment
|
||||
android:id="@+id/privacySettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.PrivacySettingsFragment"
|
||||
android:label="privacy_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_privacySettingsFragment_to_blockedUsersActivity"
|
||||
app:destination="@id/blockedUsersActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_privacySettingsFragment_to_advancedPrivacySettingsFragment"
|
||||
app:destination="@id/advancedPrivacySettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_privacySettingsFragment_to_disappearingMessagesTimerSelectFragment"
|
||||
app:destination="@id/app_settings_expire_timer"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_privacySettingsFragment_to_storyPrivacySettings"
|
||||
app:destination="@+id/story_privacy_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
|
||||
<argument
|
||||
android:name="title_id"
|
||||
app:argType="integer"
|
||||
app:nullable="false" />
|
||||
</action>
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
android:id="@+id/blockedUsersActivity"
|
||||
android:name="org.thoughtcrime.securesms.blocked.BlockedUsersActivity"
|
||||
android:label="blocked_users_activity" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/advancedPrivacySettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.advanced.AdvancedPrivacySettingsFragment"
|
||||
android:label="advanced_privacy_settings_fragment" />
|
||||
|
||||
<include app:graph="@navigation/app_settings_expire_timer" />
|
||||
<!-- endregion -->
|
||||
<include app:graph="@navigation/privacy_settings" />
|
||||
|
||||
<!-- region Data and Storage -->
|
||||
|
||||
|
@ -525,7 +474,7 @@
|
|||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_privacy"
|
||||
app:destination="@id/privacySettingsFragment"
|
||||
app:destination="@id/privacy_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
|
|
|
@ -10,6 +10,11 @@
|
|||
android:name="org.thoughtcrime.securesms.payments.preferences.PaymentsHomeFragment"
|
||||
tools:layout="@layout/payments_home_fragment">
|
||||
|
||||
<argument
|
||||
android:name="enable_payment_lock"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean"/>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_paymentsHome_to_paymentsAllActivity"
|
||||
app:destination="@id/paymentsAllActivity"
|
||||
|
@ -73,6 +78,20 @@
|
|||
|
||||
</action>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_paymentsHome_to_privacySettings"
|
||||
app:destination="@id/privacy_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
|
||||
<argument
|
||||
android:name="show_payment_lock"
|
||||
app:argType="boolean" />
|
||||
|
||||
</action>
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
@ -184,7 +203,13 @@
|
|||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/paymentsHome"
|
||||
app:popUpToInclusive="false" />
|
||||
app:popUpToInclusive="false">
|
||||
|
||||
<argument
|
||||
android:name="enable_payment_lock"
|
||||
app:argType="boolean"/>
|
||||
|
||||
</action>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_directly_to_paymentDetails"
|
||||
|
@ -216,4 +241,6 @@
|
|||
|
||||
<include app:graph="@navigation/payments_backup" />
|
||||
|
||||
<include app:graph="@navigation/privacy_settings" />
|
||||
|
||||
</navigation>
|
66
app/src/main/res/navigation/privacy_settings.xml
Normal file
66
app/src/main/res/navigation/privacy_settings.xml
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/privacy_settings"
|
||||
app:startDestination="@id/privacySettingsFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/privacySettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.PrivacySettingsFragment"
|
||||
android:label="privacy_settings_fragment">
|
||||
|
||||
<argument
|
||||
android:name="show_payment_lock"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_privacySettingsFragment_to_blockedUsersActivity"
|
||||
app:destination="@id/blockedUsersActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_privacySettingsFragment_to_advancedPrivacySettingsFragment"
|
||||
app:destination="@id/advancedPrivacySettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_privacySettingsFragment_to_disappearingMessagesTimerSelectFragment"
|
||||
app:destination="@id/app_settings_expire_timer"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_privacySettingsFragment_to_storyPrivacySettings"
|
||||
app:destination="@+id/story_privacy_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
|
||||
<argument
|
||||
android:name="title_id"
|
||||
app:argType="integer"
|
||||
app:nullable="false" />
|
||||
</action>
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
android:id="@+id/blockedUsersActivity"
|
||||
android:name="org.thoughtcrime.securesms.blocked.BlockedUsersActivity"
|
||||
android:label="blocked_users_activity" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/advancedPrivacySettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.advanced.AdvancedPrivacySettingsFragment"
|
||||
android:label="advanced_privacy_settings_fragment" />
|
||||
|
||||
<include app:graph="@navigation/app_settings_expire_timer" />
|
||||
|
||||
</navigation>
|
|
@ -2611,6 +2611,8 @@
|
|||
<string name="preferences__data_and_storage">Data and storage</string>
|
||||
<string name="preferences__storage">Storage</string>
|
||||
<string name="preferences__payments">Payments</string>
|
||||
<!-- Privacy settings payments section description -->
|
||||
<string name="preferences__payment_lock">Payment lock</string>
|
||||
<string name="preferences__payments_beta">Payments (Beta)</string>
|
||||
<string name="preferences__conversation_length_limit">Conversation length limit</string>
|
||||
<string name="preferences__keep_messages">Keep messages</string>
|
||||
|
@ -2677,6 +2679,8 @@
|
|||
<string name="preferences_app_protection__who_can">Who can…</string>
|
||||
<string name="preferences_app_protection__app_access">App access</string>
|
||||
<string name="preferences_app_protection__communication">Communication</string>
|
||||
<!-- Privacy settings payments section title -->
|
||||
<string name="preferences_app_protection__payments">Payments</string>
|
||||
<string name="preferences_chats__chats">Chats</string>
|
||||
<string name="preferences_data_and_storage__manage_storage">Manage storage</string>
|
||||
<string name="preferences_data_and_storage__calls">Calls</string>
|
||||
|
@ -2887,6 +2891,14 @@
|
|||
<string name="PaymentsHomeFragment__payments_not_available">Payments in Signal is no longer available. You can still transfer funds to an exchange but you can no longer send and receive payments or add funds.</string>
|
||||
|
||||
<string name="PaymentsHomeFragment__mobile_coin_terms_url" translatable="false">https://www.mobilecoin.com/terms-of-use.html</string>
|
||||
<!-- Alert dialog title which shows up after a payment to turn on payment lock -->
|
||||
<string name="PaymentsHomeFragment__turn_on">Turn on Payment Lock for future sends?</string>
|
||||
<!-- Alert dialog description for why payment lock should be enabled before sending payments -->
|
||||
<string name="PaymentsHomeFragment__add_an_additional_layer">Add an additional layer of security and require Android screen lock or fingerprint to transfer funds.</string>
|
||||
<!-- Alert dialog button to enable payment lock -->
|
||||
<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>
|
||||
|
||||
<!-- PaymentsAddMoneyFragment -->
|
||||
<string name="PaymentsAddMoneyFragment__add_funds">Add funds</string>
|
||||
|
@ -2978,6 +2990,8 @@
|
|||
<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>
|
||||
<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>
|
||||
|
||||
|
@ -3926,6 +3940,15 @@
|
|||
<string name="PrivacySettingsFragment__set_a_default_disappearing_message_timer_for_all_new_chats_started_by_you">Set a default disappearing message timer for all new chats started by you.</string>
|
||||
<!-- Summary for stories preference to launch into story privacy settings -->
|
||||
<string name="PrivacySettingsFragment__manage_your_stories">Manage your stories and who can view them</string>
|
||||
<string name="PrivacySettingsFragment__payment_lock_require_lock">Require Android screen lock or fingerprint to transfer funds</string>
|
||||
<!-- Alert dialog title when payment lock cannot be enabled -->
|
||||
<string name="PrivacySettingsFragment__cant_enable_title">Can\’t enable payment lock</string>
|
||||
<!-- Alert dialog description to setup screen lock or fingerprint in phone settings -->
|
||||
<string name="PrivacySettingsFragment__cant_enable_description">To use Payment Lock, you must first enable a screen lock or fingerprint ID in your phone’s settings.</string>
|
||||
<!-- Alert dialog button to go to phone settings -->
|
||||
<string name="PrivacySettingsFragment__go_to_settings">Go to settings</string>
|
||||
<!-- Alert dialog button to cancel the dialog -->
|
||||
<string name="PrivacySettingsFragment__cancel">Cancel</string>
|
||||
|
||||
<!-- AdvancedPrivacySettingsFragment -->
|
||||
<string name="AdvancedPrivacySettingsFragment__sealed_sender_link" translatable="false">https://signal.org/blog/sealed-sender</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue