Add support for pin entry sad paths.

This commit is contained in:
Cody Henthorne
2023-02-22 19:18:44 -05:00
committed by Greyson Parrelli
parent afb9b76208
commit 62414e72b5
7 changed files with 277 additions and 59 deletions

View File

@@ -132,6 +132,11 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
controller.prepopulateCountryCode();
controller.setNumberAndCountryCode(viewModel.getNumber());
showKeyboard(number.getEditText());
if (viewModel.hasUserSkippedReRegisterFlow() && viewModel.shouldAutoShowSmsConfirmDialog()) {
viewModel.setAutoShowSmsConfirmDialog(false);
ThreadUtil.runOnMainDelayed(() -> handleRegister(requireContext()), 250);
}
}
private void showKeyboard(View viewToFocus) {

View File

@@ -7,45 +7,59 @@ import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding
import org.thoughtcrime.securesms.databinding.PinRestoreEntryFragmentBinding
import org.thoughtcrime.securesms.lock.v2.KbsConstants
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor
import org.thoughtcrime.securesms.registration.viewmodel.ReRegisterWithPinViewModel
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Using a recovery password or restored KBS token attempt to register in the skip flow.
*/
class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration_lock) {
class ReRegisterWithPinFragment : LoggingFragment(R.layout.pin_restore_entry_fragment) {
companion object {
private val TAG = Log.tag(ReRegisterWithPinFragment::class.java)
}
private var _binding: FragmentRegistrationLockBinding? = null
private val binding: FragmentRegistrationLockBinding
private var _binding: PinRestoreEntryFragmentBinding? = null
private val binding: PinRestoreEntryFragmentBinding
get() = _binding!!
private val viewModel: RegistrationViewModel by activityViewModels()
private val registrationViewModel: RegistrationViewModel by activityViewModels()
private val reRegisterViewModel: ReRegisterWithPinViewModel by viewModels()
private val disposables = LifecycleDisposable()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentRegistrationLockBinding.bind(view)
_binding = PinRestoreEntryFragmentBinding.bind(view)
disposables.bindTo(viewLifecycleOwner.lifecycle)
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.kbsLockPinTitle)
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.pinRestorePinTitle)
binding.kbsLockForgotPin.visibility = View.GONE
binding.pinRestorePinDescription.setText(R.string.RegistrationLockFragment__enter_the_pin_you_created_for_your_account)
binding.kbsLockPinInput.imeOptions = EditorInfo.IME_ACTION_DONE
binding.kbsLockPinInput.setOnEditorActionListener { v, actionId, _ ->
binding.pinRestoreForgotPin.visibility = View.GONE
binding.pinRestoreForgotPin.setOnClickListener { onNeedHelpClicked() }
binding.pinRestoreSkipButton.setOnClickListener { onSkipClicked() }
binding.pinRestorePinInput.imeOptions = EditorInfo.IME_ACTION_DONE
binding.pinRestorePinInput.setOnEditorActionListener { v, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
ViewUtil.hideKeyboard(requireContext(), v!!)
handlePinEntry()
@@ -56,25 +70,31 @@ class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration
enableAndFocusPinEntry()
binding.kbsLockPinConfirm.setOnClickListener {
ViewUtil.hideKeyboard(requireContext(), binding.kbsLockPinInput)
binding.pinRestorePinConfirm.setOnClickListener {
handlePinEntry()
}
binding.kbsLockKeyboardToggle.setOnClickListener { v: View? ->
binding.pinRestoreKeyboardToggle.setOnClickListener {
val keyboardType: PinKeyboardType = getPinEntryKeyboardType()
updateKeyboard(keyboardType.other)
binding.kbsLockKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType))
binding.pinRestoreKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType))
}
val keyboardType: PinKeyboardType = getPinEntryKeyboardType().other
binding.kbsLockKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType))
binding.pinRestoreKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType))
reRegisterViewModel.updateTokenData(registrationViewModel.keyBackupCurrentToken)
disposables += reRegisterViewModel.triesRemaining.subscribe(this::updateTriesRemaining)
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
private fun handlePinEntry() {
binding.kbsLockPinInput.isEnabled = false
val pin: String? = binding.kbsLockPinInput.text?.toString()
val pin: String? = binding.pinRestorePinInput.text?.toString()
val trimmedLength = pin?.replace(" ", "")?.length ?: 0
if (trimmedLength == 0) {
@@ -89,39 +109,107 @@ class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration
return
}
binding.kbsLockPinConfirm.setSpinning()
disposables += viewModel.verifyReRegisterWithPin(pin!!)
.subscribe { p ->
if (p.hasResult()) {
disposables += registrationViewModel.verifyReRegisterWithPin(pin!!)
.doOnSubscribe {
ViewUtil.hideKeyboard(requireContext(), binding.pinRestorePinInput)
binding.pinRestorePinInput.isEnabled = false
binding.pinRestorePinConfirm.setSpinning()
}
.doAfterTerminate {
binding.pinRestorePinInput.isEnabled = true
binding.pinRestorePinConfirm.cancelSpinning()
}
.subscribe { processor ->
if (processor.hasResult()) {
Log.i(TAG, "Successfully re-registered via skip flow")
findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment)
return@subscribe
}
reRegisterViewModel.hasIncorrectGuess = true
if (processor is VerifyResponseWithRegistrationLockProcessor && processor.wrongPin()) {
reRegisterViewModel.updateTokenData(processor.tokenData)
if (processor.tokenData != null) {
registrationViewModel.setKeyBackupTokenData(processor.tokenData)
}
return@subscribe
} else if (processor.isKbsLocked()) {
Log.w(TAG, "Unable to continue skip flow, KBS is locked")
onAccountLocked()
} else if (processor.isServerSentError()) {
Log.i(TAG, "Error from server, not likely recoverable", processor.error)
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show()
} else {
Log.w(TAG, "Unable to continue skip flow, resuming normal flow", p.error)
// todo handle the various error conditions
Toast.makeText(requireContext(), "retry or nav TODO ERROR See log", Toast.LENGTH_SHORT).show()
binding.kbsLockPinInput.isEnabled = true
Log.i(TAG, "Unexpected error occurred", processor.error)
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show()
}
}
}
private fun updateTriesRemaining(triesRemaining: Int) {
if (reRegisterViewModel.hasIncorrectGuess) {
if (triesRemaining == 1 && !reRegisterViewModel.isLocalVerification) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
.setPositiveButton(android.R.string.ok, null)
.show()
}
if (triesRemaining > 5) {
binding.pinRestorePinInputLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin)
} else {
binding.pinRestorePinInputLabel.text = resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining)
}
binding.pinRestoreForgotPin.visibility = View.VISIBLE
} else {
if (triesRemaining == 1) {
binding.pinRestoreForgotPin.visibility = View.VISIBLE
if (!reRegisterViewModel.isLocalVerification) {
MaterialAlertDialogBuilder(requireContext())
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
}
if (triesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.")
onAccountLocked()
}
}
private fun onAccountLocked() {
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_out_of_guesses_local else R.string.PinRestoreLockedFragment_youve_run_out_of_pin_guesses
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.ReRegisterWithPinFragment_send_sms_code) { _, _ -> onSkipPinEntry() }
.setNegativeButton(R.string.AccountLockedFragment__learn_more) { _, _ -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url)) }
.show()
}
private fun enableAndFocusPinEntry() {
binding.kbsLockPinInput.isEnabled = true
binding.kbsLockPinInput.isFocusable = true
if (binding.kbsLockPinInput.requestFocus()) {
ServiceUtil.getInputMethodManager(binding.kbsLockPinInput.context).showSoftInput(binding.kbsLockPinInput, 0)
binding.pinRestorePinInput.isEnabled = true
binding.pinRestorePinInput.isFocusable = true
if (binding.pinRestorePinInput.requestFocus()) {
ServiceUtil.getInputMethodManager(binding.pinRestorePinInput.context).showSoftInput(binding.pinRestorePinInput, 0)
}
}
private fun getPinEntryKeyboardType(): PinKeyboardType {
val isNumeric = binding.kbsLockPinInput.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_CLASS_NUMBER
val isNumeric = binding.pinRestorePinInput.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_CLASS_NUMBER
return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC
}
private fun updateKeyboard(keyboard: PinKeyboardType) {
val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC
binding.kbsLockPinInput.inputType = if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
binding.kbsLockPinInput.text.clear()
binding.pinRestorePinInput.inputType = if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
binding.pinRestorePinInput.text?.clear()
}
@StringRes
@@ -132,4 +220,41 @@ class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration
R.string.RegistrationLockFragment__enter_numeric_pin
}
}
private fun onNeedHelpClicked() {
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_need_help_local else R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_need_help)
.setMessage(getString(message, KbsConstants.MINIMUM_PIN_LENGTH))
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ ->
val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.ReRegisterWithPinFragment_support_email_subject, null, null)
CommunicationActions.openEmail(
requireContext(),
SupportEmailUtil.getSupportEmailAddress(requireContext()),
getString(R.string.ReRegisterWithPinFragment_support_email_subject),
body
)
}
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
.show()
}
private fun onSkipClicked() {
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_skip_local else R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry)
.setMessage(message)
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
.show()
}
private fun onSkipPinEntry() {
registrationViewModel.setUserSkippedReRegisterFlow(true)
findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_enterPhoneNumberFragment)
}
}

