Add support for biometric auth for payments.

This commit is contained in:
Varsha 2022-08-24 11:26:07 -07:00 committed by Greyson Parrelli
parent 716229719a
commit 372f939a67
13 changed files with 390 additions and 100 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,7 +30,7 @@ public class ConfirmPaymentState {
amount,
note,
amount.toZero(),
FeeStatus.NOT_SET,
FeeStatus.STILL_LOADING,
null,
Status.CONFIRM,
null);

View file

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

View file

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

View file

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

View 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>

View file

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