diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt index 06f682dd8e..b03e961264 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt @@ -20,6 +20,7 @@ enum class RegistrationCheckpoint { CHALLENGE_COMPLETED, VERIFICATION_CODE_REQUESTED, VERIFICATION_CODE_ENTERED, + PIN_ENTERED, VERIFICATION_CODE_VALIDATED, SERVICE_REGISTRATION_COMPLETED, LOCAL_REGISTRATION_COMPLETE diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt index 163b4e4691..95c36a6bfa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity +import org.thoughtcrime.securesms.pin.PinRestoreActivity import org.thoughtcrime.securesms.profiles.AvatarHelper import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity import org.thoughtcrime.securesms.recipients.Recipient @@ -40,35 +41,43 @@ class RegistrationV2Activity : BaseActivity() { } private fun handleSuccessfulVerify() { - // TODO [regv2]: add functionality of [RegistrationCompleteFragment] - val isProfileNameEmpty = Recipient.self().profileName.isEmpty - val isAvatarEmpty = !AvatarHelper.hasAvatar(this, Recipient.self().id) - val needsProfile = isProfileNameEmpty || isAvatarEmpty - val needsPin = !sharedViewModel.hasPin() - - Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin") - - SignalStore.internalValues().setForceEnterRestoreV2Flow(true) - - if (!needsProfile && !needsPin) { - sharedViewModel.completeRegistration() - } - sharedViewModel.setInProgress(false) - - val startIntent = MainActivity.clearTop(this).apply { - if (needsPin) { - putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity)) - } - - if (needsProfile) { - putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity)) - } + if (SignalStore.misc().hasLinkedDevices) { + SignalStore.misc().shouldShowLinkedDevicesReminder = sharedViewModel.isReregister } - Log.d(TAG, "Launching ${startIntent.component}") - startActivity(startIntent) - finish() - ActivityNavigator.applyPopAnimationsToPendingTransition(this) + if (SignalStore.storageService().needsAccountRestore()) { + Log.i(TAG, "Performing pin restore.") + startActivity(Intent(this, PinRestoreActivity::class.java)) + } else { + val isProfileNameEmpty = Recipient.self().profileName.isEmpty + val isAvatarEmpty = !AvatarHelper.hasAvatar(this, Recipient.self().id) + val needsProfile = isProfileNameEmpty || isAvatarEmpty + val needsPin = !sharedViewModel.hasPin() + + Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin") + + SignalStore.internalValues().setForceEnterRestoreV2Flow(true) + + if (!needsProfile && !needsPin) { + sharedViewModel.completeRegistration() + } + sharedViewModel.setInProgress(false) + + val startIntent = MainActivity.clearTop(this).apply { + if (needsPin) { + putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity)) + } + + if (needsProfile) { + putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity)) + } + } + + Log.d(TAG, "Launching ${startIntent.component}") + startActivity(startIntent) + finish() + ActivityNavigator.applyPopAnimationsToPendingTransition(this) + } } companion object { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt index 3525739577..9b39372eb3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt @@ -600,6 +600,25 @@ class RegistrationV2ViewModel : ViewModel() { } } + fun verifyCodeAndRegisterAccountWithRegistrationLock(context: Context, pin: String, submissionErrorHandler: (RegistrationResult) -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) { + Log.v(TAG, "verifyCodeAndRegisterAccountWithRegistrationLock()") + store.update { + it.copy( + inProgress = true, + registrationCheckpoint = RegistrationCheckpoint.PIN_ENTERED + ) + } + viewModelScope.launch { + verifyCodeInternal( + context = context, + pin = pin, + reglockEnabled = true, + submissionErrorHandler = submissionErrorHandler, + registrationErrorHandler = registrationErrorHandler + ) + } + } + private suspend fun verifyCodeInternal(context: Context, reglockEnabled: Boolean, pin: String?, submissionErrorHandler: (RegistrationResult) -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) { val sessionId = getOrCreateValidSession(context, submissionErrorHandler)?.body?.id ?: return val registrationData = getRegistrationData() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt new file mode 100644 index 0000000000..e097c59700 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt @@ -0,0 +1,341 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.ui.registrationlock + +import android.os.Bundle +import android.text.InputType +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.signal.core.util.Stopwatch +import org.signal.core.util.concurrent.SimpleTask +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.LoggingFragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob +import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob +import org.thoughtcrime.securesms.jobs.StorageSyncJob +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.lock.v2.PinKeyboardType +import org.thoughtcrime.securesms.lock.v2.SvrConstants +import org.thoughtcrime.securesms.pin.SvrWrongPinException +import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView +import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult +import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult +import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint +import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel +import org.thoughtcrime.securesms.util.CommunicationActions +import org.thoughtcrime.securesms.util.FeatureFlags +import org.thoughtcrime.securesms.util.SupportEmailUtil +import org.thoughtcrime.securesms.util.ViewUtil +import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.whispersystems.signalservice.api.SvrNoDataException +import java.io.IOException +import java.util.concurrent.TimeUnit + +class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registration_lock) { + companion object { + private val TAG = Log.tag(RegistrationLockV2Fragment::class.java) + } + private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind) + + private val viewModel by activityViewModels() + + private var timeRemaining: Long = 0 + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title)) + + val args: RegistrationLockV2FragmentArgs = RegistrationLockV2FragmentArgs.fromBundle(requireArguments()) + + timeRemaining = args.getTimeRemaining() + + binding.kbsLockForgotPin.visibility = View.GONE + binding.kbsLockForgotPin.setOnClickListener { handleForgottenPin(timeRemaining) } + + binding.kbsLockPinInput.setImeOptions(EditorInfo.IME_ACTION_DONE) + binding.kbsLockPinInput.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + ViewUtil.hideKeyboard(requireContext(), v!!) + handlePinEntry() + return@setOnEditorActionListener true + } + false + } + + enableAndFocusPinEntry() + + binding.kbsLockPinConfirm.setOnClickListener { + ViewUtil.hideKeyboard(requireContext(), binding.kbsLockPinInput) + handlePinEntry() + } + + binding.kbsLockKeyboardToggle.setOnClickListener { + val keyboardType: PinKeyboardType = getPinEntryKeyboardType() + updateKeyboard(keyboardType.other) + binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource) + } + + val keyboardType: PinKeyboardType = getPinEntryKeyboardType().getOther() + binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource) + + viewModel.lockedTimeRemaining.observe(viewLifecycleOwner) { t: Long -> timeRemaining = t } + + viewModel.checkpoint.observe(viewLifecycleOwner) { + if (it >= RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) { + handleSuccessfulPinEntry() + } + } + + val triesRemaining: Int = viewModel.svrTriesRemaining + + if (triesRemaining <= 3) { + val daysRemaining = getLockoutDays(timeRemaining) + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.RegistrationLockFragment__not_many_tries_left) + .setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining)) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> sendEmailToSupport() } + .show() + } + + if (triesRemaining < 5) { + binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining) + } + } + + private fun handlePinEntry() { + binding.kbsLockPinInput.setEnabled(false) + + val pin: String = binding.kbsLockPinInput.getText().toString() + + val trimmedLength = pin.replace(" ", "").length + if (trimmedLength == 0) { + Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show() + enableAndFocusPinEntry() + return + } + + if (trimmedLength < SvrConstants.MINIMUM_PIN_LENGTH) { + Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, SvrConstants.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show() + enableAndFocusPinEntry() + return + } + + binding.kbsLockPinConfirm.setSpinning() + + viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin, ::handleSessionErrorResponse, ::handleRegistrationErrorResponse) + } + + private fun handleSessionErrorResponse(requestResult: RegistrationResult) { + when (requestResult) { + is VerificationCodeRequestResult.Success -> Unit + is VerificationCodeRequestResult.RateLimited -> onRateLimited() + is VerificationCodeRequestResult.AttemptsExhausted, + is VerificationCodeRequestResult.RegistrationLocked -> onKbsAccountLocked() + else -> when (val cause = requestResult.getCause()) { + is SvrWrongPinException -> { + Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.") + onIncorrectKbsRegistrationLockPin(cause.triesRemaining) + } + is SvrNoDataException -> { + Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.") + onKbsAccountLocked() + } + else -> { + Log.w(TAG, "Unable to verify code with registration lock", cause) + onError() + } + } + } + } + + private fun handleRegistrationErrorResponse(result: RegisterAccountResult) { + when (result) { + is RegisterAccountResult.Success -> Unit + is RegisterAccountResult.RateLimited -> onRateLimited() + is RegisterAccountResult.AttemptsExhausted, + is RegisterAccountResult.RegistrationLocked -> onKbsAccountLocked() + else -> when (val cause = result.getCause()) { + is SvrWrongPinException -> { + Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.") + onIncorrectKbsRegistrationLockPin(cause.triesRemaining) + } + is SvrNoDataException -> { + Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.") + onKbsAccountLocked() + } + else -> { + Log.w(TAG, "Unable to register account with registration lock", cause) + onError() + } + } + } + } + + private fun onIncorrectKbsRegistrationLockPin(svrTriesRemaining: Int) { + binding.kbsLockPinConfirm.cancelSpinning() + binding.kbsLockPinInput.getText().clear() + enableAndFocusPinEntry() + + if (svrTriesRemaining == 0) { + Log.w(TAG, "Account locked. User out of attempts on KBS.") + onAccountLocked() + return + } + + if (svrTriesRemaining == 3) { + val daysRemaining = getLockoutDays(timeRemaining) + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.RegistrationLockFragment__incorrect_pin) + .setMessage(getTriesRemainingDialogMessage(svrTriesRemaining, daysRemaining)) + .setPositiveButton(android.R.string.ok, null) + .show() + } + + if (svrTriesRemaining > 5) { + binding.kbsLockPinInputLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again) + } else { + binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, svrTriesRemaining, svrTriesRemaining) + binding.kbsLockForgotPin.visibility = View.VISIBLE + } + } + + private fun onRateLimited() { + binding.kbsLockPinConfirm.cancelSpinning() + enableAndFocusPinEntry() + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.RegistrationActivity_too_many_attempts) + .setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day) + .setPositiveButton(android.R.string.ok, null) + .show() + } + + private fun onKbsAccountLocked() { + onAccountLocked() + } + + fun onError() { + binding.kbsLockPinConfirm.cancelSpinning() + enableAndFocusPinEntry() + + Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show() + } + + private fun handleForgottenPin(timeRemainingMs: Long) { + val lockoutDays = getLockoutDays(timeRemainingMs) + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.RegistrationLockFragment__forgot_your_pin) + .setMessage(requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays)) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> sendEmailToSupport() } + .show() + } + + private fun getLockoutDays(timeRemainingMs: Long): Int { + return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1 + } + + private fun onAccountLocked() { + navigateToAccountLocked() + } + + private fun getTriesRemainingDialogMessage(triesRemaining: Int, daysRemaining: Int): String { + val resources = requireContext().resources + val tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining) + val days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining) + + return "$tries $days" + } + + private fun enableAndFocusPinEntry() { + binding.kbsLockPinInput.setEnabled(true) + binding.kbsLockPinInput.setFocusable(true) + ViewUtil.focusAndShowKeyboard(binding.kbsLockPinInput) + } + + private fun getPinEntryKeyboardType(): PinKeyboardType { + val isNumeric = (binding.kbsLockPinInput.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.setInputType( + 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.getText().clear() + } + + private fun navigateToAccountLocked() { + findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked()) + } + + private fun handleSuccessfulPinEntry() { + SignalStore.pinValues().keyboardType = getPinEntryKeyboardType() + + SimpleTask.run({ + SignalStore.onboarding().clearAll() + val stopwatch = Stopwatch("RegistrationLockRestore") + + AppDependencies.jobManager.runSynchronously(StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN) + stopwatch.split("AccountRestore") + + AppDependencies.jobManager + .startChain(StorageSyncJob()) + .then(ReclaimUsernameAndLinkJob()) + .enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10)) + stopwatch.split("ContactRestore") + + try { + FeatureFlags.refreshSync() + } catch (e: IOException) { + Log.w(TAG, "Failed to refresh flags.", e) + } + stopwatch.split("FeatureFlags") + + stopwatch.stop(TAG) + null + }, { none: Any? -> + binding.kbsLockPinConfirm.cancelSpinning() + findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionSuccessfulRegistration()) + }) + } + + private fun sendEmailToSupport() { + val subject = R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v2_pin + + val body = SupportEmailUtil.generateSupportEmailBody( + requireContext(), + subject, + null, + null + ) + CommunicationActions.openEmail( + requireContext(), + SupportEmailUtil.getSupportEmailAddress(requireContext()), + getString(subject), + body + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt index 6aee34483d..0f081cb5ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt @@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.databinding.FragmentRegistrationPinRestoreEntryV2Binding import org.thoughtcrime.securesms.lock.v2.PinKeyboardType import org.thoughtcrime.securesms.lock.v2.SvrConstants -import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint @@ -84,7 +83,7 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati if (state.networkError != null) { genericErrorDialog() } else if (!state.canSkipSms) { - abortSkipSmsFlow() + findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment()) } else if (state.isRegistrationLockEnabled && state.svrTriesRemaining == 0) { Log.w(TAG, "Unable to continue skip flow, KBS is locked") onAccountLocked() @@ -94,10 +93,6 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati } } - private fun abortSkipSmsFlow() { - findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment()) - } - private fun presentProgress(inProgress: Boolean) { if (inProgress) { ViewUtil.hideKeyboard(requireContext(), binding.pinRestorePinInput) @@ -118,8 +113,8 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati return } - if (pin.trim().length < BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH) { - Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show() + if (pin.trim().length < SvrConstants.MINIMUM_PIN_LENGTH) { + Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, SvrConstants.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show() enableAndFocusPinEntry() return } @@ -134,8 +129,6 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati }, registrationErrorHandler = ::registrationErrorHandler ) - - // TODO [regv2]: check for registration lock + wrong pin and decrement SVR tries remaining } private fun presentTriesRemaining(triesRemaining: Int) { diff --git a/app/src/main/res/navigation/registration_v2.xml b/app/src/main/res/navigation/registration_v2.xml index 4ea88cb33d..df81ab4bde 100644 --- a/app/src/main/res/navigation/registration_v2.xml +++ b/app/src/main/res/navigation/registration_v2.xml @@ -150,7 +150,7 @@ @@ -209,13 +209,13 @@ diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt index 36bc2548f1..78b28ae0fd 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt @@ -7,6 +7,7 @@ package org.whispersystems.signalservice.api.registration import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.account.AccountAttributes +import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest import org.whispersystems.signalservice.api.account.PreKeyCollection import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse import org.whispersystems.signalservice.internal.push.PushServiceSocket @@ -111,4 +112,15 @@ class RegistrationApi( pushServiceSocket.checkBackupAuthCredentials(e164, usernamePasswords) } } + + /** + * Changes the phone number that an account is associated with. + * + * `PUT /v2/accounts/number` + */ + fun changeNumber(requestBody: ChangePhoneNumberRequest): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.changeNumber(requestBody) + } + } }