View File

@@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.registration.viewmodel
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.BehaviorSubject
import org.thoughtcrime.securesms.pin.TokenData
/**
* Used during re-registration flow when pin entry is required to skip SMS verification. Mostly tracks
* guesses remaining in both the local and remote check flows.
*/
class ReRegisterWithPinViewModel : ViewModel() {
var isLocalVerification: Boolean = false
private set
var hasIncorrectGuess: Boolean = false
private val _triesRemaining: BehaviorSubject<Int> = BehaviorSubject.createDefault(10)
val triesRemaining: Observable<Int> = _triesRemaining.observeOn(AndroidSchedulers.mainThread())
fun updateTokenData(tokenData: TokenData?) {
if (tokenData == null) {
isLocalVerification = true
if (hasIncorrectGuess) {
_triesRemaining.onNext((_triesRemaining.value!! - 1).coerceAtLeast(0))
}
} else {
_triesRemaining.onNext(tokenData.triesRemaining)
}
}
}

View File

@@ -33,6 +33,7 @@ import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.push.exceptions.IncorrectCodeException;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse;
import java.io.IOException;
@@ -54,6 +55,9 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
private final RegistrationRepository registrationRepository;
private boolean userSkippedReRegisterFlow = false;
private boolean autoShowSmsConfirmDialog = false;
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle,
boolean isReregister,
@NonNull VerifyAccountRepository verifyAccountRepository,
@@ -113,6 +117,25 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
return completed != null ? completed : false;
}
public boolean hasUserSkippedReRegisterFlow() {
return userSkippedReRegisterFlow;
}
public void setUserSkippedReRegisterFlow(boolean userSkippedReRegisterFlow) {
this.userSkippedReRegisterFlow = userSkippedReRegisterFlow;
if (userSkippedReRegisterFlow) {
setAutoShowSmsConfirmDialog(true);
}
}
public boolean shouldAutoShowSmsConfirmDialog() {
return autoShowSmsConfirmDialog;
}
public void setAutoShowSmsConfirmDialog(boolean autoShowSmsConfirmDialog) {
this.autoShowSmsConfirmDialog = autoShowSmsConfirmDialog;
}
@Override
protected Single<ServiceResponse<VerifyResponse>> verifyAccountWithoutRegistrationLock() {
final String sessionId = getSessionId();
@@ -216,7 +239,18 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
throw new IllegalStateException("Unable to get token or master key");
}
})
.onErrorReturn(t -> new VerifyResponseWithoutKbs(ServiceResponse.forUnknownError(t)))
.onErrorReturn(t -> new VerifyResponseWithRegistrationLockProcessor(ServiceResponse.forUnknownError(t), getKeyBackupCurrentToken()))
.map(p -> {
if (p instanceof VerifyResponseWithRegistrationLockProcessor) {
VerifyResponseWithRegistrationLockProcessor lockProcessor = (VerifyResponseWithRegistrationLockProcessor) p;
if (lockProcessor.wrongPin() && lockProcessor.getTokenData() != null) {
TokenData newToken = TokenData.withResponse(lockProcessor.getTokenData(), lockProcessor.getTokenResponse());
return new VerifyResponseWithRegistrationLockProcessor(lockProcessor.getResponse(), newToken);
}
}
return p;
})
.doOnSuccess(p -> {
if (p.hasResult()) {
restoreFromStorageService();
@@ -231,9 +265,13 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
{
String localPinHash = SignalStore.kbsValues().getLocalPinHash();
if (hasRecoveryPassword() && localPinHash != null && PinHashing.verifyLocalPinHash(localPinHash, pin)) {
Log.i(TAG, "Local pin matches input, attempting registration");
return ReRegistrationData.canProceed(new KbsPinData(SignalStore.kbsValues().getOrCreateMasterKey(), SignalStore.kbsValues().getRegistrationLockTokenResponse()));
if (hasRecoveryPassword() && localPinHash != null) {
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
Log.i(TAG, "Local pin matches input, attempting registration");
return ReRegistrationData.canProceed(new KbsPinData(SignalStore.kbsValues().getOrCreateMasterKey(), SignalStore.kbsValues().getRegistrationLockTokenResponse()));
} else {
throw new KeyBackupSystemWrongPinException(new TokenResponse(null, null, 0));
}
} else {
TokenData data = getKeyBackupCurrentToken();
if (data == null) {
@@ -248,6 +286,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
}
setRecoveryPassword(kbsPinData.getMasterKey().deriveRegistrationRecoveryPassword());
setKeyBackupTokenData(data);
return ReRegistrationData.canProceed(kbsPinData);
}
}
@@ -270,7 +309,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
return Single.just(processor);
}
})
.<VerifyResponseProcessor>flatMap(processor -> {
.flatMap(processor -> {
if (processor.hasResult()) {
VerifyResponse verifyResponse = processor.getResult();
boolean setRegistrationLockEnabled = verifyResponse.getKbsData() != null;
@@ -280,10 +319,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
}
return registrationRepository.registerAccount(registrationData, verifyResponse, setRegistrationLockEnabled)
.map(r -> {
return setRegistrationLockEnabled ? new VerifyResponseWithRegistrationLockProcessor(r, getKeyBackupCurrentToken())
: new VerifyResponseWithoutKbs(r);
});
.map(r -> new VerifyResponseWithRegistrationLockProcessor(r, getKeyBackupCurrentToken()));
} else {
return Single.just(processor);
}
@@ -292,11 +328,14 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
}
public @NonNull Single<Boolean> canEnterSkipSmsFlow() {
if (userSkippedReRegisterFlow) {
return Single.just(false);
}
return Single.just(hasRecoveryPassword())
.flatMap(hasRecoveryPassword -> {
if (hasRecoveryPassword) {
Log.d(TAG, "Have valid recovery password but still checking kbs credentials as a backup");
return checkForValidKbsAuthCredentials().map(unused -> true);
return Single.just(true);
} else {
return checkForValidKbsAuthCredentials();
}
@@ -310,8 +349,9 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
return kbsRepository.getToken(p.getValid())
.flatMap(r -> {
if (r.getResult().isPresent()) {
setKeyBackupTokenData(r.getResult().get());
return Single.just(true);
TokenData tokenData = r.getResult().get();
setKeyBackupTokenData(tokenData);
return Single.just(tokenData.getTriesRemaining() > 0);
} else {
return Single.just(false);
}

View File

@@ -4,8 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:viewBindingIgnore="true">
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

View File

@@ -116,7 +116,9 @@
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/enterPhoneNumberFragment"
app:popUpToInclusive="true" />
</fragment>
@@ -204,15 +206,6 @@
android:name="org.thoughtcrime.securesms.registration.fragments.ReRegisterWithPinFragment"
tools:layout="@layout/fragment_registration_lock">
<action
android:id="@+id/action_reRegisterWithPinFragment_to_enterCodeFragment"
app:destination="@id/enterCodeFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/enterPhoneNumberFragment"/>
<action
android:id="@+id/action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment"
app:destination="@id/registrationCompletePlaceHolderFragment"
@@ -222,6 +215,17 @@
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_reRegisterWithPinFragment_to_enterPhoneNumberFragment"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/reRegisterWithPinFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment

View File

@@ -1596,6 +1596,17 @@
<string name="PinRestoreLockedFragment_create_new_pin">Create new PIN</string>
<string name="PinRestoreLockedFragment_learn_more_url" translatable="false">https://support.signal.org/hc/articles/360007059792</string>
<!-- Dialog button text indicating user wishes to send an sms code isntead of skipping it -->
<string name="ReRegisterWithPinFragment_send_sms_code">Send SMS code</string>
<!-- Email subject used when user contacts support about an issue with the reregister flow. -->
<string name="ReRegisterWithPinFragment_support_email_subject">Signal Registration - Need Help with reregister PIN for Android</string>
<!-- Dialog message shown in reregister flow when tapping a informational button to to learn about pins or contact support for help -->
<string name="ReRegisterWithPinFragment_need_help_local">Your PIN is a %1$d+ digit code you created that can be numeric or alphanumeric.\n\nIf you cant remember your PIN, you can create a new one.</string>
<!-- Dialog message shown in reregister flow when user requests to skip this flow and return to the normal flow -->
<string name="ReRegisterWithPinFragment_skip_local">If you cant remember your PIN, you can create a new one.</string>
<!-- Dialog message shown in reregister flow when user uses up all of their guesses for their pin and we are going to move on -->
<string name="ReRegisterWithPinFragment_out_of_guesses_local">You\'ve run out of PIN guesses, but you can still access your Signal account by creating a new PIN.</string>
<!-- PinOptOutDialog -->
<string name="PinOptOutDialog_warning">Warning</string>
<string name="PinOptOutDialog_if_you_disable_the_pin_you_will_lose_all_data">If you disable the PIN, you will lose all data when you re-register Signal unless you manually back up and restore. You cannot turn on Registration Lock while the PIN is disabled.</string>
@@ -3326,6 +3337,8 @@
<!-- KbsLockFragment -->
<string name="RegistrationLockFragment__enter_your_pin">Enter your PIN</string>
<string name="RegistrationLockFragment__enter_the_pin_you_created">Enter the PIN you created for your account. This is different from your SMS verification code.</string>
<!-- Info text shown above a pin entry text box describing what pin they should be entering. -->
<string name="RegistrationLockFragment__enter_the_pin_you_created_for_your_account">Enter the PIN you created for your account.</string>
<string name="RegistrationLockFragment__enter_alphanumeric_pin">Enter alphanumeric PIN</string>
<string name="RegistrationLockFragment__enter_numeric_pin">Enter numeric PIN</string>
<string name="RegistrationLockFragment__incorrect_pin_try_again">Incorrect PIN. Try again.</string>