diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java index d1a58e417f..04739b9859 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt index aee246e3a5..5ed2aee707 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt @@ -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) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/ReRegisterWithPinViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/ReRegisterWithPinViewModel.kt new file mode 100644 index 0000000000..4d91f54722 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/ReRegisterWithPinViewModel.kt @@ -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 = BehaviorSubject.createDefault(10) + val triesRemaining: Observable = _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) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java index 579878d0aa..87f9c78f5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java @@ -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> 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); } }) - .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 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); } diff --git a/app/src/main/res/layout/pin_restore_entry_fragment.xml b/app/src/main/res/layout/pin_restore_entry_fragment.xml index 3c39183260..7f7f139131 100644 --- a/app/src/main/res/layout/pin_restore_entry_fragment.xml +++ b/app/src/main/res/layout/pin_restore_entry_fragment.xml @@ -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"> + app:popExitAnim="@anim/nav_default_pop_exit_anim" + app:popUpTo="@id/enterPhoneNumberFragment" + app:popUpToInclusive="true" /> @@ -204,15 +206,6 @@ android:name="org.thoughtcrime.securesms.registration.fragments.ReRegisterWithPinFragment" tools:layout="@layout/fragment_registration_lock"> - - + + + Create new PIN https://support.signal.org/hc/articles/360007059792 + + Send SMS code + + Signal Registration - Need Help with reregister PIN for Android + + Your PIN is a %1$d+ digit code you created that can be numeric or alphanumeric.\n\nIf you can’t remember your PIN, you can create a new one. + + If you can’t remember your PIN, you can create a new one. + + You\'ve run out of PIN guesses, but you can still access your Signal account by creating a new PIN. + Warning 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. @@ -3326,6 +3337,8 @@ Enter your PIN Enter the PIN you created for your account. This is different from your SMS verification code. + + Enter the PIN you created for your account. Enter alphanumeric PIN Enter numeric PIN Incorrect PIN. Try again.