diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt index 88ff88bbf8..fd3382d794 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt @@ -75,7 +75,7 @@ class AccountSettingsFragment : ComposeFragment() { AccountSettingsAction.LaunchChangePinFlow -> pinFlowLauncher.launch(CreateSvrPinActivity.getIntentForPinChangeFromSettings(requireContext())) AccountSettingsAction.ShowPinCreatedConfirmation -> Snackbar.make(requireView(), R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show() AccountSettingsAction.NavigateToSignalLoginDetails -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_signalLoginViewDetailsFragment) - AccountSettingsAction.NavigateToAuthenticatorApps -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_authenticatorAppsFragment) + AccountSettingsAction.NavigateToTotpAppList -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_authenticatorAppsFragment) AccountSettingsAction.NavigateToPasskeys -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_passkeysFragment) AccountSettingsAction.NavigateToAdvancedPinSettings -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity) AccountSettingsAction.NavigateToChangePhoneNumber -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsRepository.kt index bf4d5c3e21..256b0702a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsRepository.kt @@ -8,7 +8,7 @@ package org.thoughtcrime.securesms.components.settings.app.account import kotlinx.coroutines.withContext import org.signal.core.util.concurrent.SignalDispatchers import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.components.settings.app.account.authenticator.AuthenticatorRepository +import org.thoughtcrime.securesms.components.settings.app.account.authenticator.TotpRepository import org.thoughtcrime.securesms.components.settings.app.account.passkeys.AppPasskeysRepository import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -27,7 +27,7 @@ class AccountSettingsRepository { private val TAG = Log.tag(AccountSettingsRepository::class) } - private val authenticatorRepository = AuthenticatorRepository() + private val totpRepository = TotpRepository() private val passkeysRepository = AppPasskeysRepository() fun hasPin(): Boolean = SignalStore.svr.hasPin() && !SignalStore.svr.hasOptedOut() @@ -48,7 +48,12 @@ class AccountSettingsRepository { fun isPhoneNumberless(): Boolean = SignalStore.account.isPhoneNumberless - fun getAuthenticatorAppCount(): Int = authenticatorRepository.getAuthenticatorApps().size + /** + * How many authenticator apps are on the account, or null if we couldn't find out. + */ + suspend fun getTotpAppCount(): Int? { + return (totpRepository.getTotpApps() as? TotpRepository.AppsResult.Success)?.apps?.size + } fun getPasskeyCount(): Int = passkeysRepository.getPasskeys().size diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt index 2ca7258087..cf33027ffb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.components.settings.app.account +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -12,6 +13,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.signal.appsettings.account.AccountSettingsAction import org.signal.appsettings.account.AccountSettingsEvent import org.signal.appsettings.account.AccountSettingsState @@ -40,7 +42,7 @@ class AccountSettingsViewModel( val actions: Flow = _actions.receiveAsFlow() init { - refresh() + viewModelScope.launch { refresh() } } override suspend fun processEvent(event: AccountSettingsEvent) { @@ -104,8 +106,8 @@ class AccountSettingsViewModel( AccountSettingsEvent.AccountAndRecoveryClicked -> { _actions.send(AccountSettingsAction.NavigateToSignalLoginDetails) } - AccountSettingsEvent.AuthenticatorAppClicked -> { - _actions.send(AccountSettingsAction.NavigateToAuthenticatorApps) + AccountSettingsEvent.TotpAppClicked -> { + _actions.send(AccountSettingsAction.NavigateToTotpAppList) } AccountSettingsEvent.PasskeysClicked -> { _actions.send(AccountSettingsAction.NavigateToPasskeys) @@ -147,8 +149,9 @@ class AccountSettingsViewModel( } } - private fun refresh() { + private suspend fun refresh() { val isPhoneNumberless = repository.isPhoneNumberless() + val totpAppCount = repository.getTotpAppCount() _state.update { it.copy( @@ -161,7 +164,7 @@ class AccountSettingsViewModel( isPhoneNumberless = isPhoneNumberless, signalLogin = if (isPhoneNumberless) { AccountSettingsState.SignalLogin( - authenticatorAppCount = repository.getAuthenticatorAppCount(), + totpAppCount = totpAppCount, passkeyCount = repository.getPasskeyCount() ) } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppStore.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppStore.kt deleted file mode 100644 index 20b2f3a05e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppStore.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import org.signal.appsettings.authenticatorapps.AuthenticatorApp - -/** - * Stand-in for wherever authenticator app state will eventually live. Nothing is persisted or sent to the service yet, - * so all of this is mocked up and lasts only as long as the process does. - */ -object AuthenticatorAppStore { - - /** The key we'd hand off to an authenticator app, which the service will supply for real later on. */ - const val MOCK_SETUP_KEY = "KVZ7WL3FDDWJZMTOB7PLZPKVRFD4LYSX" - - /** How many authenticator apps an account is allowed, which the service will decide for real later on. */ - const val MAX_APPS = 2 - - private val lock = Any() - private val apps = mutableListOf() - private var nextId = 1L - - fun getApps(): List = synchronized(lock) { apps.toList() } - - fun addApp(name: String, createdAt: Long): Long = synchronized(lock) { - val id = nextId++ - apps += AuthenticatorApp(id = id, name = name, createdAt = createdAt) - id - } - - fun renameApp(id: Long, name: String) = synchronized(lock) { - val index = apps.indexOfFirst { it.id == id } - if (index >= 0) { - apps[index] = apps[index].copy(name = name) - } - } - - fun removeApp(id: Long) = synchronized(lock) { - apps.removeAll { it.id == id } - } - - fun getApp(id: Long): AuthenticatorApp? = synchronized(lock) { apps.firstOrNull { it.id == id } } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppsFragment.kt deleted file mode 100644 index d7b7da4dc5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppsFragment.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import android.os.Bundle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.fragment.app.viewModels -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.fragment.findNavController -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsAction -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsEvent -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsScreen -import org.signal.core.ui.compose.CollectActions -import org.signal.core.ui.compose.ComposeFragment -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Lists the authenticator apps on the account. Carries out the [AuthenticatorAppsAction]s that need the nav graph. - */ -class AuthenticatorAppsFragment : ComposeFragment() { - - companion object { - private val TAG = Log.tag(AuthenticatorAppsFragment::class) - } - - private val viewModel: AuthenticatorAppsViewModel by viewModels() - - @Composable - override fun FragmentContent() { - val state by viewModel.state.collectAsStateWithLifecycle() - - CollectActions(viewModel.actions) { action -> handleAction(action) } - - AuthenticatorAppsScreen( - state = state, - onEvent = viewModel::onEvent - ) - } - - override fun onResume() { - super.onResume() - viewModel.onEvent(AuthenticatorAppsEvent.ScreenResumed) - } - - private fun handleAction(action: AuthenticatorAppsAction) { - when (action) { - AuthenticatorAppsAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() - AuthenticatorAppsAction.NavigateToSetup -> { - findNavController().safeNavigate(R.id.action_authenticatorAppsFragment_to_authenticatorSetupFragment) - } - is AuthenticatorAppsAction.NavigateToRename -> { - findNavController().safeNavigate( - R.id.action_authenticatorAppsFragment_to_authenticatorNameFragment, - Bundle().apply { putLong(AuthenticatorNavArgs.ARG_APP_ID, action.appId) } - ) - } - is AuthenticatorAppsAction.NavigateToRemovalCodeEntry -> { - findNavController().safeNavigate( - R.id.action_authenticatorAppsFragment_to_authenticatorCodeEntryFragment, - Bundle().apply { - putString(AuthenticatorNavArgs.ARG_PURPOSE, AuthenticatorNavArgs.PURPOSE_REMOVE) - putLong(AuthenticatorNavArgs.ARG_APP_ID, action.appId) - } - ) - } - AuthenticatorAppsAction.OpenLearnMore -> Log.w(TAG, "There's no support article to open yet.") - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppsViewModel.kt deleted file mode 100644 index 002503548a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppsViewModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsAction -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsEvent -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsState -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsState.Dialog -import org.signal.core.ui.compose.EventDrivenViewModel -import org.signal.core.util.logging.Log - -/** - * Drives the screen that lists the authenticator apps on the account. - */ -class AuthenticatorAppsViewModel( - private val repository: AuthenticatorRepository = AuthenticatorRepository() -) : EventDrivenViewModel(TAG) { - - companion object { - private val TAG = Log.tag(AuthenticatorAppsViewModel::class) - } - - private val _state = MutableStateFlow(AuthenticatorAppsState(maxApps = repository.getMaxApps())) - private val _actions = Channel(Channel.BUFFERED) - - val state: StateFlow = _state.asStateFlow() - val actions: Flow = _actions.receiveAsFlow() - - init { - refresh() - } - - override suspend fun processEvent(event: AuthenticatorAppsEvent) { - when (event) { - AuthenticatorAppsEvent.ScreenResumed -> { - refresh() - } - AuthenticatorAppsEvent.NavigateBackClicked -> { - _actions.send(AuthenticatorAppsAction.NavigateBack) - } - AuthenticatorAppsEvent.AddAuthenticatorAppClicked -> { - if (_state.value.atMaxApps) { - _state.update { it.copy(dialog = Dialog.MaxAppsReached) } - } else { - _actions.send(AuthenticatorAppsAction.NavigateToSetup) - } - } - AuthenticatorAppsEvent.LearnMoreClicked -> { - _actions.send(AuthenticatorAppsAction.OpenLearnMore) - } - is AuthenticatorAppsEvent.RenameAppClicked -> { - _actions.send(AuthenticatorAppsAction.NavigateToRename(event.appId)) - } - is AuthenticatorAppsEvent.RemoveAppClicked -> { - _state.update { it.copy(dialog = Dialog.ConfirmRemove(event.appId)) } - } - AuthenticatorAppsEvent.RemoveAppConfirmed -> { - val dialog = _state.value.dialog as? Dialog.ConfirmRemove ?: return - _state.update { it.copy(dialog = Dialog.None) } - _actions.send(AuthenticatorAppsAction.NavigateToRemovalCodeEntry(dialog.appId)) - } - AuthenticatorAppsEvent.DialogDismissed -> { - _state.update { it.copy(dialog = Dialog.None) } - } - } - } - - private fun refresh() { - _state.update { it.copy(apps = repository.getAuthenticatorApps()) } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorCodeEntryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorCodeEntryFragment.kt deleted file mode 100644 index 09ceb2489e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorCodeEntryFragment.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import android.widget.Toast -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.fragment.findNavController -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryAction -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryScreen -import org.signal.core.ui.compose.CollectActions -import org.signal.core.ui.compose.ComposeFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.navigation.safeNavigate -import org.thoughtcrime.securesms.util.viewModel -import org.signal.appsettings.R as AppSettingsR - -/** - * Collects the code from the user's authenticator app, either to confirm a newly paired one or to authorize removing - * one. Carries out the [AuthenticatorCodeEntryAction]s that need the nav graph. - */ -class AuthenticatorCodeEntryFragment : ComposeFragment() { - - private val viewModel: AuthenticatorCodeEntryViewModel by viewModel { - AuthenticatorCodeEntryViewModel(AuthenticatorNavArgs.purpose(arguments)) - } - - @Composable - override fun FragmentContent() { - val state by viewModel.state.collectAsStateWithLifecycle() - - CollectActions(viewModel.actions) { action -> handleAction(action) } - - AuthenticatorCodeEntryScreen( - state = state, - onEvent = viewModel::onEvent - ) - } - - private fun handleAction(action: AuthenticatorCodeEntryAction) { - when (action) { - AuthenticatorCodeEntryAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() - AuthenticatorCodeEntryAction.NavigateToNaming -> { - findNavController().safeNavigate(R.id.action_authenticatorCodeEntryFragment_to_authenticatorNameFragment) - } - AuthenticatorCodeEntryAction.NavigateToAuthenticatorApps -> findNavController().popBackStack(R.id.authenticatorAppsFragment, false) - AuthenticatorCodeEntryAction.ShowAuthenticatorAppRemoved -> { - Toast.makeText(requireContext(), AppSettingsR.string.AuthenticatorAppsScreen__authenticator_app_removed, Toast.LENGTH_SHORT).show() - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorCodeEntryViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorCodeEntryViewModel.kt deleted file mode 100644 index 766a9bc770..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorCodeEntryViewModel.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryAction -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryEvent -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryState -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryState.Purpose -import org.signal.core.ui.compose.EventDrivenViewModel -import org.signal.core.util.logging.Log - -/** - * Drives the screen that collects a code from the user's authenticator app, which is required both to confirm a newly - * paired app and to remove one that already exists. There's nothing to verify the code against yet, so any code of the - * right length is treated as correct. - */ -class AuthenticatorCodeEntryViewModel( - purpose: Purpose = Purpose.Add, - private val repository: AuthenticatorRepository = AuthenticatorRepository() -) : EventDrivenViewModel(TAG) { - - companion object { - private val TAG = Log.tag(AuthenticatorCodeEntryViewModel::class) - } - - private val _state = MutableStateFlow(AuthenticatorCodeEntryState(purpose = purpose)) - private val _actions = Channel(Channel.BUFFERED) - - val state: StateFlow = _state.asStateFlow() - val actions: Flow = _actions.receiveAsFlow() - - override suspend fun processEvent(event: AuthenticatorCodeEntryEvent) { - when (event) { - AuthenticatorCodeEntryEvent.NavigateBackClicked -> { - _actions.send(AuthenticatorCodeEntryAction.NavigateBack) - } - is AuthenticatorCodeEntryEvent.CodeChanged -> { - val digits = event.code.filter { it.isDigit() }.take(AuthenticatorCodeEntryState.CODE_LENGTH) - _state.update { it.copy(code = digits) } - } - AuthenticatorCodeEntryEvent.DoneClicked -> { - if (!_state.value.canSubmit) { - return - } - - Log.i(TAG, "Accepting the entered code without verifying it, which is all we can do until this is wired up.") - _state.update { it.copy(submitting = true) } - - when (val purpose = _state.value.purpose) { - Purpose.Add -> { - _actions.send(AuthenticatorCodeEntryAction.NavigateToNaming) - } - is Purpose.Remove -> { - repository.removeAuthenticatorApp(purpose.appId) - _actions.send(AuthenticatorCodeEntryAction.ShowAuthenticatorAppRemoved) - _actions.send(AuthenticatorCodeEntryAction.NavigateToAuthenticatorApps) - } - } - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNameFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNameFragment.kt deleted file mode 100644 index ab951264d8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNameFragment.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import android.widget.Toast -import androidx.annotation.StringRes -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.fragment.findNavController -import org.signal.appsettings.authenticatorname.AuthenticatorNameAction -import org.signal.appsettings.authenticatorname.AuthenticatorNameScreen -import org.signal.core.ui.compose.CollectActions -import org.signal.core.ui.compose.ComposeFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.viewModel -import org.signal.appsettings.R as AppSettingsR - -/** - * Names an authenticator app, either a newly paired one or one being renamed. Carries out the - * [AuthenticatorNameAction]s that need the nav graph. - */ -class AuthenticatorNameFragment : ComposeFragment() { - - private val viewModel: AuthenticatorNameViewModel by viewModel { - AuthenticatorNameViewModel(AuthenticatorNavArgs.appId(arguments)) - } - - @Composable - override fun FragmentContent() { - val state by viewModel.state.collectAsStateWithLifecycle() - - CollectActions(viewModel.actions) { action -> handleAction(action) } - - AuthenticatorNameScreen( - state = state, - onEvent = viewModel::onEvent - ) - } - - private fun handleAction(action: AuthenticatorNameAction) { - when (action) { - AuthenticatorNameAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() - AuthenticatorNameAction.NavigateToAuthenticatorApps -> findNavController().popBackStack(R.id.authenticatorAppsFragment, false) - AuthenticatorNameAction.ShowAuthenticatorAppSetUp -> toast(AppSettingsR.string.AuthenticatorNameScreen__authenticator_app_set_up) - AuthenticatorNameAction.ShowAuthenticatorAppRenamed -> toast(AppSettingsR.string.AuthenticatorNameScreen__authenticator_app_renamed) - } - } - - private fun toast(@StringRes message: Int) { - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNameViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNameViewModel.kt deleted file mode 100644 index 0cbbc218cf..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNameViewModel.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update -import org.signal.appsettings.authenticatorname.AuthenticatorNameAction -import org.signal.appsettings.authenticatorname.AuthenticatorNameEvent -import org.signal.appsettings.authenticatorname.AuthenticatorNameState -import org.signal.core.ui.compose.EventDrivenViewModel -import org.signal.core.util.logging.Log - -/** - * Drives the screen that names an authenticator app. [appId] is null when a newly paired app is being named for the - * first time, and set when an existing one is being renamed. - */ -class AuthenticatorNameViewModel( - private val appId: Long?, - private val repository: AuthenticatorRepository = AuthenticatorRepository() -) : EventDrivenViewModel(TAG) { - - companion object { - private val TAG = Log.tag(AuthenticatorNameViewModel::class) - } - - private val _state = MutableStateFlow( - AuthenticatorNameState( - name = appId?.let { repository.getAuthenticatorApp(it)?.name } ?: "", - renaming = appId != null - ) - ) - private val _actions = Channel(Channel.BUFFERED) - - val state: StateFlow = _state.asStateFlow() - val actions: Flow = _actions.receiveAsFlow() - - override suspend fun processEvent(event: AuthenticatorNameEvent) { - when (event) { - AuthenticatorNameEvent.NavigateBackClicked -> { - _actions.send(AuthenticatorNameAction.NavigateBack) - } - is AuthenticatorNameEvent.NameChanged -> { - _state.update { it.copy(name = event.name) } - } - AuthenticatorNameEvent.NextClicked -> { - if (!_state.value.canSubmit) { - return - } - - val name = _state.value.name.trim() - _state.update { it.copy(submitting = true) } - - if (appId != null) { - repository.renameAuthenticatorApp(appId, name) - _actions.send(AuthenticatorNameAction.ShowAuthenticatorAppRenamed) - } else { - repository.addAuthenticatorApp(name) - _actions.send(AuthenticatorNameAction.ShowAuthenticatorAppSetUp) - } - - _actions.send(AuthenticatorNameAction.NavigateToAuthenticatorApps) - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNavArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNavArgs.kt deleted file mode 100644 index c481d74d02..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNavArgs.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import android.os.Bundle -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryState.Purpose -import org.signal.core.util.logging.Log - -/** - * The nav arguments the authenticator app screens pass between each other, and the parsing that turns them back into - * something typed. Keep the values in sync with the argument defaults declared for these destinations in - * app_settings_with_change_number.xml. - */ -object AuthenticatorNavArgs { - - private val TAG = Log.tag(AuthenticatorNavArgs::class) - - /** Which of [PURPOSE_ADD]/[PURPOSE_REMOVE] a code is being collected for. */ - const val ARG_PURPOSE = "purpose" - const val PURPOSE_ADD = "ADD" - const val PURPOSE_REMOVE = "REMOVE" - - /** The app being removed or renamed, or [NO_APP_ID] when the screen is acting on a newly paired app. */ - const val ARG_APP_ID = "app_id" - const val NO_APP_ID = -1L - - /** The app id in [arguments], or null when the screen is acting on a newly paired app. */ - fun appId(arguments: Bundle?): Long? = arguments?.getLong(ARG_APP_ID, NO_APP_ID)?.takeIf { it != NO_APP_ID } - - /** The purpose in [arguments], falling back to [Purpose.Add] rather than removing an app we can't identify. */ - fun purpose(arguments: Bundle?): Purpose { - if (arguments?.getString(ARG_PURPOSE) != PURPOSE_REMOVE) { - return Purpose.Add - } - - val appId = appId(arguments) - if (appId == null) { - Log.w(TAG, "Asked to remove an authenticator app without an id. Collecting a code to add one instead.") - return Purpose.Add - } - - return Purpose.Remove(appId) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorRepository.kt deleted file mode 100644 index 4d810a8913..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorRepository.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import org.signal.appsettings.authenticatorapps.AuthenticatorApp - -class AuthenticatorRepository { - - fun getSetupKey(): String = AuthenticatorAppStore.MOCK_SETUP_KEY - - fun getMaxApps(): Int = AuthenticatorAppStore.MAX_APPS - - fun getAuthenticatorApps(): List = AuthenticatorAppStore.getApps() - - fun getAuthenticatorApp(id: Long): AuthenticatorApp? = AuthenticatorAppStore.getApp(id) - - fun addAuthenticatorApp(name: String): Long = AuthenticatorAppStore.addApp(name, System.currentTimeMillis()) - - fun renameAuthenticatorApp(id: Long, name: String) = AuthenticatorAppStore.renameApp(id, name) - - fun removeAuthenticatorApp(id: Long) = AuthenticatorAppStore.removeApp(id) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorSetupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorSetupViewModel.kt deleted file mode 100644 index 8ce040d845..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorSetupViewModel.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import android.net.Uri -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.receiveAsFlow -import org.signal.appsettings.authenticatorsetup.AuthenticatorSetupAction -import org.signal.appsettings.authenticatorsetup.AuthenticatorSetupEvent -import org.signal.appsettings.authenticatorsetup.AuthenticatorSetupState -import org.signal.core.ui.compose.EventDrivenViewModel -import org.signal.core.util.logging.Log - -/** - * Drives the screen that walks the user through pairing an authenticator app. The setup key is mocked up for now, - * since there's nothing to fetch it from yet. - */ -class AuthenticatorSetupViewModel( - repository: AuthenticatorRepository = AuthenticatorRepository() -) : EventDrivenViewModel(TAG) { - - companion object { - private val TAG = Log.tag(AuthenticatorSetupViewModel::class) - private const val ACCOUNT_LABEL = "Signal" - } - - private val _state = MutableStateFlow(AuthenticatorSetupState(setupKey = repository.getSetupKey())) - private val _actions = Channel(Channel.BUFFERED) - - val state: StateFlow = _state.asStateFlow() - val actions: Flow = _actions.receiveAsFlow() - - override suspend fun processEvent(event: AuthenticatorSetupEvent) { - when (event) { - AuthenticatorSetupEvent.NavigateBackClicked -> { - _actions.send(AuthenticatorSetupAction.NavigateBack) - } - AuthenticatorSetupEvent.OpenAuthenticatorAppClicked -> { - _actions.send(AuthenticatorSetupAction.LaunchAuthenticatorApp(buildSetupUri(_state.value.setupKey))) - } - AuthenticatorSetupEvent.CopyKeyClicked -> { - _actions.send(AuthenticatorSetupAction.CopyKeyToClipboard(_state.value.setupKey)) - _actions.send(AuthenticatorSetupAction.ShowKeyCopied) - } - AuthenticatorSetupEvent.NoAuthenticatorAppFound -> { - _actions.send(AuthenticatorSetupAction.ShowNoAuthenticatorAppFound) - } - AuthenticatorSetupEvent.ContinueClicked -> { - _actions.send(AuthenticatorSetupAction.NavigateToCodeEntry) - } - } - } - - private fun buildSetupUri(setupKey: String): String { - return Uri.Builder() - .scheme("otpauth") - .authority("totp") - .appendPath(ACCOUNT_LABEL) - .appendQueryParameter("secret", setupKey) - .appendQueryParameter("issuer", ACCOUNT_LABEL) - .build() - .toString() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/InMemoryTotpApi.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/InMemoryTotpApi.kt new file mode 100644 index 0000000000..f1c49e5ea1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/InMemoryTotpApi.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import org.signal.libsignal.net.RequestResult +import java.security.SecureRandom + +/** + * Stands in for the service until the gRPC methods land. Nothing here is persisted or sent anywhere, so it lasts only + * as long as the process does. + * + * It doesn't verify one-time passwords -- any code confirms the pending key. It does copy the service's behaviour where + * that behaviour shapes the screens: one pending key at a time, [TotpApi.MAX_KEYS] confirmed keys, ids drawn from the + * lowest free slot, and name length enforced. + */ +class InMemoryTotpApi : TotpApi { + + companion object { + /** What the service generates: a 256-bit key. */ + private const val KEY_LENGTH_BYTES = 32 + } + + private val lock = Any() + private val secureRandom = SecureRandom() + + private var hasPendingKey = false + private val confirmedKeys = mutableMapOf() + + override suspend fun generateKey(): RequestResult = synchronized(lock) { + if (confirmedKeys.size >= TotpApi.MAX_KEYS) { + return RequestResult.NonSuccess(TotpApi.GenerateKeyError.TooManyKeys) + } + + hasPendingKey = true + + RequestResult.Success(TotpApi.GeneratedKey(key = ByteArray(KEY_LENGTH_BYTES).also { secureRandom.nextBytes(it) })) + } + + override suspend fun confirmKey(oneTimePassword: Int, metadata: TotpApi.Metadata): RequestResult = synchronized(lock) { + requireNameFits(metadata) + + if (!hasPendingKey) { + return RequestResult.NonSuccess(TotpApi.ConfirmKeyError.NotVerified) + } + + if (confirmedKeys.size >= TotpApi.MAX_KEYS) { + return RequestResult.NonSuccess(TotpApi.ConfirmKeyError.TooManyKeys) + } + + val keyId = nextKeyId() + confirmedKeys[keyId] = metadata + hasPendingKey = false + + RequestResult.Success(keyId) + } + + override suspend fun listKeys(): RequestResult, Nothing> = synchronized(lock) { + RequestResult.Success( + confirmedKeys.entries + .sortedBy { it.key } + .map { (keyId, metadata) -> TotpApi.RemoteKey(keyId = keyId, metadata = metadata) } + ) + } + + override suspend fun setKeyMetadata(keyId: Int, metadata: TotpApi.Metadata): RequestResult = synchronized(lock) { + requireNameFits(metadata) + + if (keyId !in confirmedKeys) { + return RequestResult.NonSuccess(TotpApi.SetKeyMetadataError.KeyNotFound) + } + confirmedKeys[keyId] = metadata + + RequestResult.Success(Unit) + } + + override suspend fun removeKey(keyId: Int): RequestResult = synchronized(lock) { + confirmedKeys.remove(keyId) + RequestResult.Success(Unit) + } + + /** The service hands out the lowest free id rather than counting upwards, so removing a key frees its id for reuse. */ + private fun nextKeyId(): Int = TotpApi.KEY_ID_RANGE.first { it !in confirmedKeys } + + private fun requireNameFits(metadata: TotpApi.Metadata) { + require(metadata.name.toByteArray(Charsets.UTF_8).size <= TotpApi.Metadata.NAME_MAX_LENGTH) { + "Name must be at most ${TotpApi.Metadata.NAME_MAX_LENGTH} bytes of UTF-8" + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpApi.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpApi.kt new file mode 100644 index 0000000000..51deb8065d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpApi.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import org.signal.core.util.censor +import org.signal.libsignal.net.BadRequestError +import org.signal.libsignal.net.RequestResult +import java.time.Instant + +/** + * All TOTP operations. + */ +interface TotpApi { + + companion object { + /** How many confirmed keys an account may have, which the service enforces. */ + const val MAX_KEYS = 2 + + /** The ids the service will assign, which fit in a byte with the sign bit clear. */ + val KEY_ID_RANGE = 0..127 + } + + /** + * Generates a new pending key, replacing any pending key already on the account. The key doesn't take effect, or show + * up in [listKeys], until [confirmKey] proves the caller kept a copy of it. + */ + suspend fun generateKey(): RequestResult + + /** + * Confirms the pending key by proving a one-time password can be derived from it, and attaches [metadata] to it, + * returning the id the service assigned. + */ + suspend fun confirmKey(oneTimePassword: Int, metadata: Metadata): RequestResult + + /** The confirmed keys on the account. Key material is never returned, only metadata and parameters. */ + suspend fun listKeys(): RequestResult, Nothing> + + /** Replaces the metadata attached to a confirmed key. */ + suspend fun setKeyMetadata(keyId: Int, metadata: Metadata): RequestResult + + /** Removes a key, which also succeeds when there's no key with that id, so retries look the same as the first try. */ + suspend fun removeKey(keyId: Int): RequestResult + + data class Metadata(val name: String, val createdAt: Instant) { + override fun toString(): String = "Metadata(name=${name.censor()}, createdAt=$createdAt)" + + companion object { + /** How long [name] may be, in bytes of UTF-8, which is the room the service's metadata blob leaves for it. */ + const val NAME_MAX_LENGTH = 98 + } + } + + data class GeneratedKey(val key: ByteArray) { + override fun equals(other: Any?): Boolean = other is GeneratedKey && key.contentEquals(other.key) + override fun hashCode(): Int = key.contentHashCode() + override fun toString(): String = "GeneratedKey()" + } + + data class RemoteKey( + /** The account-specific id, in [KEY_ID_RANGE]. */ + val keyId: Int, + val metadata: Metadata + ) { + override fun toString(): String = "RemoteKey(keyId=$keyId)" + } + + sealed interface GenerateKeyError : BadRequestError { + /** The account already has [MAX_KEYS] keys. */ + data object TooManyKeys : GenerateKeyError + } + + sealed interface ConfirmKeyError : BadRequestError { + /** The password was wrong, the clocks are too far apart, or there was no pending key. The service can't tell us which. */ + data object NotVerified : ConfirmKeyError + + /** The account filled up with keys between generating this one and confirming it. */ + data object TooManyKeys : ConfirmKeyError + } + + sealed interface SetKeyMetadataError : BadRequestError { + data object KeyNotFound : SetKeyMetadataError + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpAppListFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpAppListFragment.kt new file mode 100644 index 0000000000..9f64aaf376 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpAppListFragment.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import android.os.Bundle +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.fragment.findNavController +import org.signal.appsettings.totpapplist.TotpAppListAction +import org.signal.appsettings.totpapplist.TotpAppListEvent +import org.signal.appsettings.totpapplist.TotpAppListScreen +import org.signal.core.ui.compose.CollectActions +import org.signal.core.ui.compose.ComposeFragment +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.signal.appsettings.R as AppSettingsR + +/** + * Lists the authenticator apps on the account. Carries out the [TotpAppListAction]s that need the nav graph. + */ +class TotpAppListFragment : ComposeFragment() { + + companion object { + private val TAG = Log.tag(TotpAppListFragment::class) + } + + private val viewModel: TotpAppListViewModel by viewModels() + + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() + + CollectActions(viewModel.actions) { action -> handleAction(action) } + + TotpAppListScreen( + state = state, + onEvent = viewModel::onEvent + ) + } + + override fun onResume() { + super.onResume() + viewModel.onEvent(TotpAppListEvent.ScreenResumed) + } + + private fun handleAction(action: TotpAppListAction) { + when (action) { + TotpAppListAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() + TotpAppListAction.NavigateToSetup -> { + findNavController().safeNavigate(R.id.action_authenticatorAppsFragment_to_authenticatorSetupFragment) + } + is TotpAppListAction.NavigateToRename -> { + findNavController().safeNavigate( + R.id.action_authenticatorAppsFragment_to_authenticatorNameFragment, + Bundle().apply { TotpNavArgs.putRenamedApp(this, action.app) } + ) + } + TotpAppListAction.ShowTotpAppRemoved -> toast(AppSettingsR.string.TotpAppListScreen__authenticator_app_removed) + TotpAppListAction.ShowRemovalFailed -> toast(AppSettingsR.string.TotpAppListScreen__couldnt_remove_authenticator_app) + TotpAppListAction.OpenLearnMore -> Log.w(TAG, "There's no support article to open yet.") + } + } + + private fun toast(@StringRes message: Int) { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpAppListViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpAppListViewModel.kt new file mode 100644 index 0000000000..de26eded8d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpAppListViewModel.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.signal.appsettings.totpapplist.TotpAppListAction +import org.signal.appsettings.totpapplist.TotpAppListEvent +import org.signal.appsettings.totpapplist.TotpAppListState +import org.signal.appsettings.totpapplist.TotpAppListState.Dialog +import org.signal.appsettings.totpapplist.TotpAppListState.LoadState +import org.signal.core.ui.compose.EventDrivenViewModel +import org.signal.core.util.logging.Log + +/** + * Drives the screen that lists the authenticator apps on the account. + */ +class TotpAppListViewModel( + private val repository: TotpRepository = TotpRepository() +) : EventDrivenViewModel(TAG) { + + companion object { + private val TAG = Log.tag(TotpAppListViewModel::class) + } + + private val _state = MutableStateFlow(TotpAppListState(maxApps = repository.getMaxApps())) + private val _actions = Channel(Channel.BUFFERED) + + val state: StateFlow = _state.asStateFlow() + val actions: Flow = _actions.receiveAsFlow() + + init { + viewModelScope.launch { refresh() } + } + + override suspend fun processEvent(event: TotpAppListEvent) { + when (event) { + TotpAppListEvent.ScreenResumed -> { + refresh() + } + TotpAppListEvent.NavigateBackClicked -> { + _actions.send(TotpAppListAction.NavigateBack) + } + TotpAppListEvent.AddTotpAppClicked -> { + if (_state.value.atMaxApps) { + _state.update { it.copy(dialog = Dialog.MaxAppsReached) } + } else { + _actions.send(TotpAppListAction.NavigateToSetup) + } + } + TotpAppListEvent.LearnMoreClicked -> { + _actions.send(TotpAppListAction.OpenLearnMore) + } + is TotpAppListEvent.RenameAppClicked -> { + val app = _state.value.apps.firstOrNull { it.id == event.appId } + if (app == null) { + Log.w(TAG, "Asked to rename an app that isn't in the list.") + } else { + _actions.send(TotpAppListAction.NavigateToRename(app)) + } + } + is TotpAppListEvent.RemoveAppClicked -> { + _state.update { it.copy(dialog = Dialog.ConfirmRemove(event.appId)) } + } + is TotpAppListEvent.RemoveAppConfirmed -> { + _state.update { it.copy(dialog = Dialog.None) } + removeApp(event.appId) + } + TotpAppListEvent.DialogDismissed -> { + _state.update { it.copy(dialog = Dialog.None) } + } + } + } + + private suspend fun removeApp(appId: Long) { + when (repository.removeTotpApp(appId)) { + TotpRepository.UpdateResult.Success, TotpRepository.UpdateResult.AppNotFound -> { + _actions.send(TotpAppListAction.ShowTotpAppRemoved) + refresh() + } + TotpRepository.UpdateResult.NetworkFailure -> { + Log.w(TAG, "Couldn't remove the authenticator app. Leaving it in the list, where it still is.") + _actions.send(TotpAppListAction.ShowRemovalFailed) + } + } + } + + private suspend fun refresh() { + when (val result = repository.getTotpApps()) { + is TotpRepository.AppsResult.Success -> { + _state.update { it.copy(apps = result.apps, loadState = LoadState.LOADED) } + } + TotpRepository.AppsResult.NetworkFailure -> { + Log.w(TAG, "Couldn't reach the service to list authenticator apps.") + _state.update { it.copy(apps = emptyList(), loadState = LoadState.NETWORK_FAILURE) } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpCodeEntryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpCodeEntryFragment.kt new file mode 100644 index 0000000000..d51bdc0e99 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpCodeEntryFragment.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.fragment.findNavController +import org.signal.appsettings.totpcodeentry.TotpCodeEntryAction +import org.signal.appsettings.totpcodeentry.TotpCodeEntryScreen +import org.signal.core.ui.compose.CollectActions +import org.signal.core.ui.compose.ComposeFragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +/** + * Wrapper around [org.signal.appsettings.totpcodeentry.TotpCodeEntryScreen] + */ +class TotpCodeEntryFragment : ComposeFragment() { + + /** A one-time code is not a credential, and a password manager offering to save one every time is pure noise. */ + override val autofillEnabled: Boolean = false + + private val viewModel: TotpCodeEntryViewModel by viewModels() + + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() + + CollectActions(viewModel.actions) { action -> handleAction(action) } + + TotpCodeEntryScreen( + state = state, + onEvent = viewModel::onEvent + ) + } + + private fun handleAction(action: TotpCodeEntryAction) { + when (action) { + TotpCodeEntryAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() + is TotpCodeEntryAction.NavigateToNaming -> { + val args = Bundle().apply { putLong(TotpNavArgs.ARG_APP_ID, action.appId) } + findNavController().safeNavigate(R.id.action_authenticatorCodeEntryFragment_to_authenticatorNameFragment, args) + } + TotpCodeEntryAction.NavigateToSetup -> findNavController().popBackStack(R.id.authenticatorSetupFragment, false) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpCodeEntryViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpCodeEntryViewModel.kt new file mode 100644 index 0000000000..3d47f1bd39 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpCodeEntryViewModel.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import org.signal.appsettings.totpcodeentry.TotpCodeEntryAction +import org.signal.appsettings.totpcodeentry.TotpCodeEntryEvent +import org.signal.appsettings.totpcodeentry.TotpCodeEntryState +import org.signal.appsettings.totpcodeentry.TotpCodeEntryState.Error +import org.signal.core.ui.compose.EventDrivenViewModel +import org.signal.core.util.logging.Log + +/** + * Drives the screen that collects a code from the user's authenticator app, which is how the service learns the user + * kept a copy of the key it just handed out. + */ +class TotpCodeEntryViewModel( + private val repository: TotpRepository = TotpRepository() +) : EventDrivenViewModel(TAG) { + + companion object { + private val TAG = Log.tag(TotpCodeEntryViewModel::class) + } + + private val _state = MutableStateFlow(TotpCodeEntryState()) + private val _actions = Channel(Channel.BUFFERED) + + val state: StateFlow = _state.asStateFlow() + val actions: Flow = _actions.receiveAsFlow() + + override suspend fun processEvent(event: TotpCodeEntryEvent) { + when (event) { + TotpCodeEntryEvent.NavigateBackClicked -> { + _actions.send(TotpCodeEntryAction.NavigateBack) + } + is TotpCodeEntryEvent.CodeChanged -> { + val digits = event.code.filter { it.isDigit() }.take(TotpCodeEntryState.CODE_LENGTH) + _state.update { it.copy(code = digits, error = Error.None) } + } + TotpCodeEntryEvent.DoneClicked -> { + if (!_state.value.canSubmit) { + return + } + + _state.update { it.copy(submitting = true, error = Error.None) } + + confirmNewApp() + } + } + } + + private suspend fun confirmNewApp() { + when (val result = repository.confirmPendingApp(_state.value.code)) { + is TotpRepository.ConfirmResult.Success -> { + _actions.send(TotpCodeEntryAction.NavigateToNaming(result.appId)) + } + TotpRepository.ConfirmResult.IncorrectCode -> { + fail(Error.IncorrectCode) + } + TotpRepository.ConfirmResult.TooManyApps -> { + Log.w(TAG, "The account filled up while this key was pending. Going back to setup, which will explain the limit.") + _state.update { it.copy(submitting = false) } + _actions.send(TotpCodeEntryAction.NavigateToSetup) + } + TotpRepository.ConfirmResult.NetworkFailure -> { + fail(Error.NetworkFailure) + } + } + } + + private fun fail(error: Error) { + _state.update { it.copy(submitting = false, error = error) } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNameEntryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNameEntryFragment.kt new file mode 100644 index 0000000000..b5d18cd3db --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNameEntryFragment.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.fragment.findNavController +import org.signal.appsettings.totpnameentry.TotpNameEntryAction +import org.signal.appsettings.totpnameentry.TotpNameEntryScreen +import org.signal.core.ui.compose.CollectActions +import org.signal.core.ui.compose.ComposeFragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.viewModel +import org.signal.appsettings.R as AppSettingsR + +/** + * Wrapper around [TotpNameEntryScreen]. + */ +class TotpNameEntryFragment : ComposeFragment() { + + /** The name the user gives an authenticator app is not a credential, so there's nothing here worth offering to save. */ + override val autofillEnabled: Boolean = false + + private val viewModel: TotpNameEntryViewModel by viewModel { + TotpNameEntryViewModel( + appId = TotpNavArgs.appId(arguments) ?: TotpNavArgs.NO_APP_ID, + renamedApp = TotpNavArgs.renamedApp(arguments) + ) + } + + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() + + CollectActions(viewModel.actions) { action -> handleAction(action) } + + TotpNameEntryScreen( + state = state, + onEvent = viewModel::onEvent + ) + } + + private fun handleAction(action: TotpNameEntryAction) { + when (action) { + TotpNameEntryAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() + TotpNameEntryAction.NavigateToTotpAppList -> findNavController().popBackStack(R.id.authenticatorAppsFragment, false) + TotpNameEntryAction.ShowTotpAppSetUp -> toast(AppSettingsR.string.TotpNameEntryScreen__authenticator_app_set_up) + TotpNameEntryAction.ShowTotpAppRenamed -> toast(AppSettingsR.string.TotpNameEntryScreen__authenticator_app_renamed) + TotpNameEntryAction.ShowNameNotSaved -> toast(AppSettingsR.string.TotpNameEntryScreen__couldnt_save_name) + } + } + + private fun toast(@StringRes message: Int) { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNameEntryViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNameEntryViewModel.kt new file mode 100644 index 0000000000..bda3d99fa5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNameEntryViewModel.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import org.signal.appsettings.totpapplist.TotpApp +import org.signal.appsettings.totpnameentry.TotpNameEntryAction +import org.signal.appsettings.totpnameentry.TotpNameEntryEvent +import org.signal.appsettings.totpnameentry.TotpNameEntryState +import org.signal.core.ui.compose.EventDrivenViewModel +import org.signal.core.util.BreakIteratorCompat +import org.signal.core.util.StringUtil +import org.signal.core.util.logging.Log + +/** + * Handles both naming and renaming TOTP apps. + */ +class TotpNameEntryViewModel( + private val appId: Long, + private val renamedApp: TotpApp? = null, + private val repository: TotpRepository = TotpRepository() +) : EventDrivenViewModel(TAG) { + + companion object { + private val TAG = Log.tag(TotpNameEntryViewModel::class) + } + + private val breakIterator = BreakIteratorCompat.getInstance() + + private val _state = MutableStateFlow( + TotpNameEntryState( + renaming = renamedApp != null, + name = renamedApp?.name?.trimNameToLengthLimits() ?: "" + ) + ) + private val _actions = Channel(Channel.BUFFERED) + + val state: StateFlow = _state.asStateFlow() + val actions: Flow = _actions.receiveAsFlow() + + override suspend fun processEvent(event: TotpNameEntryEvent) { + when (event) { + TotpNameEntryEvent.NavigateBackClicked -> { + _actions.send(TotpNameEntryAction.NavigateBack) + } + is TotpNameEntryEvent.NameChanged -> { + _state.update { it.copy(name = event.name.trimNameToLengthLimits()) } + } + TotpNameEntryEvent.NextClicked -> { + if (!_state.value.canSubmit) { + return + } + + val name = _state.value.name.trim() + _state.update { it.copy(submitting = true) } + + val result = if (renamedApp != null) { + repository.renameTotpApp(renamedApp, name) + } else { + repository.nameNewTotpApp(appId, name) + } + + when (result) { + TotpRepository.UpdateResult.Success -> { + _actions.send(if (renamedApp != null) TotpNameEntryAction.ShowTotpAppRenamed else TotpNameEntryAction.ShowTotpAppSetUp) + _actions.send(TotpNameEntryAction.NavigateToTotpAppList) + } + TotpRepository.UpdateResult.AppNotFound -> { + Log.w(TAG, "Asked to name an app the service doesn't have. Going back to the list rather than stranding the user here.") + _actions.send(TotpNameEntryAction.NavigateToTotpAppList) + } + TotpRepository.UpdateResult.NetworkFailure -> { + _state.update { it.copy(submitting = false) } + _actions.send(TotpNameEntryAction.ShowNameNotSaved) + } + } + } + } + } + + /** Keeps totp name within length limits */ + private fun String.trimNameToLengthLimits(): String { + val input = this + val graphemeTruncated = breakIterator + .apply { setText(input) } + .take(TotpRepository.MAX_NAME_LENGTH_GRAPHEMES) + .toString() + + return StringUtil.trimToFit(graphemeTruncated, TotpRepository.MAX_NAME_LENGTH_BYTES) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNavArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNavArgs.kt new file mode 100644 index 0000000000..cd46b11b80 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNavArgs.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import android.os.Bundle +import org.signal.appsettings.totpapplist.TotpApp + +/** + * The nav arguments the authenticator app screens pass between each other, and the parsing that turns them back into + * something typed. Keep the values in sync with the argument defaults declared for these destinations in + * app_settings_with_change_number.xml. + */ +object TotpNavArgs { + + /** The app being acted on. [NO_APP_ID] when nothing identified one. */ + const val ARG_APP_ID = "app_id" + const val NO_APP_ID = -1L + + /** The rest of the app being renamed. Absent when the screen is naming a newly paired app instead. */ + const val ARG_APP_NAME = "app_name" + const val ARG_APP_CREATED_AT = "app_created_at" + const val NO_CREATED_AT = -1L + + /** The app id in [arguments], or null when there isn't one. */ + fun appId(arguments: Bundle?): Long? = arguments?.getLong(ARG_APP_ID, NO_APP_ID)?.takeIf { it != NO_APP_ID } + + /** Packs [app] into [bundle] for the rename flow, so the name screen doesn't have to fetch what the list already had. */ + fun putRenamedApp(bundle: Bundle, app: TotpApp) { + bundle.putLong(ARG_APP_ID, app.id) + bundle.putString(ARG_APP_NAME, app.name) + bundle.putLong(ARG_APP_CREATED_AT, app.createdAt) + } + + /** The app being renamed, or null when [arguments] describe naming a newly paired app rather than a rename. */ + fun renamedApp(arguments: Bundle?): TotpApp? { + val appId = appId(arguments) ?: return null + val createdAt = arguments?.getLong(ARG_APP_CREATED_AT, NO_CREATED_AT)?.takeIf { it != NO_CREATED_AT } ?: return null + + return TotpApp(id = appId, name = arguments.getString(ARG_APP_NAME).orEmpty(), createdAt = createdAt) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpRepository.kt new file mode 100644 index 0000000000..bb08b7ec6f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpRepository.kt @@ -0,0 +1,243 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import org.signal.appsettings.totpapplist.TotpApp +import org.signal.core.util.Base32 +import org.signal.core.util.logging.Log +import org.signal.libsignal.net.RequestResult +import java.net.URLEncoder +import java.time.Instant + +/** + * Everything the authenticator app screens need, sitting between them and the TOTP operations on [TotpApi]. + */ +class TotpRepository( + private val api: TotpApi = SHARED_API, + private val clock: () -> Long = System::currentTimeMillis +) { + + companion object { + private val TAG = Log.tag(TotpRepository::class) + + /** Shared so that every screen in the flow sees the same state until there's a service behind this. */ + private val SHARED_API: TotpApi = InMemoryTotpApi() + + private const val ISSUER = "Signal" + + /** What the service uses, and what every authenticator app supports without reading a single URI parameter. */ + private const val ALGORITHM = "SHA1" + private const val DIGITS = 6 + private const val PERIOD_SECONDS = 30 + + /** How many characters of the display form go between spaces. */ + private const val DISPLAY_GROUP_SIZE = 4 + + const val MAX_NAME_LENGTH_BYTES = TotpApi.Metadata.NAME_MAX_LENGTH + const val MAX_NAME_LENGTH_GRAPHEMES = 30 + } + + fun getMaxApps(): Int { + return TotpApi.MAX_KEYS + } + + /** + * Asks the service for a new key, returning what the setup screen needs to hand it to an authenticator app. The + * service holds the pending key from here until [confirmPendingApp], so nothing is kept on this side. + */ + suspend fun beginSetup(accountName: String): BeginSetupResult { + return when (val result = api.generateKey()) { + is RequestResult.Success -> { + val key = result.result.key + + BeginSetupResult.Success( + setupUri = buildSetupUri(key = key, accountName = accountName), + displayKey = Base32.encode(key).chunked(DISPLAY_GROUP_SIZE).joinToString(" "), + clipboardKey = Base32.encode(key) + ) + } + is RequestResult.NonSuccess -> { + when (result.error) { + TotpApi.GenerateKeyError.TooManyKeys -> BeginSetupResult.TooManyApps + } + } + is RequestResult.RetryableNetworkError -> { + Log.w(TAG, "Couldn't generate a key.", result.networkError) + BeginSetupResult.NetworkFailure + } + is RequestResult.ApplicationError -> { + Log.w(TAG, "Couldn't generate a key.", result.cause) + BeginSetupResult.NetworkFailure + } + } + } + + /** + * Confirms the pending key with a code from the user's authenticator app. + * + * The key is confirmed without a name, because the service wants metadata at confirmation time and the user doesn't + * name their app until the screen after this one. Naming it later means a brief window where a key has no name, which + * is a better failure than a window where the second factor isn't active yet. + */ + suspend fun confirmPendingApp(code: String): ConfirmResult { + val oneTimePassword = code.toIntOrNull() ?: return ConfirmResult.IncorrectCode + + val metadata = TotpApi.Metadata(name = "", createdAt = Instant.ofEpochMilli(clock())) + + return when (val result = api.confirmKey(oneTimePassword = oneTimePassword, metadata = metadata)) { + is RequestResult.Success -> { + ConfirmResult.Success(appId = result.result.toLong()) + } + is RequestResult.NonSuccess -> when (result.error) { + TotpApi.ConfirmKeyError.NotVerified -> ConfirmResult.IncorrectCode + TotpApi.ConfirmKeyError.TooManyKeys -> { + Log.w(TAG, "The account filled up with keys between generating this one and confirming it.") + ConfirmResult.TooManyApps + } + } + is RequestResult.RetryableNetworkError -> { + Log.w(TAG, "Couldn't confirm the pending key.", result.networkError) + ConfirmResult.NetworkFailure + } + is RequestResult.ApplicationError -> { + Log.w(TAG, "Couldn't confirm the pending key.", result.cause) + ConfirmResult.NetworkFailure + } + } + } + + /** The authenticator apps on the account, newest id last. */ + suspend fun getTotpApps(): AppsResult { + return when (val result = api.listKeys()) { + is RequestResult.Success -> { + AppsResult.Success( + result.result.map { key -> + TotpApp( + id = key.keyId.toLong(), + name = key.metadata.name, + createdAt = key.metadata.createdAt.toEpochMilli() + ) + } + ) + } + is RequestResult.RetryableNetworkError -> { + Log.w(TAG, "Couldn't list keys.", result.networkError) + AppsResult.NetworkFailure + } + is RequestResult.ApplicationError -> { + Log.w(TAG, "Couldn't list keys.", result.cause) + AppsResult.NetworkFailure + } + is RequestResult.NonSuccess -> error("Code branch is unreachable") + } + } + + /** Renames [app], which means handing the whole metadata blob back to the service. */ + suspend fun renameTotpApp(app: TotpApp, name: String): UpdateResult { + return setMetadata(app.id, TotpApi.Metadata(name = name, createdAt = Instant.ofEpochMilli(app.createdAt))) + } + + /** Names a newly confirmed app, which was confirmed without one moments ago. */ + suspend fun nameNewTotpApp(appId: Long, name: String): UpdateResult { + return setMetadata(appId, TotpApi.Metadata(name = name, createdAt = Instant.ofEpochMilli(clock()))) + } + + suspend fun removeTotpApp(appId: Long): UpdateResult { + return when (val result = api.removeKey(appId.toInt())) { + is RequestResult.Success -> UpdateResult.Success + is RequestResult.RetryableNetworkError -> { + Log.w(TAG, "Couldn't remove the key.", result.networkError) + UpdateResult.NetworkFailure + } + is RequestResult.ApplicationError -> { + Log.w(TAG, "Couldn't remove the key.", result.cause) + UpdateResult.NetworkFailure + } + is RequestResult.NonSuccess -> error("Code branch is unreachable") + } + } + + private suspend fun setMetadata(appId: Long, metadata: TotpApi.Metadata): UpdateResult { + return when (val result = api.setKeyMetadata(keyId = appId.toInt(), metadata = metadata)) { + is RequestResult.Success -> UpdateResult.Success + is RequestResult.NonSuccess -> when (result.error) { + TotpApi.SetKeyMetadataError.KeyNotFound -> UpdateResult.AppNotFound + } + is RequestResult.RetryableNetworkError -> { + Log.w(TAG, "Couldn't set key metadata.", result.networkError) + UpdateResult.NetworkFailure + } + is RequestResult.ApplicationError -> { + Log.w(TAG, "Couldn't set key metadata.", result.cause) + UpdateResult.NetworkFailure + } + } + } + + /** + * The `otpauth://` URI that hands the key to an authenticator app, following the de facto Key Uri Format every app + * implements. Note that a lot of apps ignore params like "algorithm", but we set them just in case. + */ + private fun buildSetupUri(key: ByteArray, accountName: String): String { + val label = if (accountName.isBlank()) encode(ISSUER) else "${encode(ISSUER)}:${encode(accountName)}" + + val query = listOf( + "secret" to Base32.encode(key), + "issuer" to ISSUER, + "algorithm" to ALGORITHM, + "digits" to DIGITS.toString(), + "period" to PERIOD_SECONDS.toString() + ).joinToString("&") { (name, value) -> "$name=${encode(value)}" } + + return "otpauth://totp/$label?$query" + } + + /** + * [URLEncoder] targets form encoding rather than URIs, so it renders a space as `+` where a URI needs `%20`, and + * escapes `~` where a URI leaves it alone. + */ + private fun encode(value: String): String { + return URLEncoder.encode(value, Charsets.UTF_8.name()) + .replace("+", "%20") + .replace("%7E", "~") + } + + sealed interface BeginSetupResult { + data class Success(val setupUri: String, val displayKey: String, val clipboardKey: String) : BeginSetupResult { + override fun toString(): String = "Success()" + } + + /** The account already has as many authenticator apps as it's allowed. */ + data object TooManyApps : BeginSetupResult + + data object NetworkFailure : BeginSetupResult + } + + sealed interface ConfirmResult { + data class Success(val appId: Long) : ConfirmResult + + data object IncorrectCode : ConfirmResult + + /** Another device filled the account up between generating the key and confirming it. */ + data object TooManyApps : ConfirmResult + + data object NetworkFailure : ConfirmResult + } + + sealed interface AppsResult { + data class Success(val apps: List) : AppsResult + + data object NetworkFailure : AppsResult + } + + sealed interface UpdateResult { + data object Success : UpdateResult + + data object AppNotFound : UpdateResult + + data object NetworkFailure : UpdateResult + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorSetupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpSetupFragment.kt similarity index 53% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorSetupFragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpSetupFragment.kt index 0ec0b8ed0a..47294a8905 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorSetupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpSetupFragment.kt @@ -15,9 +15,9 @@ import androidx.compose.runtime.getValue import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.fragment.findNavController -import org.signal.appsettings.authenticatorsetup.AuthenticatorSetupAction -import org.signal.appsettings.authenticatorsetup.AuthenticatorSetupEvent -import org.signal.appsettings.authenticatorsetup.AuthenticatorSetupScreen +import org.signal.appsettings.totpsetup.TotpSetupAction +import org.signal.appsettings.totpsetup.TotpSetupEvent +import org.signal.appsettings.totpsetup.TotpSetupScreen import org.signal.core.ui.compose.CollectActions import org.signal.core.ui.compose.ComposeFragment import org.signal.core.util.Util @@ -27,16 +27,16 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.signal.appsettings.R as AppSettingsR /** - * Walks the user through setting up an authenticator app. Carries out the [AuthenticatorSetupAction]s that need an + * Walks the user through setting up an authenticator app. Carries out the [TotpSetupAction]s that need an * Activity or the nav graph. */ -class AuthenticatorSetupFragment : ComposeFragment() { +class TotpSetupFragment : ComposeFragment() { companion object { - private val TAG = Log.tag(AuthenticatorSetupFragment::class) + private val TAG = Log.tag(TotpSetupFragment::class) } - private val viewModel: AuthenticatorSetupViewModel by viewModels() + private val viewModel: TotpSetupViewModel by viewModels() @Composable override fun FragmentContent() { @@ -44,29 +44,29 @@ class AuthenticatorSetupFragment : ComposeFragment() { CollectActions(viewModel.actions) { action -> handleAction(action) } - AuthenticatorSetupScreen( + TotpSetupScreen( state = state, onEvent = viewModel::onEvent ) } - private fun handleAction(action: AuthenticatorSetupAction) { + private fun handleAction(action: TotpSetupAction) { when (action) { - AuthenticatorSetupAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() - is AuthenticatorSetupAction.LaunchAuthenticatorApp -> launchAuthenticatorApp(action.uri) - is AuthenticatorSetupAction.CopyKeyToClipboard -> Util.copyToClipboard(requireContext(), action.key) - AuthenticatorSetupAction.ShowKeyCopied -> toast(AppSettingsR.string.AuthenticatorSetupScreen__copied_to_clipboard) - AuthenticatorSetupAction.ShowNoAuthenticatorAppFound -> toast(AppSettingsR.string.AuthenticatorSetupScreen__no_authenticator_app_found) - AuthenticatorSetupAction.NavigateToCodeEntry -> findNavController().safeNavigate(R.id.action_authenticatorSetupFragment_to_authenticatorCodeEntryFragment) + TotpSetupAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() + is TotpSetupAction.LaunchTotpApp -> launchTotpApp(action.uri) + is TotpSetupAction.CopyKeyToClipboard -> Util.copyToClipboard(requireContext(), action.key) + TotpSetupAction.ShowKeyCopied -> toast(AppSettingsR.string.TotpSetupScreen__copied_to_clipboard) + TotpSetupAction.ShowNoTotpAppFound -> toast(AppSettingsR.string.TotpSetupScreen__no_authenticator_app_found) + TotpSetupAction.NavigateToCodeEntry -> findNavController().safeNavigate(R.id.action_authenticatorSetupFragment_to_authenticatorCodeEntryFragment) } } - private fun launchAuthenticatorApp(uri: String) { + private fun launchTotpApp(uri: String) { try { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(uri))) } catch (e: ActivityNotFoundException) { Log.w(TAG, "No app is willing to handle the authenticator setup link.", e) - viewModel.onEvent(AuthenticatorSetupEvent.NoAuthenticatorAppFound) + viewModel.onEvent(TotpSetupEvent.NoTotpAppFound) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpSetupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpSetupViewModel.kt new file mode 100644 index 0000000000..9fca5b0351 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpSetupViewModel.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.signal.appsettings.totpsetup.TotpSetupAction +import org.signal.appsettings.totpsetup.TotpSetupEvent +import org.signal.appsettings.totpsetup.TotpSetupState +import org.signal.appsettings.totpsetup.TotpSetupState.Dialog +import org.signal.core.ui.compose.EventDrivenViewModel +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.keyvalue.SignalStore +import java.util.UUID + +class TotpSetupViewModel( + private val repository: TotpRepository = TotpRepository(), + private val accountName: String = accountNameFor(SignalStore.account.aci?.rawUuid) +) : EventDrivenViewModel(TAG) { + + companion object { + private val TAG = Log.tag(TotpSetupViewModel::class) + + @VisibleForTesting + fun accountNameFor(aci: UUID?): String = aci?.toString()?.substringBefore('-')?.uppercase().orEmpty() + } + + private val _state = MutableStateFlow(TotpSetupState()) + private val _actions = Channel(Channel.BUFFERED) + + val state: StateFlow = _state.asStateFlow() + val actions: Flow = _actions.receiveAsFlow() + + /** The URI and clipboard forms of the key, which differ from the grouped form the screen shows. */ + private var setupUri: String = "" + private var clipboardKey: String = "" + + init { + viewModelScope.launch { beginSetup() } + } + + override suspend fun processEvent(event: TotpSetupEvent) { + when (event) { + TotpSetupEvent.NavigateBackClicked -> { + _actions.send(TotpSetupAction.NavigateBack) + } + TotpSetupEvent.OpenTotpAppClicked -> { + _actions.send(TotpSetupAction.LaunchTotpApp(setupUri)) + } + TotpSetupEvent.CopyKeyClicked -> { + _actions.send(TotpSetupAction.CopyKeyToClipboard(clipboardKey)) + _actions.send(TotpSetupAction.ShowKeyCopied) + } + TotpSetupEvent.NoTotpAppFound -> { + _actions.send(TotpSetupAction.ShowNoTotpAppFound) + } + TotpSetupEvent.ContinueClicked -> { + _actions.send(TotpSetupAction.NavigateToCodeEntry) + } + TotpSetupEvent.DialogDismissed -> { + _state.update { it.copy(dialog = Dialog.None) } + _actions.send(TotpSetupAction.NavigateBack) + } + } + } + + private suspend fun beginSetup() { + when (val result = repository.beginSetup(accountName)) { + is TotpRepository.BeginSetupResult.Success -> { + setupUri = result.setupUri + clipboardKey = result.clipboardKey + _state.update { it.copy(setupKey = result.displayKey, loading = false) } + } + TotpRepository.BeginSetupResult.TooManyApps -> { + Log.w(TAG, "The account already has as many authenticator apps as it's allowed.") + _state.update { it.copy(loading = false, dialog = Dialog.MaxAppsReached(repository.getMaxApps())) } + } + TotpRepository.BeginSetupResult.NetworkFailure -> { + Log.w(TAG, "Couldn't reach the service to start setup.") + _state.update { it.copy(loading = false, dialog = Dialog.NetworkFailure) } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationNetworkController.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationNetworkController.kt index c6e8a1672c..d777d45b50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationNetworkController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationNetworkController.kt @@ -183,7 +183,8 @@ class AppRegistrationNetworkController( pniPreKeys: PreKeyCollection?, fcmToken: String?, skipDeviceTransfer: Boolean, - aci: ACI? + aci: ACI?, + totp: Int? ): RequestResult { return registrationApi.registerAccount( e164 = e164, @@ -196,7 +197,8 @@ class AppRegistrationNetworkController( pniPreKeys = pniPreKeys, fcmToken = fcmToken, skipDeviceTransfer = skipDeviceTransfer, - aci = aci + aci = aci, + totp = totp ) } diff --git a/app/src/main/res/navigation/app_settings_with_change_number.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml index 9e8f03c7be..46f5b02e1a 100644 --- a/app/src/main/res/navigation/app_settings_with_change_number.xml +++ b/app/src/main/res/navigation/app_settings_with_change_number.xml @@ -246,7 +246,7 @@ - + - + - - - + + diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt index d184972f47..f0a32cdea4 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt @@ -63,7 +63,7 @@ class AccountSettingsViewModelTest { every { repository.isClientDeprecated() } returns false every { repository.getPinKeyboardType() } returns PinKeyboardType.NUMERIC every { repository.isPhoneNumberless() } returns false - every { repository.getAuthenticatorAppCount() } returns 0 + coEvery { repository.getTotpAppCount() } returns 0 every { repository.getPasskeyCount() } returns 0 every { repository.verifyLocalPin(any()) } answers { firstArg() == CORRECT_PIN } coEvery { repository.setRegistrationLockEnabled(any()) } returns true @@ -303,16 +303,27 @@ class AccountSettingsViewModelTest { @Test fun `the Signal Login section is filled in when the account is phone-numberless`() = runTest(testDispatcher) { every { repository.isPhoneNumberless() } returns true - every { repository.getAuthenticatorAppCount() } returns 2 + coEvery { repository.getTotpAppCount() } returns 2 every { repository.getPasskeyCount() } returns 8 val viewModel = createViewModel() assertThat(viewModel.state.value.isPhoneNumberless).isTrue() - assertThat(viewModel.state.value.signalLogin?.authenticatorAppCount).isEqualTo(2) + assertThat(viewModel.state.value.signalLogin?.totpAppCount).isEqualTo(2) assertThat(viewModel.state.value.signalLogin?.passkeyCount).isEqualTo(8) } + /** Zero would render as "no authenticator apps", which is a claim we can't make when we couldn't reach the service. */ + @Test + fun `a count we couldn't fetch is null rather than zero`() = runTest(testDispatcher) { + every { repository.isPhoneNumberless() } returns true + coEvery { repository.getTotpAppCount() } returns null + + val viewModel = createViewModel() + + assertThat(viewModel.state.value.signalLogin?.totpAppCount).isNull() + } + @Test fun `AccountAndRecoveryClicked opens the Signal Login details screen`() = runTest(testDispatcher) { every { repository.isPhoneNumberless() } returns true @@ -326,15 +337,15 @@ class AccountSettingsViewModelTest { } @Test - fun `AuthenticatorAppClicked opens the authenticator apps screen`() = runTest(testDispatcher) { + fun `TotpAppClicked opens the authenticator apps screen`() = runTest(testDispatcher) { every { repository.isPhoneNumberless() } returns true val viewModel = createViewModel() val actions = collectActions(viewModel.actions) - viewModel.onEvent(AccountSettingsEvent.AuthenticatorAppClicked) + viewModel.onEvent(AccountSettingsEvent.TotpAppClicked) - assertThat(actions.last()).isEqualTo(AccountSettingsAction.NavigateToAuthenticatorApps) + assertThat(actions.last()).isEqualTo(AccountSettingsAction.NavigateToTotpAppList) } @Test diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppsViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppsViewModelTest.kt deleted file mode 100644 index dcc6f8500c..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorAppsViewModelTest.kt +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import assertk.assertThat -import assertk.assertions.containsExactly -import assertk.assertions.isEmpty -import assertk.assertions.isEqualTo -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.signal.appsettings.authenticatorapps.AuthenticatorApp -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsAction -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsEvent -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsState.Dialog -import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule - -@OptIn(ExperimentalCoroutinesApi::class) -class AuthenticatorAppsViewModelTest { - - companion object { - private val APP_ONE = AuthenticatorApp(id = 1, name = "Bitwarden Authenticator", createdAt = 0) - private val APP_TWO = AuthenticatorApp(id = 2, name = "Twilio Authy", createdAt = 0) - } - - private val testDispatcher = UnconfinedTestDispatcher() - private val repository: AuthenticatorRepository = mockk(relaxed = true) - - @get:Rule - val dispatcherRule = CoroutineDispatcherRule(testDispatcher) - - @Before - fun setUp() { - Dispatchers.setMain(testDispatcher) - - every { repository.getMaxApps() } returns 2 - every { repository.getAuthenticatorApps() } returns emptyList() - } - - @After - fun tearDown() { - Dispatchers.resetMain() - } - - @Test - fun `the configured apps are read on creation`() = runTest(testDispatcher) { - every { repository.getAuthenticatorApps() } returns listOf(APP_ONE) - - val viewModel = createViewModel() - - assertThat(viewModel.state.value.apps).containsExactly(APP_ONE) - } - - @Test - fun `ScreenResumed picks up apps added elsewhere`() = runTest(testDispatcher) { - val viewModel = createViewModel() - - assertThat(viewModel.state.value.apps).isEmpty() - - every { repository.getAuthenticatorApps() } returns listOf(APP_ONE) - viewModel.onEvent(AuthenticatorAppsEvent.ScreenResumed) - - assertThat(viewModel.state.value.apps).containsExactly(APP_ONE) - } - - @Test - fun `AddAuthenticatorAppClicked opens setup when there's room for another app`() = runTest(testDispatcher) { - every { repository.getAuthenticatorApps() } returns listOf(APP_ONE) - - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorAppsEvent.AddAuthenticatorAppClicked) - - assertThat(actions.last()).isEqualTo(AuthenticatorAppsAction.NavigateToSetup) - } - - @Test - fun `AddAuthenticatorAppClicked explains the limit when there's no room for another app`() = runTest(testDispatcher) { - every { repository.getAuthenticatorApps() } returns listOf(APP_ONE, APP_TWO) - - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorAppsEvent.AddAuthenticatorAppClicked) - - assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.MaxAppsReached) - assertThat(actions).isEmpty() - } - - @Test - fun `RenameAppClicked opens the naming screen for that app`() = runTest(testDispatcher) { - every { repository.getAuthenticatorApps() } returns listOf(APP_ONE) - - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorAppsEvent.RenameAppClicked(APP_ONE.id)) - - assertThat(actions.last()).isEqualTo(AuthenticatorAppsAction.NavigateToRename(APP_ONE.id)) - } - - @Test - fun `RemoveAppClicked asks the user to confirm first`() = runTest(testDispatcher) { - every { repository.getAuthenticatorApps() } returns listOf(APP_ONE) - - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorAppsEvent.RemoveAppClicked(APP_ONE.id)) - - assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.ConfirmRemove(APP_ONE.id)) - assertThat(actions).isEmpty() - } - - @Test - fun `RemoveAppConfirmed collects a code before removing the app`() = runTest(testDispatcher) { - every { repository.getAuthenticatorApps() } returns listOf(APP_ONE) - - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorAppsEvent.RemoveAppClicked(APP_ONE.id)) - viewModel.onEvent(AuthenticatorAppsEvent.RemoveAppConfirmed) - - assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.None) - assertThat(actions.last()).isEqualTo(AuthenticatorAppsAction.NavigateToRemovalCodeEntry(APP_ONE.id)) - } - - @Test - fun `RemoveAppConfirmed does nothing when no removal is pending`() = runTest(testDispatcher) { - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorAppsEvent.RemoveAppConfirmed) - - assertThat(actions).isEmpty() - } - - @Test - fun `DialogDismissed clears the dialog`() = runTest(testDispatcher) { - every { repository.getAuthenticatorApps() } returns listOf(APP_ONE) - - val viewModel = createViewModel() - - viewModel.onEvent(AuthenticatorAppsEvent.RemoveAppClicked(APP_ONE.id)) - viewModel.onEvent(AuthenticatorAppsEvent.DialogDismissed) - - assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.None) - } - - @Test - fun `NavigateBackClicked leaves the screen`() = runTest(testDispatcher) { - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorAppsEvent.NavigateBackClicked) - - assertThat(actions.last()).isEqualTo(AuthenticatorAppsAction.NavigateBack) - } - - @Test - fun `LearnMoreClicked opens the support article`() = runTest(testDispatcher) { - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorAppsEvent.LearnMoreClicked) - - assertThat(actions.last()).isEqualTo(AuthenticatorAppsAction.OpenLearnMore) - } - - private fun createViewModel() = AuthenticatorAppsViewModel(repository) - - private fun TestScope.collectActions(actions: Flow): List { - val collected = mutableListOf() - backgroundScope.launch { actions.toList(collected) } - return collected - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorCodeEntryViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorCodeEntryViewModelTest.kt deleted file mode 100644 index 2702fa8fdc..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorCodeEntryViewModelTest.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import assertk.assertThat -import assertk.assertions.contains -import assertk.assertions.isEmpty -import assertk.assertions.isEqualTo -import assertk.assertions.isFalse -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryAction -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryEvent -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryState.Purpose -import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule - -@OptIn(ExperimentalCoroutinesApi::class) -class AuthenticatorCodeEntryViewModelTest { - - companion object { - private const val FULL_CODE = "123456" - } - - private val testDispatcher = UnconfinedTestDispatcher() - - @get:Rule - val dispatcherRule = CoroutineDispatcherRule(testDispatcher) - - @Before - fun setUp() { - Dispatchers.setMain(testDispatcher) - clearApps() - } - - @After - fun tearDown() { - Dispatchers.resetMain() - clearApps() - } - - @Test - fun `non-digits are dropped and the code is capped at six digits`() = runTest(testDispatcher) { - val viewModel = createViewModel() - - viewModel.onEvent(AuthenticatorCodeEntryEvent.CodeChanged("12a34 5678")) - - assertThat(viewModel.state.value.code).isEqualTo(FULL_CODE) - } - - @Test - fun `a partial code can't be submitted`() = runTest(testDispatcher) { - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorCodeEntryEvent.CodeChanged("123")) - - assertThat(viewModel.state.value.canSubmit).isFalse() - - viewModel.onEvent(AuthenticatorCodeEntryEvent.DoneClicked) - - assertThat(actions).isEmpty() - } - - @Test - fun `a full code entered while adding sends the user on to name the app`() = runTest(testDispatcher) { - val viewModel = createViewModel(purpose = Purpose.Add) - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorCodeEntryEvent.CodeChanged(FULL_CODE)) - viewModel.onEvent(AuthenticatorCodeEntryEvent.DoneClicked) - - assertThat(actions.last()).isEqualTo(AuthenticatorCodeEntryAction.NavigateToNaming) - } - - @Test - fun `a full code entered while removing removes the app and goes back to the list`() = runTest(testDispatcher) { - val appId = AuthenticatorAppStore.addApp(name = "Bitwarden Authenticator", createdAt = 0) - val viewModel = createViewModel(purpose = Purpose.Remove(appId)) - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorCodeEntryEvent.CodeChanged(FULL_CODE)) - viewModel.onEvent(AuthenticatorCodeEntryEvent.DoneClicked) - - assertThat(AuthenticatorAppStore.getApps()).isEmpty() - assertThat(actions).contains(AuthenticatorCodeEntryAction.ShowAuthenticatorAppRemoved) - assertThat(actions.last()).isEqualTo(AuthenticatorCodeEntryAction.NavigateToAuthenticatorApps) - } - - @Test - fun `NavigateBackClicked leaves the screen`() = runTest(testDispatcher) { - val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorCodeEntryEvent.NavigateBackClicked) - - assertThat(actions.last()).isEqualTo(AuthenticatorCodeEntryAction.NavigateBack) - } - - private fun createViewModel(purpose: Purpose = Purpose.Add) = AuthenticatorCodeEntryViewModel(purpose = purpose) - - private fun clearApps() { - AuthenticatorAppStore.getApps().forEach { AuthenticatorAppStore.removeApp(it.id) } - } - - private fun TestScope.collectActions(actions: Flow): List { - val collected = mutableListOf() - backgroundScope.launch { actions.toList(collected) } - return collected - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNameViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNameViewModelTest.kt deleted file mode 100644 index c752e6e4cb..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNameViewModelTest.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import assertk.assertThat -import assertk.assertions.contains -import assertk.assertions.isEmpty -import assertk.assertions.isEqualTo -import assertk.assertions.isFalse -import assertk.assertions.isTrue -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.signal.appsettings.authenticatorapps.AuthenticatorApp -import org.signal.appsettings.authenticatorname.AuthenticatorNameAction -import org.signal.appsettings.authenticatorname.AuthenticatorNameEvent -import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule - -@OptIn(ExperimentalCoroutinesApi::class) -class AuthenticatorNameViewModelTest { - - companion object { - private val EXISTING_APP = AuthenticatorApp(id = 7, name = "Twilio Authy", createdAt = 0) - } - - private val testDispatcher = UnconfinedTestDispatcher() - private val repository: AuthenticatorRepository = mockk(relaxed = true) - - @get:Rule - val dispatcherRule = CoroutineDispatcherRule(testDispatcher) - - @Before - fun setUp() { - Dispatchers.setMain(testDispatcher) - - every { repository.getAuthenticatorApp(EXISTING_APP.id) } returns EXISTING_APP - } - - @After - fun tearDown() { - Dispatchers.resetMain() - } - - @Test - fun `naming a new app starts empty and isn't renaming`() = runTest(testDispatcher) { - val viewModel = createViewModel(appId = null) - - assertThat(viewModel.state.value.name).isEqualTo("") - assertThat(viewModel.state.value.renaming).isFalse() - } - - @Test - fun `renaming starts from the app's current name`() = runTest(testDispatcher) { - val viewModel = createViewModel(appId = EXISTING_APP.id) - - assertThat(viewModel.state.value.name).isEqualTo(EXISTING_APP.name) - assertThat(viewModel.state.value.renaming).isTrue() - } - - @Test - fun `a blank name can't be submitted`() = runTest(testDispatcher) { - val viewModel = createViewModel(appId = null) - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorNameEvent.NameChanged(" ")) - - assertThat(viewModel.state.value.canSubmit).isFalse() - - viewModel.onEvent(AuthenticatorNameEvent.NextClicked) - - assertThat(actions).isEmpty() - } - - @Test - fun `NextClicked adds a new app and goes back to the list`() = runTest(testDispatcher) { - val viewModel = createViewModel(appId = null) - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorNameEvent.NameChanged(" Bitwarden Authenticator ")) - viewModel.onEvent(AuthenticatorNameEvent.NextClicked) - - verify { repository.addAuthenticatorApp("Bitwarden Authenticator") } - assertThat(actions).contains(AuthenticatorNameAction.ShowAuthenticatorAppSetUp) - assertThat(actions.last()).isEqualTo(AuthenticatorNameAction.NavigateToAuthenticatorApps) - } - - @Test - fun `NextClicked renames an existing app and goes back to the list`() = runTest(testDispatcher) { - val viewModel = createViewModel(appId = EXISTING_APP.id) - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorNameEvent.NameChanged("Work Authenticator")) - viewModel.onEvent(AuthenticatorNameEvent.NextClicked) - - verify { repository.renameAuthenticatorApp(EXISTING_APP.id, "Work Authenticator") } - assertThat(actions).contains(AuthenticatorNameAction.ShowAuthenticatorAppRenamed) - assertThat(actions.last()).isEqualTo(AuthenticatorNameAction.NavigateToAuthenticatorApps) - } - - @Test - fun `NavigateBackClicked leaves the screen`() = runTest(testDispatcher) { - val viewModel = createViewModel(appId = null) - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorNameEvent.NavigateBackClicked) - - assertThat(actions.last()).isEqualTo(AuthenticatorNameAction.NavigateBack) - } - - private fun createViewModel(appId: Long?) = AuthenticatorNameViewModel(appId, repository) - - private fun TestScope.collectActions(actions: Flow): List { - val collected = mutableListOf() - backgroundScope.launch { actions.toList(collected) } - return collected - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNavArgsTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNavArgsTest.kt deleted file mode 100644 index 0f0b2f37d8..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorNavArgsTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import android.app.Application -import android.os.Bundle -import assertk.assertThat -import assertk.assertions.isEqualTo -import assertk.assertions.isNull -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.signal.appsettings.authenticatorcodeentry.AuthenticatorCodeEntryState.Purpose - -@RunWith(RobolectricTestRunner::class) -@Config(application = Application::class) -class AuthenticatorNavArgsTest { - - @Test - fun `no arguments means adding a new app`() { - assertThat(AuthenticatorNavArgs.purpose(null)).isEqualTo(Purpose.Add) - assertThat(AuthenticatorNavArgs.appId(null)).isNull() - } - - @Test - fun `an unset app id reads as null`() { - val arguments = Bundle().apply { putLong(AuthenticatorNavArgs.ARG_APP_ID, AuthenticatorNavArgs.NO_APP_ID) } - - assertThat(AuthenticatorNavArgs.appId(arguments)).isNull() - } - - @Test - fun `a removal carries the app id it names`() { - val arguments = Bundle().apply { - putString(AuthenticatorNavArgs.ARG_PURPOSE, AuthenticatorNavArgs.PURPOSE_REMOVE) - putLong(AuthenticatorNavArgs.ARG_APP_ID, 7) - } - - assertThat(AuthenticatorNavArgs.appId(arguments)).isEqualTo(7L) - assertThat(AuthenticatorNavArgs.purpose(arguments)).isEqualTo(Purpose.Remove(7)) - } - - @Test - fun `a removal without an app id falls back to adding rather than removing something unidentified`() { - val arguments = Bundle().apply { putString(AuthenticatorNavArgs.ARG_PURPOSE, AuthenticatorNavArgs.PURPOSE_REMOVE) } - - assertThat(AuthenticatorNavArgs.purpose(arguments)).isEqualTo(Purpose.Add) - } - - @Test - fun `an unrecognized purpose falls back to adding instead of throwing`() { - val arguments = Bundle().apply { putString(AuthenticatorNavArgs.ARG_PURPOSE, "nonsense") } - - assertThat(AuthenticatorNavArgs.purpose(arguments)).isEqualTo(Purpose.Add) - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorSetupViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorSetupViewModelTest.kt deleted file mode 100644 index d5f6898e7a..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/AuthenticatorSetupViewModelTest.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.account.authenticator - -import android.app.Application -import assertk.assertThat -import assertk.assertions.contains -import assertk.assertions.isEqualTo -import assertk.assertions.isInstanceOf -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.signal.appsettings.authenticatorsetup.AuthenticatorSetupAction -import org.signal.appsettings.authenticatorsetup.AuthenticatorSetupEvent -import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricTestRunner::class) -@Config(application = Application::class) -class AuthenticatorSetupViewModelTest { - - private val testDispatcher = UnconfinedTestDispatcher() - - @get:Rule - val dispatcherRule = CoroutineDispatcherRule(testDispatcher) - - @Before - fun setUp() { - Dispatchers.setMain(testDispatcher) - } - - @After - fun tearDown() { - Dispatchers.resetMain() - } - - @Test - fun `the setup key is available as soon as the screen opens`() = runTest(testDispatcher) { - val viewModel = AuthenticatorSetupViewModel() - - assertThat(viewModel.state.value.setupKey).isEqualTo(AuthenticatorAppStore.MOCK_SETUP_KEY) - } - - @Test - fun `OpenAuthenticatorAppClicked hands off a link carrying the setup key`() = runTest(testDispatcher) { - val viewModel = AuthenticatorSetupViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorSetupEvent.OpenAuthenticatorAppClicked) - - val action = actions.last() - assertThat(action).isInstanceOf(AuthenticatorSetupAction.LaunchAuthenticatorApp::class) - assertThat((action as AuthenticatorSetupAction.LaunchAuthenticatorApp).uri).contains(AuthenticatorAppStore.MOCK_SETUP_KEY) - } - - @Test - fun `CopyKeyClicked copies the key and tells the user`() = runTest(testDispatcher) { - val viewModel = AuthenticatorSetupViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorSetupEvent.CopyKeyClicked) - - assertThat(actions).contains(AuthenticatorSetupAction.CopyKeyToClipboard(AuthenticatorAppStore.MOCK_SETUP_KEY)) - assertThat(actions.last()).isEqualTo(AuthenticatorSetupAction.ShowKeyCopied) - } - - @Test - fun `ContinueClicked moves on to code entry`() = runTest(testDispatcher) { - val viewModel = AuthenticatorSetupViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorSetupEvent.ContinueClicked) - - assertThat(actions.last()).isEqualTo(AuthenticatorSetupAction.NavigateToCodeEntry) - } - - @Test - fun `NoAuthenticatorAppFound reports the failure`() = runTest(testDispatcher) { - val viewModel = AuthenticatorSetupViewModel() - val actions = collectActions(viewModel.actions) - - viewModel.onEvent(AuthenticatorSetupEvent.NoAuthenticatorAppFound) - - assertThat(actions.last()).isEqualTo(AuthenticatorSetupAction.ShowNoAuthenticatorAppFound) - } - - private fun TestScope.collectActions(actions: Flow): List { - val collected = mutableListOf() - backgroundScope.launch { actions.toList(collected) } - return collected - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/InMemoryTotpApiTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/InMemoryTotpApiTest.kt new file mode 100644 index 0000000000..94c1f6d9e5 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/InMemoryTotpApiTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.hasSize +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.signal.libsignal.net.RequestResult +import java.time.Instant + +/** + * Covers the parts of the stand-in that copy behaviour the service is strict about, since those are the parts most + * likely to be wrong once there's a real service behind [TotpApi]. + */ +class InMemoryTotpApiTest { + + companion object { + private const val NOW = 1_700_000_000_000L + private const val CODE = 123456 + private val METADATA = TotpApi.Metadata(name = "Aegis", createdAt = Instant.ofEpochMilli(NOW)) + private val OTHER_METADATA = TotpApi.Metadata(name = "Aegis on my tablet", createdAt = Instant.ofEpochMilli(NOW)) + } + + private val api = InMemoryTotpApi() + + @Test + fun `a generated key is 32 bytes`() = runTest { + val result = api.generateKey() + + assertThat(result).isInstanceOf(RequestResult.Success::class) + assertThat((result as RequestResult.Success).result.key.size).isEqualTo(32) + } + + @Test + fun `a pending key doesn't show up until it's confirmed`() = runTest { + api.generateKey() + + assertThat(listedKeys()).hasSize(0) + } + + @Test + fun `a confirmed key is assigned the lowest free id`() = runTest { + val first = confirmNewKey() + val second = confirmNewKey() + + assertThat(first).isEqualTo(0) + assertThat(second).isEqualTo(1) + } + + @Test + fun `an id freed by a removal is handed out again rather than counting upwards`() = runTest { + confirmNewKey() + val second = confirmNewKey() + api.removeKey(second) + + assertThat(confirmNewKey()).isEqualTo(second) + } + + @Test + fun `confirming with no pending key doesn't confirm anything`() = runTest { + assertThat(api.confirmKey(oneTimePassword = CODE, metadata = METADATA)).isEqualTo(RequestResult.NonSuccess(TotpApi.ConfirmKeyError.NotVerified)) + assertThat(listedKeys()).hasSize(0) + } + + @Test + fun `an account at its limit can't generate another key`() = runTest { + repeat(TotpApi.MAX_KEYS) { confirmNewKey() } + + assertThat(api.generateKey()).isEqualTo(RequestResult.NonSuccess(TotpApi.GenerateKeyError.TooManyKeys)) + } + + @Test + fun `removing a key makes room for another`() = runTest { + repeat(TotpApi.MAX_KEYS) { confirmNewKey() } + api.removeKey(0) + + assertThat(api.generateKey()).isInstanceOf(RequestResult.Success::class) + } + + @Test + fun `metadata can be replaced on a confirmed key`() = runTest { + val keyId = confirmNewKey() + + assertThat(api.setKeyMetadata(keyId, OTHER_METADATA)).isEqualTo(RequestResult.Success(Unit)) + assertThat(listedKeys().first().metadata).isEqualTo(OTHER_METADATA) + } + + @Test + fun `metadata for a key that isn't there is reported rather than created`() = runTest { + assertThat(api.setKeyMetadata(7, METADATA)).isEqualTo(RequestResult.NonSuccess(TotpApi.SetKeyMetadataError.KeyNotFound)) + } + + /** The service leaves a fixed amount of room for the name, so a name that doesn't fit is the caller's bug. */ + @Test + fun `a name longer than the room the service leaves is refused`() = runTest { + val keyId = confirmNewKey() + val tooLong = METADATA.copy(name = "a".repeat(TotpApi.Metadata.NAME_MAX_LENGTH + 1)) + + assertFailure { api.setKeyMetadata(keyId, tooLong) }.isInstanceOf(IllegalArgumentException::class) + } + + /** The service reports success either way, so a retried removal looks like the original. */ + @Test + fun `removing a key that isn't there still succeeds`() = runTest { + assertThat(api.removeKey(7)).isEqualTo(RequestResult.Success(Unit)) + } + + @Test + fun `keys are listed in ascending id order`() = runTest { + confirmNewKey() + confirmNewKey() + + assertThat(listedKeys().map { it.keyId }).isEqualTo(listOf(0, 1)) + } + + private suspend fun confirmNewKey(): Int { + api.generateKey() + return (api.confirmKey(oneTimePassword = CODE, metadata = METADATA) as RequestResult.Success).result + } + + private suspend fun listedKeys(): List = (api.listKeys() as RequestResult.Success).result +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpAppListViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpAppListViewModelTest.kt new file mode 100644 index 0000000000..8d6168cfb1 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpAppListViewModelTest.kt @@ -0,0 +1,280 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.signal.appsettings.totpapplist.TotpApp +import org.signal.appsettings.totpapplist.TotpAppListAction +import org.signal.appsettings.totpapplist.TotpAppListEvent +import org.signal.appsettings.totpapplist.TotpAppListState.Dialog +import org.signal.appsettings.totpapplist.TotpAppListState.LoadState +import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule + +@OptIn(ExperimentalCoroutinesApi::class) +class TotpAppListViewModelTest { + + companion object { + private val APP_ONE = TotpApp(id = 1, name = "Bitwarden Authenticator", createdAt = 0) + private val APP_TWO = TotpApp(id = 2, name = "Twilio Authy", createdAt = 0) + } + + private val testDispatcher = UnconfinedTestDispatcher() + private val repository: TotpRepository = mockk(relaxed = true) + + @get:Rule + val dispatcherRule = CoroutineDispatcherRule(testDispatcher) + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + + every { repository.getMaxApps() } returns 2 + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(emptyList()) + coEvery { repository.removeTotpApp(any()) } returns TotpRepository.UpdateResult.Success + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `the configured apps are read on creation`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + + val viewModel = createViewModel() + + assertThat(viewModel.state.value.apps).containsExactly(APP_ONE) + } + + /** An empty list says nothing on its own, so the screen leans on [LoadState] to know we haven't heard back yet. */ + @Test + fun `the state is LOADING until we've heard back about the account`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } coAnswers { awaitCancellation() } + + val viewModel = createViewModel() + + assertThat(viewModel.state.value.loadState).isEqualTo(LoadState.LOADING) + assertThat(viewModel.state.value.apps).isEmpty() + } + + @Test + fun `a service we couldn't reach clears the list and says so`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.NetworkFailure + + val viewModel = createViewModel() + + assertThat(viewModel.state.value.loadState).isEqualTo(LoadState.NETWORK_FAILURE) + assertThat(viewModel.state.value.apps).isEmpty() + } + + @Test + fun `a load that succeeds after one that failed clears the failure`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.NetworkFailure + + val viewModel = createViewModel() + + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + viewModel.onEvent(TotpAppListEvent.ScreenResumed) + + assertThat(viewModel.state.value.loadState).isEqualTo(LoadState.LOADED) + assertThat(viewModel.state.value.apps).containsExactly(APP_ONE) + } + + @Test + fun `ScreenResumed picks up apps added elsewhere`() = runTest(testDispatcher) { + val viewModel = createViewModel() + + assertThat(viewModel.state.value.apps).isEmpty() + + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + viewModel.onEvent(TotpAppListEvent.ScreenResumed) + + assertThat(viewModel.state.value.apps).containsExactly(APP_ONE) + } + + @Test + fun `AddTotpAppClicked opens setup when there's room for another app`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpAppListEvent.AddTotpAppClicked) + + assertThat(actions.last()).isEqualTo(TotpAppListAction.NavigateToSetup) + } + + @Test + fun `AddTotpAppClicked explains the limit when there's no room for another app`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE, APP_TWO)) + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpAppListEvent.AddTotpAppClicked) + + assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.MaxAppsReached) + assertThat(actions).isEmpty() + } + + @Test + fun `RenameAppClicked opens the naming screen for that app`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpAppListEvent.RenameAppClicked(APP_ONE.id)) + + assertThat(actions.last()).isEqualTo(TotpAppListAction.NavigateToRename(APP_ONE)) + } + + @Test + fun `RemoveAppClicked asks the user to confirm first`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpAppListEvent.RemoveAppClicked(APP_ONE.id)) + + assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.ConfirmRemove(APP_ONE.id)) + assertThat(actions).isEmpty() + } + + @Test + fun `RemoveAppConfirmed removes the app and says so`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpAppListEvent.RemoveAppClicked(APP_ONE.id)) + viewModel.onEvent(TotpAppListEvent.RemoveAppConfirmed(APP_ONE.id)) + + coVerify { repository.removeTotpApp(APP_ONE.id) } + assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.None) + assertThat(actions.last()).isEqualTo(TotpAppListAction.ShowTotpAppRemoved) + } + + /** The list is what tells the user the app is gone, so it has to be read again rather than assumed. */ + @Test + fun `a removal re-reads the list`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + + val viewModel = createViewModel() + + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(emptyList()) + viewModel.onEvent(TotpAppListEvent.RemoveAppConfirmed(APP_ONE.id)) + + assertThat(viewModel.state.value.apps).isEmpty() + } + + /** Removing a key the service has already forgotten is the outcome the user wanted, so it isn't an error. */ + @Test + fun `removing an app the service doesn't have still counts as removed`() = runTest(testDispatcher) { + coEvery { repository.removeTotpApp(any()) } returns TotpRepository.UpdateResult.AppNotFound + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpAppListEvent.RemoveAppConfirmed(APP_ONE.id)) + + assertThat(actions.last()).isEqualTo(TotpAppListAction.ShowTotpAppRemoved) + } + + @Test + fun `a removal that didn't go through says so rather than pretending the app is gone`() = runTest(testDispatcher) { + coEvery { repository.removeTotpApp(any()) } returns TotpRepository.UpdateResult.NetworkFailure + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpAppListEvent.RemoveAppConfirmed(APP_ONE.id)) + + assertThat(actions.last()).isEqualTo(TotpAppListAction.ShowRemovalFailed) + } + + /** + * What the confirm button actually does: the dialog dismisses itself before it reports the confirmation, so the + * removal has to survive arriving after the dialog is already gone. + */ + @Test + fun `RemoveAppConfirmed removes the app even though the dialog dismissed itself first`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + + val viewModel = createViewModel() + + viewModel.onEvent(TotpAppListEvent.RemoveAppClicked(APP_ONE.id)) + viewModel.onEvent(TotpAppListEvent.DialogDismissed) + viewModel.onEvent(TotpAppListEvent.RemoveAppConfirmed(APP_ONE.id)) + + coVerify { repository.removeTotpApp(APP_ONE.id) } + } + + @Test + fun `DialogDismissed clears the dialog`() = runTest(testDispatcher) { + coEvery { repository.getTotpApps() } returns TotpRepository.AppsResult.Success(listOf(APP_ONE)) + + val viewModel = createViewModel() + + viewModel.onEvent(TotpAppListEvent.RemoveAppClicked(APP_ONE.id)) + viewModel.onEvent(TotpAppListEvent.DialogDismissed) + + assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.None) + } + + @Test + fun `NavigateBackClicked leaves the screen`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpAppListEvent.NavigateBackClicked) + + assertThat(actions.last()).isEqualTo(TotpAppListAction.NavigateBack) + } + + @Test + fun `LearnMoreClicked opens the support article`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpAppListEvent.LearnMoreClicked) + + assertThat(actions.last()).isEqualTo(TotpAppListAction.OpenLearnMore) + } + + private fun createViewModel() = TotpAppListViewModel(repository) + + private fun TestScope.collectActions(actions: Flow): List { + val collected = mutableListOf() + backgroundScope.launch { actions.toList(collected) } + return collected + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpCodeEntryViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpCodeEntryViewModelTest.kt new file mode 100644 index 0000000000..1ea9ae36fa --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpCodeEntryViewModelTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import assertk.assertThat +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.signal.appsettings.totpcodeentry.TotpCodeEntryAction +import org.signal.appsettings.totpcodeentry.TotpCodeEntryEvent +import org.signal.appsettings.totpcodeentry.TotpCodeEntryState.Error +import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule + +@OptIn(ExperimentalCoroutinesApi::class) +class TotpCodeEntryViewModelTest { + + companion object { + private const val FULL_CODE = "123456" + private const val APP_ID = 3L + } + + private val testDispatcher = UnconfinedTestDispatcher() + private val repository: TotpRepository = mockk(relaxed = true) + + @get:Rule + val dispatcherRule = CoroutineDispatcherRule(testDispatcher) + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + + coEvery { repository.confirmPendingApp(any()) } returns TotpRepository.ConfirmResult.Success(APP_ID) + coEvery { repository.removeTotpApp(any()) } returns TotpRepository.UpdateResult.Success + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `non-digits are dropped and the code is capped at six digits`() = runTest(testDispatcher) { + val viewModel = createViewModel() + + viewModel.onEvent(TotpCodeEntryEvent.CodeChanged("12a34 5678")) + + assertThat(viewModel.state.value.code).isEqualTo(FULL_CODE) + } + + @Test + fun `a partial code can't be submitted`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpCodeEntryEvent.CodeChanged("123")) + + assertThat(viewModel.state.value.canSubmit).isFalse() + + viewModel.onEvent(TotpCodeEntryEvent.DoneClicked) + + assertThat(actions).isEmpty() + } + + @Test + fun `a confirmed code sends the user on to name the app the service just created`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + submit(viewModel) + + assertThat(actions.last()).isEqualTo(TotpCodeEntryAction.NavigateToNaming(APP_ID)) + } + + @Test + fun `a rejected code is reported and the user stays put to try again`() = runTest(testDispatcher) { + coEvery { repository.confirmPendingApp(any()) } returns TotpRepository.ConfirmResult.IncorrectCode + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + submit(viewModel) + + assertThat(viewModel.state.value.error).isEqualTo(Error.IncorrectCode) + assertThat(viewModel.state.value.submitting).isFalse() + assertThat(actions).isEmpty() + } + + @Test + fun `an account that filled up mid-setup goes back to setup, which explains the limit`() = runTest(testDispatcher) { + coEvery { repository.confirmPendingApp(any()) } returns TotpRepository.ConfirmResult.TooManyApps + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + submit(viewModel) + + assertThat(viewModel.state.value.submitting).isFalse() + assertThat(actions.last()).isEqualTo(TotpCodeEntryAction.NavigateToSetup) + } + + @Test + fun `typing again clears the last error`() = runTest(testDispatcher) { + coEvery { repository.confirmPendingApp(any()) } returns TotpRepository.ConfirmResult.IncorrectCode + + val viewModel = createViewModel() + submit(viewModel) + + viewModel.onEvent(TotpCodeEntryEvent.CodeChanged("1")) + + assertThat(viewModel.state.value.error).isEqualTo(Error.None) + } + + @Test + fun `NavigateBackClicked leaves the screen`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpCodeEntryEvent.NavigateBackClicked) + + assertThat(actions.last()).isEqualTo(TotpCodeEntryAction.NavigateBack) + } + + private fun submit(viewModel: TotpCodeEntryViewModel) { + viewModel.onEvent(TotpCodeEntryEvent.CodeChanged(FULL_CODE)) + viewModel.onEvent(TotpCodeEntryEvent.DoneClicked) + } + + private fun createViewModel() = TotpCodeEntryViewModel(repository = repository) + + private fun TestScope.collectActions(actions: Flow): List { + val collected = mutableListOf() + backgroundScope.launch { actions.toList(collected) } + return collected + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNameEntryViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNameEntryViewModelTest.kt new file mode 100644 index 0000000000..138112cba1 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNameEntryViewModelTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.signal.appsettings.totpapplist.TotpApp +import org.signal.appsettings.totpnameentry.TotpNameEntryAction +import org.signal.appsettings.totpnameentry.TotpNameEntryEvent +import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule + +@OptIn(ExperimentalCoroutinesApi::class) +class TotpNameEntryViewModelTest { + + companion object { + private val EXISTING_APP = TotpApp(id = 7, name = "Twilio Authy", createdAt = 0) + + /** The id the service assigned when the code was confirmed, before the app had a name. */ + private const val NEW_APP_ID = 1L + } + + private val testDispatcher = UnconfinedTestDispatcher() + private val repository: TotpRepository = mockk(relaxed = true) + + @get:Rule + val dispatcherRule = CoroutineDispatcherRule(testDispatcher) + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + + every { repository.getMaxApps() } returns 2 + coEvery { repository.nameNewTotpApp(any(), any()) } returns TotpRepository.UpdateResult.Success + coEvery { repository.renameTotpApp(any(), any()) } returns TotpRepository.UpdateResult.Success + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `naming a new app starts empty and isn't renaming`() = runTest(testDispatcher) { + val viewModel = createViewModel(appId = NEW_APP_ID) + + assertThat(viewModel.state.value.name).isEqualTo("") + assertThat(viewModel.state.value.renaming).isFalse() + } + + @Test + fun `renaming starts from the app's current name`() = runTest(testDispatcher) { + val viewModel = createViewModel(appId = EXISTING_APP.id, renamedApp = EXISTING_APP) + + assertThat(viewModel.state.value.name).isEqualTo(EXISTING_APP.name) + assertThat(viewModel.state.value.renaming).isTrue() + } + + @Test + fun `a blank name can't be submitted`() = runTest(testDispatcher) { + val viewModel = createViewModel(appId = NEW_APP_ID) + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpNameEntryEvent.NameChanged(" ")) + + assertThat(viewModel.state.value.canSubmit).isFalse() + + viewModel.onEvent(TotpNameEntryEvent.NextClicked) + + assertThat(actions).isEmpty() + } + + @Test + fun `NextClicked names the newly confirmed app and goes back to the list`() = runTest(testDispatcher) { + val viewModel = createViewModel(appId = NEW_APP_ID) + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpNameEntryEvent.NameChanged(" Bitwarden Authenticator ")) + viewModel.onEvent(TotpNameEntryEvent.NextClicked) + + coVerify { repository.nameNewTotpApp(NEW_APP_ID, "Bitwarden Authenticator") } + assertThat(actions).contains(TotpNameEntryAction.ShowTotpAppSetUp) + assertThat(actions.last()).isEqualTo(TotpNameEntryAction.NavigateToTotpAppList) + } + + @Test + fun `NextClicked renames an existing app and goes back to the list`() = runTest(testDispatcher) { + val viewModel = createViewModel(appId = EXISTING_APP.id, renamedApp = EXISTING_APP) + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpNameEntryEvent.NameChanged("Work Authenticator")) + viewModel.onEvent(TotpNameEntryEvent.NextClicked) + + coVerify { repository.renameTotpApp(EXISTING_APP, "Work Authenticator") } + assertThat(actions).contains(TotpNameEntryAction.ShowTotpAppRenamed) + assertThat(actions.last()).isEqualTo(TotpNameEntryAction.NavigateToTotpAppList) + } + + @Test + fun `NavigateBackClicked leaves the screen`() = runTest(testDispatcher) { + val viewModel = createViewModel(appId = NEW_APP_ID) + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpNameEntryEvent.NavigateBackClicked) + + assertThat(actions.last()).isEqualTo(TotpNameEntryAction.NavigateBack) + } + + @Test + fun `entry is capped at the grapheme limit rather than rejected`() = runTest(testDispatcher) { + val viewModel = createViewModel(appId = NEW_APP_ID) + + viewModel.onEvent(TotpNameEntryEvent.NameChanged("a".repeat(TotpRepository.MAX_NAME_LENGTH_GRAPHEMES + 20))) + + assertThat(viewModel.state.value.name).isEqualTo("a".repeat(TotpRepository.MAX_NAME_LENGTH_GRAPHEMES)) + assertThat(viewModel.state.value.canSubmit).isTrue() + } + + /** + * The case the byte trim exists for: thirty emoji are inside the grapheme cap and well past the 98 bytes the service + * leaves room for, so the grapheme cap alone would let an unencryptable name through. + */ + @Test + fun `entry is also capped in bytes, which the grapheme limit does not guarantee`() = runTest(testDispatcher) { + val viewModel = createViewModel(appId = NEW_APP_ID) + + viewModel.onEvent(TotpNameEntryEvent.NameChanged("\uD83D\uDD10".repeat(TotpRepository.MAX_NAME_LENGTH_GRAPHEMES))) + + val name = viewModel.state.value.name + assertThat(name.toByteArray(Charsets.UTF_8).size <= TotpRepository.MAX_NAME_LENGTH_BYTES).isTrue() + assertThat(name.isNotEmpty()).isTrue() + } + + /** Trimming to a byte budget must not leave half a character behind. */ + @Test + fun `capping in bytes does not split a character`() = runTest(testDispatcher) { + val viewModel = createViewModel(appId = NEW_APP_ID) + + viewModel.onEvent(TotpNameEntryEvent.NameChanged("\uD83D\uDD10".repeat(TotpRepository.MAX_NAME_LENGTH_GRAPHEMES))) + + val name = viewModel.state.value.name + assertThat(name.toByteArray(Charsets.UTF_8).toString(Charsets.UTF_8)).isEqualTo(name) + assertThat(name.length % 2).isEqualTo(0) + } + + @Test + fun `a name that didn't save leaves the user on the screen to try again`() = runTest(testDispatcher) { + coEvery { repository.nameNewTotpApp(any(), any()) } returns TotpRepository.UpdateResult.NetworkFailure + + val viewModel = createViewModel(appId = NEW_APP_ID) + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpNameEntryEvent.NameChanged("Aegis")) + viewModel.onEvent(TotpNameEntryEvent.NextClicked) + + assertThat(actions.last()).isEqualTo(TotpNameEntryAction.ShowNameNotSaved) + assertThat(viewModel.state.value.submitting).isFalse() + } + + private fun createViewModel(appId: Long, renamedApp: TotpApp? = null) = TotpNameEntryViewModel(appId = appId, renamedApp = renamedApp, repository = repository) + + private fun TestScope.collectActions(actions: Flow): List { + val collected = mutableListOf() + backgroundScope.launch { actions.toList(collected) } + return collected + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNavArgsTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNavArgsTest.kt new file mode 100644 index 0000000000..6e30e5588b --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpNavArgsTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import android.app.Application +import android.os.Bundle +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.appsettings.totpapplist.TotpApp + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class TotpNavArgsTest { + + companion object { + private val APP = TotpApp(id = 7, name = "Aegis", createdAt = 1_700_000_000_000L) + } + + @Test + fun `no arguments means the screen is acting on a newly paired app`() { + assertThat(TotpNavArgs.appId(null)).isNull() + assertThat(TotpNavArgs.renamedApp(null)).isNull() + } + + @Test + fun `an unset app id reads as null`() { + val arguments = Bundle().apply { putLong(TotpNavArgs.ARG_APP_ID, TotpNavArgs.NO_APP_ID) } + + assertThat(TotpNavArgs.appId(arguments)).isNull() + } + + @Test + fun `a newly paired app carries its id but no renamed app`() { + val arguments = Bundle().apply { putLong(TotpNavArgs.ARG_APP_ID, 7) } + + assertThat(TotpNavArgs.appId(arguments)).isEqualTo(7L) + assertThat(TotpNavArgs.renamedApp(arguments)).isNull() + } + + @Test + fun `a rename carries the whole app the list already had`() { + val arguments = Bundle().apply { TotpNavArgs.putRenamedApp(this, APP) } + + assertThat(TotpNavArgs.appId(arguments)).isEqualTo(APP.id) + assertThat(TotpNavArgs.renamedApp(arguments)).isEqualTo(APP) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpRepositoryTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpRepositoryTest.kt new file mode 100644 index 0000000000..973febb803 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpRepositoryTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.hasSize +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.startsWith +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.signal.appsettings.totpapplist.TotpApp +import org.thoughtcrime.securesms.components.settings.app.account.authenticator.TotpRepository.AppsResult +import org.thoughtcrime.securesms.components.settings.app.account.authenticator.TotpRepository.BeginSetupResult +import org.thoughtcrime.securesms.components.settings.app.account.authenticator.TotpRepository.ConfirmResult +import org.thoughtcrime.securesms.components.settings.app.account.authenticator.TotpRepository.UpdateResult + +class TotpRepositoryTest { + + companion object { + private const val NOW = 1_700_000_000_000L + private const val ACCOUNT_NAME = "8B4A1F0C" + private const val CODE = "123456" + } + + private var now = NOW + private val api = InMemoryTotpApi() + private val repository = TotpRepository(api = api, clock = { now }) + + @Test + fun `beginSetup returns a link and a key in both the forms the screen needs`() = runTest { + val result = repository.beginSetup(ACCOUNT_NAME) as BeginSetupResult.Success + + assertThat(result.setupUri).startsWith("otpauth://totp/Signal:$ACCOUNT_NAME?") + assertThat(result.setupUri).contains("secret=${result.clipboardKey}") + assertThat(result.displayKey).isEqualTo(result.clipboardKey.chunked(4).joinToString(" ")) + } + + /** The issuer and the account name have to differ, or an app that shows both renders "Signal: Signal". */ + @Test + fun `beginSetup names the entry after the account, under the issuer`() = runTest { + val result = repository.beginSetup(ACCOUNT_NAME) as BeginSetupResult.Success + + assertThat(result.setupUri).startsWith("otpauth://totp/Signal:$ACCOUNT_NAME?") + assertThat(result.setupUri).contains("issuer=Signal&") + } + + /** Nothing should reach this without an ACI, but a bare issuer beats a label ending in a colon if anything does. */ + @Test + fun `beginSetup falls back to the issuer alone when there's no account name`() = runTest { + val result = repository.beginSetup("") as BeginSetupResult.Success + + assertThat(result.setupUri).startsWith("otpauth://totp/Signal?") + } + + @Test + fun `beginSetup treats an account name of nothing but whitespace as no account name`() = runTest { + val result = repository.beginSetup(" ") as BeginSetupResult.Success + + assertThat(result.setupUri).startsWith("otpauth://totp/Signal?") + } + + /** A space has to become %20 rather than the + form encoding would produce, or apps render it literally. */ + @Test + fun `beginSetup percent-encodes the account name`() = runTest { + val result = repository.beginSetup("+1 555") as BeginSetupResult.Success + + assertThat(result.setupUri).startsWith("otpauth://totp/Signal:%2B1%20555?") + } + + @Test + fun `setup asks for the parameters every authenticator app supports`() = runTest { + val result = repository.beginSetup(ACCOUNT_NAME) as BeginSetupResult.Success + + assertThat(result.setupUri).contains("algorithm=SHA1") + assertThat(result.setupUri).contains("digits=6") + assertThat(result.setupUri).contains("period=30") + } + + @Test + fun `an account at its limit is told rather than handed a key`() = runTest { + repeat(TotpApi.MAX_KEYS) { confirmNewApp() } + + assertThat(repository.beginSetup(ACCOUNT_NAME)).isEqualTo(BeginSetupResult.TooManyApps) + } + + @Test + fun `a confirmed app shows up in the list with the time it was confirmed`() = runTest { + val appId = confirmNewApp() + + val apps = (repository.getTotpApps() as AppsResult.Success).apps + + assertThat(apps).hasSize(1) + assertThat(apps.first().id).isEqualTo(appId) + assertThat(apps.first().createdAt).isEqualTo(NOW) + } + + /** The service wants metadata at confirmation time, and the user hasn't been asked for a name yet. */ + @Test + fun `a newly confirmed app starts out with no name`() = runTest { + confirmNewApp() + + val apps = (repository.getTotpApps() as AppsResult.Success).apps + + assertThat(apps.first().name).isEqualTo("") + } + + @Test + fun `naming a new app names it`() = runTest { + val appId = confirmNewApp() + + assertThat(repository.nameNewTotpApp(appId, "Aegis")).isEqualTo(UpdateResult.Success) + + assertThat(listedApp(appId)?.name).isEqualTo("Aegis") + } + + @Test + fun `renaming keeps the time the app was confirmed`() = runTest { + val appId = confirmNewApp() + repository.nameNewTotpApp(appId, "Aegis") + now += 60_000 + + assertThat(repository.renameTotpApp(listedApp(appId)!!, "Aegis on my tablet")).isEqualTo(UpdateResult.Success) + + val app = listedApp(appId) + assertThat(app?.name).isEqualTo("Aegis on my tablet") + assertThat(app?.createdAt).isEqualTo(NOW) + } + + @Test + fun `renaming an app that isn't there is reported rather than creating one`() = runTest { + val gone = TotpApp(id = 7, name = "Aegis", createdAt = NOW) + + assertThat(repository.renameTotpApp(gone, "Aegis on my tablet")).isEqualTo(UpdateResult.AppNotFound) + } + + @Test + fun `a removed app leaves the list`() = runTest { + val appId = confirmNewApp() + + assertThat(repository.removeTotpApp(appId)).isEqualTo(UpdateResult.Success) + assertThat((repository.getTotpApps() as AppsResult.Success).apps).isEmpty() + } + + /** The service can't tell a wrong code from a missing pending key, so neither can we. */ + @Test + fun `confirming with nothing pending is just a wrong code`() = runTest { + assertThat(repository.confirmPendingApp(CODE)).isEqualTo(ConfirmResult.IncorrectCode) + } + + @Test + fun `a code that isn't a number is reported as a wrong code`() = runTest { + repository.beginSetup(ACCOUNT_NAME) + + assertThat(repository.confirmPendingApp("abcdef")).isEqualTo(ConfirmResult.IncorrectCode) + } + + @Test + fun `a code confirms the app`() = runTest { + repository.beginSetup(ACCOUNT_NAME) + + assertThat(repository.confirmPendingApp(CODE)).isInstanceOf(ConfirmResult.Success::class) + } + + private suspend fun listedApp(appId: Long): TotpApp? { + return (repository.getTotpApps() as AppsResult.Success).apps.firstOrNull { it.id == appId } + } + + private suspend fun confirmNewApp(): Long { + repository.beginSetup(ACCOUNT_NAME) + return (repository.confirmPendingApp(CODE) as ConfirmResult.Success).appId + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpSetupViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpSetupViewModelTest.kt new file mode 100644 index 0000000000..16363cacb6 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/authenticator/TotpSetupViewModelTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account.authenticator + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.signal.appsettings.totpsetup.TotpSetupAction +import org.signal.appsettings.totpsetup.TotpSetupEvent +import org.signal.appsettings.totpsetup.TotpSetupState.Dialog +import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule +import java.util.UUID + +@OptIn(ExperimentalCoroutinesApi::class) +class TotpSetupViewModelTest { + + companion object { + private const val ACCOUNT_NAME = "8B4A1F0C" + private const val SETUP_URI = "otpauth://totp/Signal:%2B15551234567?secret=MZXW6YTBOI" + private const val DISPLAY_KEY = "MZXW 6YTB OI" + private const val CLIPBOARD_KEY = "MZXW6YTBOI" + + private val SETUP_SUCCESS = TotpRepository.BeginSetupResult.Success( + setupUri = SETUP_URI, + displayKey = DISPLAY_KEY, + clipboardKey = CLIPBOARD_KEY + ) + } + + private val testDispatcher = UnconfinedTestDispatcher() + private val repository: TotpRepository = mockk(relaxed = true) + + @get:Rule + val dispatcherRule = CoroutineDispatcherRule(testDispatcher) + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + + every { repository.getMaxApps() } returns 2 + coEvery { repository.beginSetup(any()) } returns SETUP_SUCCESS + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `the screen asks for a key as soon as it opens and shows it grouped`() = runTest(testDispatcher) { + val viewModel = createViewModel() + + assertThat(viewModel.state.value.setupKey).isEqualTo(DISPLAY_KEY) + assertThat(viewModel.state.value.loading).isFalse() + assertThat(viewModel.state.value.canContinue).isTrue() + } + + @Test + fun `OpenTotpAppClicked hands off the setup link rather than the displayed key`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpSetupEvent.OpenTotpAppClicked) + + assertThat(actions.last()).isEqualTo(TotpSetupAction.LaunchTotpApp(SETUP_URI)) + } + + @Test + fun `CopyKeyClicked copies the unbroken key rather than the grouped one`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpSetupEvent.CopyKeyClicked) + + assertThat(actions.first()).isEqualTo(TotpSetupAction.CopyKeyToClipboard(CLIPBOARD_KEY)) + assertThat(actions.last()).isEqualTo(TotpSetupAction.ShowKeyCopied) + } + + @Test + fun `ContinueClicked moves on to code entry`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpSetupEvent.ContinueClicked) + + assertThat(actions.last()).isEqualTo(TotpSetupAction.NavigateToCodeEntry) + } + + @Test + fun `NoTotpAppFound reports the failure`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpSetupEvent.NoTotpAppFound) + + assertThat(actions.last()).isEqualTo(TotpSetupAction.ShowNoTotpAppFound) + } + + @Test + fun `backing out leaves the screen`() = runTest(testDispatcher) { + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpSetupEvent.NavigateBackClicked) + + assertThat(actions.last()).isEqualTo(TotpSetupAction.NavigateBack) + } + + @Test + fun `an account at its limit is told so rather than shown an empty key`() = runTest(testDispatcher) { + coEvery { repository.beginSetup(any()) } returns TotpRepository.BeginSetupResult.TooManyApps + + val viewModel = createViewModel() + + assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.MaxAppsReached(2)) + assertThat(viewModel.state.value.canContinue).isFalse() + } + + @Test + fun `a network failure is reported rather than left spinning`() = runTest(testDispatcher) { + coEvery { repository.beginSetup(any()) } returns TotpRepository.BeginSetupResult.NetworkFailure + + val viewModel = createViewModel() + + assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.NetworkFailure) + assertThat(viewModel.state.value.loading).isFalse() + } + + @Test + fun `dismissing a failure dialog leaves the screen, since there's nothing to retry here`() = runTest(testDispatcher) { + coEvery { repository.beginSetup(any()) } returns TotpRepository.BeginSetupResult.NetworkFailure + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(TotpSetupEvent.DialogDismissed) + + assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.None) + assertThat(actions.last()).isEqualTo(TotpSetupAction.NavigateBack) + } + + @Test + fun `accountNameFor - takes the first hunk of the ACI, uppercased`() { + val aci = UUID.fromString("8b4a1f0c-2d3e-4a5b-9c7d-1e2f3a4b5c6d") + + assertThat(TotpSetupViewModel.accountNameFor(aci)).isEqualTo("8B4A1F0C") + } + + @Test + fun `accountNameFor - has nothing to say without an ACI, which leaves the issuer as the whole label`() { + assertThat(TotpSetupViewModel.accountNameFor(null)).isEqualTo("") + } + + private fun createViewModel() = TotpSetupViewModel(repository = repository, accountName = ACCOUNT_NAME) + + private fun TestScope.collectActions(actions: Flow): List { + val collected = mutableListOf() + backgroundScope.launch { actions.toList(collected) } + return collected + } +} diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/ComposeFragment.kt b/core/ui/src/main/java/org/signal/core/ui/compose/ComposeFragment.kt index d2ed4bb08b..9b25c1773d 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/ComposeFragment.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/ComposeFragment.kt @@ -5,6 +5,7 @@ package org.signal.core.ui.compose +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -18,10 +19,28 @@ import org.signal.core.ui.logging.LoggingFragment * Generic ComposeFragment which can be subclassed to build UI with compose. */ abstract class ComposeFragment : LoggingFragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = content { - SignalTheme { - FragmentContent() + + /** + * Whether the platform may offer to fill or save what the user types here. + * + * Compose registers its text fields with the autofill framework, so a password manager will offer to save the + * contents of any screen with a field on it. Override to false where that offer is wrong -- a one-time code or the + * name of a device is not a credential, and being asked to save it every time is noise the user can't turn off. + */ + open val autofillEnabled: Boolean = true + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = content { + SignalTheme { + FragmentContent() + } } + + if (!autofillEnabled && Build.VERSION.SDK_INT >= 26) { + view.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS + } + + return view } @Composable diff --git a/core/util/src/main/java/org/signal/core/util/Base32.kt b/core/util/src/main/java/org/signal/core/util/Base32.kt new file mode 100644 index 0000000000..c9be373c78 --- /dev/null +++ b/core/util/src/main/java/org/signal/core/util/Base32.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.util + +/** + * RFC 4648 base32. Padding is omitted on encode but tolerated on decode. + */ +object Base32 { + + private const val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + private const val PADDING = '=' + + private val DECODE_TABLE: IntArray = IntArray(128) { -1 }.apply { + ALPHABET.forEachIndexed { index, char -> + this[char.code] = index + this[char.lowercaseChar().code] = index + } + } + + /** Encodes [data] as unpadded, uppercase base32. */ + fun encode(data: ByteArray): String { + if (data.isEmpty()) { + return "" + } + + val out = StringBuilder((data.size * 8 + 4) / 5) + var buffer = 0L + var bitsBuffered = 0 + + for (byte in data) { + buffer = (buffer shl 8) or (byte.toLong() and 0xFF) + bitsBuffered += 8 + + while (bitsBuffered >= 5) { + bitsBuffered -= 5 + out.append(ALPHABET[((buffer shr bitsBuffered) and 0x1F).toInt()]) + } + } + + if (bitsBuffered > 0) { + out.append(ALPHABET[((buffer shl (5 - bitsBuffered)) and 0x1F).toInt()]) + } + + return out.toString() + } + + /** + * Decodes base32 [input], ignoring padding and whitespace, or null if [input] contains anything else that isn't in + * the base32 alphabet. + */ + fun decodeOrNull(input: String): ByteArray? { + val out = ArrayList(input.length * 5 / 8 + 1) + var buffer = 0L + var bitsBuffered = 0 + + for (char in input) { + if (char == PADDING || char.isWhitespace()) { + continue + } + + val value = if (char.code < DECODE_TABLE.size) DECODE_TABLE[char.code] else -1 + if (value < 0) { + return null + } + + buffer = (buffer shl 5) or value.toLong() + bitsBuffered += 5 + + if (bitsBuffered >= 8) { + bitsBuffered -= 8 + out += ((buffer shr bitsBuffered) and 0xFF).toByte() + } + } + + return out.toByteArray() + } +} diff --git a/core/util/src/test/java/org/signal/core/util/Base32Test.kt b/core/util/src/test/java/org/signal/core/util/Base32Test.kt new file mode 100644 index 0000000000..6031217fd2 --- /dev/null +++ b/core/util/src/test/java/org/signal/core/util/Base32Test.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.util + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNull +import org.junit.Test + +class Base32Test { + + /** The RFC 4648 section 10 vectors, minus the padding we don't emit. */ + @Test + fun `encode - matches the RFC 4648 test vectors`() { + assertThat(Base32.encode("".toByteArray())).isEqualTo("") + assertThat(Base32.encode("f".toByteArray())).isEqualTo("MY") + assertThat(Base32.encode("fo".toByteArray())).isEqualTo("MZXQ") + assertThat(Base32.encode("foo".toByteArray())).isEqualTo("MZXW6") + assertThat(Base32.encode("foob".toByteArray())).isEqualTo("MZXW6YQ") + assertThat(Base32.encode("fooba".toByteArray())).isEqualTo("MZXW6YTB") + assertThat(Base32.encode("foobar".toByteArray())).isEqualTo("MZXW6YTBOI") + } + + @Test + fun `decodeOrNull - matches the RFC 4648 test vectors`() { + assertThat(Base32.decodeOrNull("")?.decodeToString()).isEqualTo("") + assertThat(Base32.decodeOrNull("MY")?.decodeToString()).isEqualTo("f") + assertThat(Base32.decodeOrNull("MZXQ")?.decodeToString()).isEqualTo("fo") + assertThat(Base32.decodeOrNull("MZXW6")?.decodeToString()).isEqualTo("foo") + assertThat(Base32.decodeOrNull("MZXW6YQ")?.decodeToString()).isEqualTo("foob") + assertThat(Base32.decodeOrNull("MZXW6YTB")?.decodeToString()).isEqualTo("fooba") + assertThat(Base32.decodeOrNull("MZXW6YTBOI")?.decodeToString()).isEqualTo("foobar") + } + + @Test + fun `decodeOrNull - tolerates the padding we don't emit`() { + assertThat(Base32.decodeOrNull("MZXW6YTBOI======")?.decodeToString()).isEqualTo("foobar") + } + + @Test + fun `decodeOrNull - tolerates the spaces and lowercase a pasted key arrives with`() { + assertThat(Base32.decodeOrNull("mzxw 6ytb oi")?.decodeToString()).isEqualTo("foobar") + } + + @Test + fun `decodeOrNull - rejects characters outside the alphabet`() { + assertThat(Base32.decodeOrNull("MZXW6YTB1")).isNull() + assertThat(Base32.decodeOrNull("MZXW6YTB0")).isNull() + assertThat(Base32.decodeOrNull("MZXW6YTB!")).isNull() + assertThat(Base32.decodeOrNull("MZXW6YTBé")).isNull() + } + + @Test + fun `round trip - survives every byte value`() { + val data = ByteArray(256) { it.toByte() } + + assertThat(Base32.decodeOrNull(Base32.encode(data))?.toList()).isEqualTo(data.toList()) + } + + @Test + fun `encode - produces a 52 character key for the 32 byte keys the service generates`() { + assertThat(Base32.encode(ByteArray(32)).length).isEqualTo(52) + } +} diff --git a/demo/registration/src/main/java/org/signal/registration/sample/debug/DebugNetworkController.kt b/demo/registration/src/main/java/org/signal/registration/sample/debug/DebugNetworkController.kt index cd2a35f912..7d395efd59 100644 --- a/demo/registration/src/main/java/org/signal/registration/sample/debug/DebugNetworkController.kt +++ b/demo/registration/src/main/java/org/signal/registration/sample/debug/DebugNetworkController.kt @@ -140,13 +140,14 @@ class DebugNetworkController( pniPreKeys: PreKeyCollection?, fcmToken: String?, skipDeviceTransfer: Boolean, - aci: ACI? + aci: ACI?, + totp: Int? ): RequestResult { NetworkDebugState.getOverride>("registerAccount")?.let { Log.d(TAG, "[registerAccount] Returning debug override") return it } - return delegate.registerAccount(e164, password, sessionId, recoveryPassword, receiptCredentialPresentation, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer, aci) + return delegate.registerAccount(e164, password, sessionId, recoveryPassword, receiptCredentialPresentation, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer, aci, totp) } override suspend fun createLoginPurchaseReceiptCredential( diff --git a/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoNetworkController.kt b/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoNetworkController.kt index 51e411cbf0..687c8f2f2b 100644 --- a/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoNetworkController.kt +++ b/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoNetworkController.kt @@ -214,7 +214,8 @@ class DemoNetworkController( pniPreKeys: PreKeyCollection?, fcmToken: String?, skipDeviceTransfer: Boolean, - aci: ACI? + aci: ACI?, + totp: Int? ): RequestResult { return registrationApi.registerAccount( e164 = e164, @@ -227,7 +228,8 @@ class DemoNetworkController( pniPreKeys = pniPreKeys, fcmToken = fcmToken, skipDeviceTransfer = skipDeviceTransfer, - aci = aci + aci = aci, + totp = totp ) } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt index 9d670d5f82..33eac38911 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt @@ -29,7 +29,7 @@ sealed interface AccountSettingsAction { data object NavigateToSignalLoginDetails : AccountSettingsAction /** Open the screen listing the account's authenticator apps. */ - data object NavigateToAuthenticatorApps : AccountSettingsAction + data object NavigateToTotpAppList : AccountSettingsAction /** Open the passkeys screen. */ data object NavigateToPasskeys : AccountSettingsAction diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt index 2fbefff316..f5519dc140 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt @@ -46,7 +46,7 @@ sealed interface AccountSettingsEvent { data object RegistrationLockConfirmed : AccountSettingsEvent /** The user tapped the authenticator app row in the two-factor authentication section. */ - data object AuthenticatorAppClicked : AccountSettingsEvent + data object TotpAppClicked : AccountSettingsEvent /** The user tapped the passkeys row in the two-factor authentication section. */ data object PasskeysClicked : AccountSettingsEvent diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsScreen.kt b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsScreen.kt index ceb13292b9..02adeee904 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsScreen.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsScreen.kt @@ -63,7 +63,7 @@ import org.signal.core.ui.R as CoreUiR object AccountSettingsTestTags { const val SCROLLER = "scroller" const val CARD_SIGNAL_LOGIN = "card-signal-login" - const val ROW_AUTHENTICATOR_APP = "row-authenticator-app" + const val ROW_TOTP_APP = "row-totp-app" const val ROW_PASSKEYS = "row-passkeys" const val ROW_MODIFY_PIN = "row-modify-pin" const val ROW_PIN_REMINDER = "row-pin-reminder" @@ -126,16 +126,19 @@ fun AccountSettingsScreen( } item { + // A null count means we couldn't find out, which reads as the generic subtitle rather than as "none configured". + val totpAppCount = state.signalLogin.totpAppCount + Rows.TextRow( icon = SignalIcons.DevicePhone.imageVector, text = stringResource(R.string.AccountSettingsFragment__authenticator_app), - label = if (state.signalLogin.authenticatorAppCount > 0) { - pluralStringResource(R.plurals.AccountSettingsFragment__d_configured, state.signalLogin.authenticatorAppCount, state.signalLogin.authenticatorAppCount) + label = if (totpAppCount != null && totpAppCount > 0) { + pluralStringResource(R.plurals.AccountSettingsFragment__d_configured, totpAppCount, totpAppCount) } else { stringResource(R.string.AccountSettingsFragment__one_time_verification_codes) }, - onClick = { onEvent(AccountSettingsEvent.AuthenticatorAppClicked) }, - modifier = Modifier.testTag(AccountSettingsTestTags.ROW_AUTHENTICATOR_APP) + onClick = { onEvent(AccountSettingsEvent.TotpAppClicked) }, + modifier = Modifier.testTag(AccountSettingsTestTags.ROW_TOTP_APP) ) } @@ -565,7 +568,7 @@ private fun AccountSettingsScreenSignalLoginPreview() { AccountSettingsScreen( state = AccountSettingsState( isPhoneNumberless = true, - signalLogin = AccountSettingsState.SignalLogin(authenticatorAppCount = 2, passkeyCount = 8) + signalLogin = AccountSettingsState.SignalLogin(totpAppCount = 2, passkeyCount = 8) ), onEvent = {} ) diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsState.kt b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsState.kt index a60002b8f1..e208230f72 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsState.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsState.kt @@ -26,7 +26,8 @@ data class AccountSettingsState( * the sections aren't shown at all. */ data class SignalLogin( - val authenticatorAppCount: Int, + /** How many authenticator apps are on the account, or null while we don't know -- see `getTotpAppCount`. */ + val totpAppCount: Int?, val passkeyCount: Int ) diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsAction.kt b/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsAction.kt deleted file mode 100644 index fb82f2970f..0000000000 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsAction.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.appsettings.authenticatorapps - -/** - * One-shot side effects that need the nav graph, and therefore have to be carried out by the fragment hosting - * [AuthenticatorAppsScreen] rather than the screen itself. - * - * Actions are logged, so be sure `toString()` contains nothing sensitive. - */ -sealed interface AuthenticatorAppsAction { - - /** Leave the screen. */ - data object NavigateBack : AuthenticatorAppsAction - - /** Open the flow that pairs a new authenticator app. */ - data object NavigateToSetup : AuthenticatorAppsAction - - /** Open the screen that renames [appId]. */ - data class NavigateToRename(val appId: Long) : AuthenticatorAppsAction - - /** Collect a code from [appId] before it's removed. */ - data class NavigateToRemovalCodeEntry(val appId: Long) : AuthenticatorAppsAction - - /** Send the user to a support article about authenticator apps. */ - data object OpenLearnMore : AuthenticatorAppsAction -} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsEvent.kt b/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsEvent.kt deleted file mode 100644 index a774f52466..0000000000 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsEvent.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.appsettings.authenticatorapps - -/** - * Reminder that these events are logged, so don't include anything sensitive in the toString. - */ -sealed interface AuthenticatorAppsEvent { - - /** The screen came back to the foreground, so the list we read out of storage may be stale. */ - data object ScreenResumed : AuthenticatorAppsEvent - - /** The user tapped the navigation (back) icon. */ - data object NavigateBackClicked : AuthenticatorAppsEvent - - /** The user tapped the button that starts setting up another authenticator app. */ - data object AddAuthenticatorAppClicked : AuthenticatorAppsEvent - - /** The user tapped the learn more link. */ - data object LearnMoreClicked : AuthenticatorAppsEvent - - /** The user tapped the rename option in an app's overflow menu. */ - data class RenameAppClicked(val appId: Long) : AuthenticatorAppsEvent - - /** The user tapped the remove option in an app's overflow menu, which asks them to confirm first. */ - data class RemoveAppClicked(val appId: Long) : AuthenticatorAppsEvent - - /** The user confirmed removing the app named in [AuthenticatorAppsState.Dialog.ConfirmRemove]. */ - data object RemoveAppConfirmed : AuthenticatorAppsEvent - - /** Dismisses whatever is in [AuthenticatorAppsState.dialog]. */ - data object DialogDismissed : AuthenticatorAppsEvent -} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryAction.kt b/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryAction.kt deleted file mode 100644 index 4ab3d9d44b..0000000000 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryAction.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.appsettings.authenticatorcodeentry - -/** - * One-shot side effects that need the nav graph, and therefore have to be carried out by the fragment hosting - * [AuthenticatorCodeEntryScreen] rather than the screen itself. - * - * Actions are logged, so be sure `toString()` contains nothing sensitive. - */ -sealed interface AuthenticatorCodeEntryAction { - - /** Leave the screen. */ - data object NavigateBack : AuthenticatorCodeEntryAction - - /** The new authenticator app is confirmed, so go name it. */ - data object NavigateToNaming : AuthenticatorCodeEntryAction - - /** The removal is done, so go back to the list of authenticator apps. */ - data object NavigateToAuthenticatorApps : AuthenticatorCodeEntryAction - - /** Tell the user their authenticator app was removed. */ - data object ShowAuthenticatorAppRemoved : AuthenticatorCodeEntryAction -} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryState.kt b/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryState.kt deleted file mode 100644 index 665f6ff7b3..0000000000 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryState.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.appsettings.authenticatorcodeentry - -data class AuthenticatorCodeEntryState( - val code: String = "", - val submitting: Boolean = false, - /** What the code is being collected for, which decides where the user goes once it's accepted. */ - val purpose: Purpose = Purpose.Add -) { - - val canSubmit: Boolean - get() = code.length == CODE_LENGTH && !submitting - - override fun toString(): String = "AuthenticatorCodeEntryState(codeLength=${code.length}, submitting=$submitting, purpose=$purpose)" - - sealed interface Purpose { - /** Confirming a newly paired authenticator app, which is then named. */ - data object Add : Purpose - - /** Confirming removal of the already-configured app identified by [appId]. */ - data class Remove(val appId: Long) : Purpose - } - - companion object { - const val CODE_LENGTH = 6 - } -} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameState.kt b/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameState.kt deleted file mode 100644 index 6769f5e49a..0000000000 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameState.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.appsettings.authenticatorname - -data class AuthenticatorNameState( - val name: String = "", - /** True when an already-configured app is being renamed, false when one is being named for the first time. */ - val renaming: Boolean = false, - val submitting: Boolean = false -) { - - val canSubmit: Boolean - get() = name.isNotBlank() && !submitting - - override fun toString(): String = "AuthenticatorNameState(nameLength=${name.length}, renaming=$renaming, submitting=$submitting)" -} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupState.kt b/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupState.kt deleted file mode 100644 index 28ac7c7c40..0000000000 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupState.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.appsettings.authenticatorsetup - -data class AuthenticatorSetupState( - /** The key the user hands to their authenticator app, either through the app link or by copying it. */ - val setupKey: String = "" -) { - override fun toString(): String = "AuthenticatorSetupState(setupKey=${if (setupKey.isEmpty()) "empty" else "present"})" -} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorApp.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpApp.kt similarity index 61% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorApp.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpApp.kt index 0aab02a3c4..c2e1a938a4 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorApp.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpApp.kt @@ -3,16 +3,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorapps +package org.signal.appsettings.totpapplist /** - * A single authenticator app configured on the user's account, as shown on [AuthenticatorAppsScreen]. + * A single authenticator app configured on the user's account, as shown on [TotpAppListScreen]. */ -data class AuthenticatorApp( +data class TotpApp( val id: Long, val name: String, /** When the app was configured, in epoch milliseconds. */ val createdAt: Long ) { - override fun toString(): String = "AuthenticatorApp(id=$id)" + override fun toString(): String = "TotpApp(id=$id)" } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListAction.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListAction.kt new file mode 100644 index 0000000000..15e578a59d --- /dev/null +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListAction.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.appsettings.totpapplist + +/** + * One-shot side effects that need the nav graph, and therefore have to be carried out by the fragment hosting + * [TotpAppListScreen] rather than the screen itself. + * + * Actions are logged, so be sure `toString()` contains nothing sensitive. + */ +sealed interface TotpAppListAction { + + /** Leave the screen. */ + data object NavigateBack : TotpAppListAction + + /** Open the flow that pairs a new authenticator app. */ + data object NavigateToSetup : TotpAppListAction + + /** Open the screen that renames [app]. */ + data class NavigateToRename(val app: TotpApp) : TotpAppListAction + + /** Tell the user their authenticator app was removed. */ + data object ShowTotpAppRemoved : TotpAppListAction + + /** Tell the user the removal didn't go through, so they know the app is still on the account. */ + data object ShowRemovalFailed : TotpAppListAction + + /** Send the user to a support article about authenticator apps. */ + data object OpenLearnMore : TotpAppListAction +} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListEvent.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListEvent.kt new file mode 100644 index 0000000000..dd8644a396 --- /dev/null +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListEvent.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.appsettings.totpapplist + +/** + * Reminder that these events are logged, so don't include anything sensitive in the toString. + */ +sealed interface TotpAppListEvent { + + /** The screen came back to the foreground, so the list we read out of storage may be stale. */ + data object ScreenResumed : TotpAppListEvent + + /** The user tapped the navigation (back) icon. */ + data object NavigateBackClicked : TotpAppListEvent + + /** The user tapped the button that starts setting up another authenticator app. */ + data object AddTotpAppClicked : TotpAppListEvent + + /** The user tapped the learn more link. */ + data object LearnMoreClicked : TotpAppListEvent + + /** The user tapped the rename option in an app's overflow menu. */ + data class RenameAppClicked(val appId: Long) : TotpAppListEvent + + /** The user tapped the remove option in an app's overflow menu, which asks them to confirm first. */ + data class RemoveAppClicked(val appId: Long) : TotpAppListEvent + + /** The user confirmed removing the app, which removes it. */ + data class RemoveAppConfirmed(val appId: Long) : TotpAppListEvent + + /** Dismisses whatever is in [TotpAppListState.dialog]. */ + data object DialogDismissed : TotpAppListEvent +} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsScreen.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListScreen.kt similarity index 54% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsScreen.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListScreen.kt index 41ad0b7592..c830109214 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsScreen.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListScreen.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorapps +package org.signal.appsettings.totpapplist import android.text.format.DateUtils import androidx.annotation.VisibleForTesting @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -35,7 +36,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import org.signal.appsettings.R -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsState.Dialog +import org.signal.appsettings.totpapplist.TotpAppListState.Dialog +import org.signal.appsettings.totpapplist.TotpAppListState.LoadState import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.Dialogs @@ -50,7 +52,7 @@ import org.signal.core.ui.compose.Texts import org.signal.core.ui.R as CoreUiR @VisibleForTesting -object AuthenticatorAppsTestTags { +object TotpAppListTestTags { const val SCROLLER = "scroller" const val LEARN_MORE = "learn-more" const val BUTTON_ADD = "button-add" @@ -59,28 +61,30 @@ object AuthenticatorAppsTestTags { const val MENU_ITEM_RENAME = "menu-item-rename" const val MENU_ITEM_REMOVE = "menu-item-remove" const val EMPTY_MESSAGE = "empty-message" + const val LOAD_FAILED_MESSAGE = "load-failed-message" const val DIALOG_CONFIRM_REMOVE = "dialog-confirm-remove" const val DIALOG_MAX_APPS_REACHED = "dialog-max-apps-reached" + const val LOADING = "loading" } /** * Lists the authenticator apps configured on the account and lets the user add, rename, or remove one. */ @Composable -fun AuthenticatorAppsScreen( - state: AuthenticatorAppsState, - onEvent: (AuthenticatorAppsEvent) -> Unit +fun TotpAppListScreen( + state: TotpAppListState, + onEvent: (TotpAppListEvent) -> Unit ) { Scaffolds.Settings( - title = stringResource(R.string.AuthenticatorAppsScreen__authenticator_app), - onNavigationClick = { onEvent(AuthenticatorAppsEvent.NavigateBackClicked) }, + title = stringResource(R.string.TotpAppListScreen__authenticator_app), + onNavigationClick = { onEvent(TotpAppListEvent.NavigateBackClicked) }, navigationIcon = SignalIcons.ArrowStart.imageVector ) { contentPadding -> LazyColumn( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .padding(contentPadding) - .testTag(AuthenticatorAppsTestTags.SCROLLER) + .testTag(TotpAppListTestTags.SCROLLER) ) { item { Image( @@ -98,13 +102,13 @@ fun AuthenticatorAppsScreen( modifier = Modifier .padding(top = 24.dp) .padding(horizontal = 34.dp) - .testTag(AuthenticatorAppsTestTags.LEARN_MORE) + .testTag(TotpAppListTestTags.LEARN_MORE) ) } item { Buttons.MediumTonal( - onClick = { onEvent(AuthenticatorAppsEvent.AddAuthenticatorAppClicked) }, + onClick = { onEvent(TotpAppListEvent.AddTotpAppClicked) }, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer @@ -113,9 +117,9 @@ fun AuthenticatorAppsScreen( .fillMaxWidth() .padding(top = 24.dp, bottom = 20.dp) .padding(horizontal = 40.dp) - .testTag(AuthenticatorAppsTestTags.BUTTON_ADD) + .testTag(TotpAppListTestTags.BUTTON_ADD) ) { - Text(text = stringResource(R.string.AuthenticatorAppsScreen__add_authenticator_app)) + Text(text = stringResource(R.string.TotpAppListScreen__add_authenticator_app)) } } @@ -125,44 +129,75 @@ fun AuthenticatorAppsScreen( item { Texts.SectionHeader( - text = stringResource(R.string.AuthenticatorAppsScreen__authenticator_apps), + text = stringResource(R.string.TotpAppListScreen__authenticator_apps), modifier = Modifier.fillMaxWidth() ) } - if (state.apps.isEmpty()) { - item { - Text( - text = stringResource(R.string.AuthenticatorAppsScreen__no_authenticator_apps), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, + when (state.loadState) { + LoadState.LOADING -> item { + CircularProgressIndicator( modifier = Modifier .padding(top = 40.dp) - .testTag(AuthenticatorAppsTestTags.EMPTY_MESSAGE) + .size(24.dp) + .testTag(TotpAppListTestTags.LOADING) ) } - } else { - items(state.apps, key = { it.id }) { app -> - AuthenticatorAppRow( - app = app, - onEvent = onEvent + + LoadState.NETWORK_FAILURE -> item { + SectionMessage( + text = stringResource(R.string.TotpAppListScreen__couldnt_load_authenticator_apps), + modifier = Modifier.testTag(TotpAppListTestTags.LOAD_FAILED_MESSAGE) ) } + + LoadState.LOADED -> if (state.apps.isEmpty()) { + item { + SectionMessage( + text = stringResource(R.string.TotpAppListScreen__no_authenticator_apps), + modifier = Modifier.testTag(TotpAppListTestTags.EMPTY_MESSAGE) + ) + } + } else { + items(state.apps, key = { it.id }) { app -> + TotpAppRow( + app = app, + onEvent = onEvent + ) + } + } } } } when (val dialog = state.dialog) { Dialog.None -> Unit - is Dialog.ConfirmRemove -> ConfirmRemoveDialog(onEvent) + is Dialog.ConfirmRemove -> ConfirmRemoveDialog(appId = dialog.appId, onEvent = onEvent) Dialog.MaxAppsReached -> MaxAppsReachedDialog(maxApps = state.maxApps, onEvent = onEvent) } } +/** Whatever the list section has to say when it has no rows to show. */ @Composable -private fun AuthenticatorAppRow( - app: AuthenticatorApp, - onEvent: (AuthenticatorAppsEvent) -> Unit +private fun SectionMessage( + text: String, + modifier: Modifier = Modifier +) { + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = modifier + .padding(top = 40.dp) + .padding(horizontal = 34.dp) + ) +} + +@Composable +private fun TotpAppRow( + app: TotpApp, + onEvent: (TotpAppListEvent) -> Unit ) { val context = LocalContext.current val addedTime = remember(app.createdAt) { @@ -180,33 +215,33 @@ private fun AuthenticatorAppRow( text = { TextAndLabel( text = app.name, - label = stringResource(R.string.AuthenticatorAppsScreen__added_s, addedTime) + label = stringResource(R.string.TotpAppListScreen__added_s, addedTime) ) - AuthenticatorAppMenuButton( + TotpAppMenuButton( app = app, onEvent = onEvent ) }, - modifier = Modifier.testTag(AuthenticatorAppsTestTags.ROW_APP) + modifier = Modifier.testTag(TotpAppListTestTags.ROW_APP) ) } @Composable -private fun AuthenticatorAppMenuButton( - app: AuthenticatorApp, - onEvent: (AuthenticatorAppsEvent) -> Unit +private fun TotpAppMenuButton( + app: TotpApp, + onEvent: (TotpAppListEvent) -> Unit ) { val menuController = remember { DropdownMenus.MenuController() } Box { IconButton( onClick = menuController::show, - modifier = Modifier.testTag(AuthenticatorAppsTestTags.BUTTON_APP_MENU) + modifier = Modifier.testTag(TotpAppListTestTags.BUTTON_APP_MENU) ) { Icon( imageVector = SignalIcons.MoreVertical.imageVector, - contentDescription = stringResource(R.string.AuthenticatorAppsScreen__open_authenticator_app_options), + contentDescription = stringResource(R.string.TotpAppListScreen__open_authenticator_app_options), tint = MaterialTheme.colorScheme.onSurface ) } @@ -214,22 +249,22 @@ private fun AuthenticatorAppMenuButton( DropdownMenus.Menu(controller = menuController) { controller -> DropdownMenus.Item( leadingIconResId = CoreUiR.drawable.symbol_edit_24, - text = { Text(text = stringResource(R.string.AuthenticatorAppsScreen__rename)) }, + text = { Text(text = stringResource(R.string.TotpAppListScreen__rename)) }, onClick = { - onEvent(AuthenticatorAppsEvent.RenameAppClicked(app.id)) + onEvent(TotpAppListEvent.RenameAppClicked(app.id)) controller.hide() }, - modifier = Modifier.testTag(AuthenticatorAppsTestTags.MENU_ITEM_RENAME) + modifier = Modifier.testTag(TotpAppListTestTags.MENU_ITEM_RENAME) ) DropdownMenus.Item( leadingIconResId = CoreUiR.drawable.symbol_x_circle_24, - text = { Text(text = stringResource(R.string.AuthenticatorAppsScreen__remove)) }, + text = { Text(text = stringResource(R.string.TotpAppListScreen__remove)) }, onClick = { - onEvent(AuthenticatorAppsEvent.RemoveAppClicked(app.id)) + onEvent(TotpAppListEvent.RemoveAppClicked(app.id)) controller.hide() }, - modifier = Modifier.testTag(AuthenticatorAppsTestTags.MENU_ITEM_REMOVE) + modifier = Modifier.testTag(TotpAppListTestTags.MENU_ITEM_REMOVE) ) } } @@ -237,22 +272,22 @@ private fun AuthenticatorAppMenuButton( @Composable private fun DescriptionWithLearnMore( - onEvent: (AuthenticatorAppsEvent) -> Unit, + onEvent: (TotpAppListEvent) -> Unit, modifier: Modifier = Modifier ) { Text( text = buildAnnotatedString { - append(stringResource(R.string.AuthenticatorAppsScreen__set_up_an_authenticator_app)) + append(stringResource(R.string.TotpAppListScreen__set_up_an_authenticator_app)) append(' ') withLink( LinkAnnotation.Clickable( tag = "learn-more", styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary)), - linkInteractionListener = { onEvent(AuthenticatorAppsEvent.LearnMoreClicked) } + linkInteractionListener = { onEvent(TotpAppListEvent.LearnMoreClicked) } ) ) { - append(stringResource(R.string.AuthenticatorAppsScreen__learn_more)) + append(stringResource(R.string.TotpAppListScreen__learn_more)) } }, style = MaterialTheme.typography.bodyLarge, @@ -264,44 +299,45 @@ private fun DescriptionWithLearnMore( @Composable private fun ConfirmRemoveDialog( - onEvent: (AuthenticatorAppsEvent) -> Unit + appId: Long, + onEvent: (TotpAppListEvent) -> Unit ) { Dialogs.SimpleAlertDialog( - title = stringResource(R.string.AuthenticatorAppsScreen__remove_authenticator_app), - body = stringResource(R.string.AuthenticatorAppsScreen__to_remove_this_authentication_method), - confirm = stringResource(R.string.AuthenticatorAppsScreen__remove), - onConfirm = { onEvent(AuthenticatorAppsEvent.RemoveAppConfirmed) }, - onDismiss = { onEvent(AuthenticatorAppsEvent.DialogDismissed) }, + title = stringResource(R.string.TotpAppListScreen__remove_authenticator_app), + body = stringResource(R.string.TotpAppListScreen__you_wont_be_able_to_use_this_app), + confirm = stringResource(R.string.TotpAppListScreen__remove), + onConfirm = { onEvent(TotpAppListEvent.RemoveAppConfirmed(appId)) }, + onDismiss = { onEvent(TotpAppListEvent.DialogDismissed) }, dismiss = stringResource(android.R.string.cancel), - onDismissRequest = { onEvent(AuthenticatorAppsEvent.DialogDismissed) }, - modifier = Modifier.testTag(AuthenticatorAppsTestTags.DIALOG_CONFIRM_REMOVE) + onDismissRequest = { onEvent(TotpAppListEvent.DialogDismissed) }, + modifier = Modifier.testTag(TotpAppListTestTags.DIALOG_CONFIRM_REMOVE) ) } @Composable private fun MaxAppsReachedDialog( maxApps: Int, - onEvent: (AuthenticatorAppsEvent) -> Unit + onEvent: (TotpAppListEvent) -> Unit ) { Dialogs.SimpleAlertDialog( - title = stringResource(R.string.AuthenticatorAppsScreen__cant_add_authenticator_app), - body = stringResource(R.string.AuthenticatorAppsScreen__you_cant_add_more_than_d, maxApps), + title = stringResource(R.string.TotpAppListScreen__cant_add_authenticator_app), + body = stringResource(R.string.TotpAppListScreen__you_cant_add_more_than_d, maxApps), confirm = stringResource(android.R.string.ok), onConfirm = {}, - onDismiss = { onEvent(AuthenticatorAppsEvent.DialogDismissed) }, - dismiss = stringResource(R.string.AuthenticatorAppsScreen__learn_more), - onDeny = { onEvent(AuthenticatorAppsEvent.LearnMoreClicked) }, - onDismissRequest = { onEvent(AuthenticatorAppsEvent.DialogDismissed) }, - modifier = Modifier.testTag(AuthenticatorAppsTestTags.DIALOG_MAX_APPS_REACHED) + onDismiss = { onEvent(TotpAppListEvent.DialogDismissed) }, + dismiss = stringResource(R.string.TotpAppListScreen__learn_more), + onDeny = { onEvent(TotpAppListEvent.LearnMoreClicked) }, + onDismissRequest = { onEvent(TotpAppListEvent.DialogDismissed) }, + modifier = Modifier.testTag(TotpAppListTestTags.DIALOG_MAX_APPS_REACHED) ) } @DayNightPreviews @Composable -private fun AuthenticatorAppsScreenPreview() { +private fun TotpAppListScreenPreview() { Previews.Preview { - AuthenticatorAppsScreen( - state = AuthenticatorAppsState(), + TotpAppListScreen( + state = TotpAppListState(), onEvent = {} ) } @@ -309,10 +345,32 @@ private fun AuthenticatorAppsScreenPreview() { @DayNightPreviews @Composable -private fun AuthenticatorAppsScreenWithAppsPreview() { +private fun TotpAppListScreenEmptyPreview() { Previews.Preview { - AuthenticatorAppsScreen( - state = AuthenticatorAppsState(apps = PREVIEW_APPS), + TotpAppListScreen( + state = TotpAppListState(loadState = LoadState.LOADED), + onEvent = {} + ) + } +} + +@DayNightPreviews +@Composable +private fun TotpAppListScreenLoadFailedPreview() { + Previews.Preview { + TotpAppListScreen( + state = TotpAppListState(loadState = LoadState.NETWORK_FAILURE), + onEvent = {} + ) + } +} + +@DayNightPreviews +@Composable +private fun TotpAppListPreview() { + Previews.Preview { + TotpAppListScreen( + state = TotpAppListState(apps = PREVIEW_APPS, loadState = LoadState.LOADED), onEvent = {} ) } @@ -322,7 +380,7 @@ private fun AuthenticatorAppsScreenWithAppsPreview() { @Composable private fun ConfirmRemoveDialogPreview() { Previews.Preview { - ConfirmRemoveDialog(onEvent = {}) + ConfirmRemoveDialog(appId = 1, onEvent = {}) } } @@ -335,6 +393,6 @@ private fun MaxAppsReachedDialogPreview() { } private val PREVIEW_APPS = listOf( - AuthenticatorApp(id = 1, name = "Bitwarden Authenticator", createdAt = System.currentTimeMillis()), - AuthenticatorApp(id = 2, name = "Twilio Authy", createdAt = System.currentTimeMillis()) + TotpApp(id = 1, name = "Bitwarden Authenticator", createdAt = System.currentTimeMillis()), + TotpApp(id = 2, name = "Twilio Authy", createdAt = System.currentTimeMillis()) ) diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsState.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListState.kt similarity index 50% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsState.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListState.kt index 2dc9606bd4..dbc0de346e 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsState.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpapplist/TotpAppListState.kt @@ -3,19 +3,32 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorapps +package org.signal.appsettings.totpapplist -data class AuthenticatorAppsState( - /** The authenticator apps configured on the account. When empty, the list section says so instead of listing rows. */ - val apps: List = emptyList(), +data class TotpAppListState( + /** The authenticator apps configured on the account, which only means anything once [loadState] is [LoadState.LOADED]. */ + val apps: List = emptyList(), /** How many authenticator apps the account is allowed to have at once. */ val maxApps: Int = 0, + /** How the last look at the account went, which decides what the list section shows in place of rows. */ + val loadState: LoadState = LoadState.LOADING, val dialog: Dialog = Dialog.None ) { val atMaxApps: Boolean get() = apps.size >= maxApps + /** How the last attempt to read the account's authenticator apps went, since an empty list can't say on its own. */ + enum class LoadState { + /** We haven't heard back about the account yet. */ + LOADING, + + LOADED, + + /** We couldn't reach the service, which is worth another try. */ + NETWORK_FAILURE + } + /** Whichever dialog the screen is showing, if any. Only one is ever up at a time. */ sealed interface Dialog { data object None : Dialog diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryAction.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryAction.kt new file mode 100644 index 0000000000..2d2cf5961c --- /dev/null +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.appsettings.totpcodeentry + +/** + * One-shot side effects that need the nav graph, and therefore have to be carried out by the fragment hosting + * [TotpCodeEntryScreen] rather than the screen itself. + * + * Actions are logged, so be sure `toString()` contains nothing sensitive. + */ +sealed interface TotpCodeEntryAction { + + /** Leave the screen. */ + data object NavigateBack : TotpCodeEntryAction + + /** The new authenticator app is confirmed and the service gave it [appId], so go name it. */ + data class NavigateToNaming(val appId: Long) : TotpCodeEntryAction + + /** Go back to setup, because the key the user was confirming is gone and there's nothing to retry against. */ + data object NavigateToSetup : TotpCodeEntryAction +} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryEvent.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryEvent.kt similarity index 58% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryEvent.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryEvent.kt index a34f2dc15b..d56605a06f 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryEvent.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryEvent.kt @@ -3,21 +3,21 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorcodeentry +package org.signal.appsettings.totpcodeentry /** * Reminder that these events are logged, so don't include anything sensitive in the toString. */ -sealed interface AuthenticatorCodeEntryEvent { +sealed interface TotpCodeEntryEvent { /** The user tapped the navigation (back) icon. */ - data object NavigateBackClicked : AuthenticatorCodeEntryEvent + data object NavigateBackClicked : TotpCodeEntryEvent /** The user typed in the code field. */ - data class CodeChanged(val code: String) : AuthenticatorCodeEntryEvent { + data class CodeChanged(val code: String) : TotpCodeEntryEvent { override fun toString(): String = "CodeChanged(length=${code.length})" } /** The user submitted the code they entered. */ - data object DoneClicked : AuthenticatorCodeEntryEvent + data object DoneClicked : TotpCodeEntryEvent } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryScreen.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryScreen.kt similarity index 63% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryScreen.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryScreen.kt index 61b61bcf3a..3a0d09a7c4 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryScreen.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryScreen.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorcodeentry +package org.signal.appsettings.totpcodeentry import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.Column @@ -38,18 +38,19 @@ import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons @VisibleForTesting -object AuthenticatorCodeEntryTestTags { +object TotpCodeEntryTestTags { const val CODE_INPUT = "code-input" const val BUTTON_DONE = "button-done" + const val ERROR = "error" } /** * Collects the one-time code the user's authenticator app generated, which is the last step of setting one up. */ @Composable -fun AuthenticatorCodeEntryScreen( - state: AuthenticatorCodeEntryState, - onEvent: (AuthenticatorCodeEntryEvent) -> Unit +fun TotpCodeEntryScreen( + state: TotpCodeEntryState, + onEvent: (TotpCodeEntryEvent) -> Unit ) { val focusRequester = remember { FocusRequester() } @@ -58,8 +59,8 @@ fun AuthenticatorCodeEntryScreen( } Scaffolds.Settings( - title = stringResource(R.string.AuthenticatorCodeEntryScreen__enter_your_code), - onNavigationClick = { onEvent(AuthenticatorCodeEntryEvent.NavigateBackClicked) }, + title = stringResource(R.string.TotpCodeEntryScreen__enter_your_code), + onNavigationClick = { onEvent(TotpCodeEntryEvent.NavigateBackClicked) }, navigationIcon = SignalIcons.ArrowStart.imageVector ) { contentPadding -> Column( @@ -70,7 +71,7 @@ fun AuthenticatorCodeEntryScreen( horizontalAlignment = Alignment.End ) { Text( - text = stringResource(R.string.AuthenticatorCodeEntryScreen__enter_the_6_digit_code), + text = stringResource(R.string.TotpCodeEntryScreen__enter_the_6_digit_code), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier @@ -78,25 +79,31 @@ fun AuthenticatorCodeEntryScreen( .padding(horizontal = 24.dp, vertical = 16.dp) ) + val errorMessage = state.error.message() + TextField( value = state.code, - onValueChange = { onEvent(AuthenticatorCodeEntryEvent.CodeChanged(it)) }, - label = { Text(text = stringResource(R.string.AuthenticatorCodeEntryScreen__code)) }, + onValueChange = { onEvent(TotpCodeEntryEvent.CodeChanged(it)) }, + label = { Text(text = stringResource(R.string.TotpCodeEntryScreen__code)) }, singleLine = true, enabled = !state.submitting, + isError = errorMessage != null, + supportingText = errorMessage?.let { message -> + { Text(text = message, modifier = Modifier.testTag(TotpCodeEntryTestTags.ERROR)) } + }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { if (state.canSubmit) onEvent(AuthenticatorCodeEntryEvent.DoneClicked) }), + keyboardActions = KeyboardActions(onDone = { if (state.canSubmit) onEvent(TotpCodeEntryEvent.DoneClicked) }), modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) .focusRequester(focusRequester) - .testTag(AuthenticatorCodeEntryTestTags.CODE_INPUT) + .testTag(TotpCodeEntryTestTags.CODE_INPUT) ) Spacer(modifier = Modifier.weight(1f)) Buttons.LargeTonal( - onClick = { onEvent(AuthenticatorCodeEntryEvent.DoneClicked) }, + onClick = { onEvent(TotpCodeEntryEvent.DoneClicked) }, enabled = state.canSubmit, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, @@ -104,20 +111,41 @@ fun AuthenticatorCodeEntryScreen( ), modifier = Modifier .padding(horizontal = 24.dp, vertical = 24.dp) - .testTag(AuthenticatorCodeEntryTestTags.BUTTON_DONE) + .testTag(TotpCodeEntryTestTags.BUTTON_DONE) ) { - Text(text = stringResource(R.string.AuthenticatorCodeEntryScreen__done)) + Text(text = stringResource(R.string.TotpCodeEntryScreen__done)) } } } } +/** + * The message shown under the code field, or null when there's nothing wrong. + */ +@Composable +private fun TotpCodeEntryState.Error.message(): String? = when (this) { + TotpCodeEntryState.Error.None -> null + TotpCodeEntryState.Error.IncorrectCode -> stringResource(R.string.TotpCodeEntryScreen__incorrect_code) + TotpCodeEntryState.Error.NetworkFailure -> stringResource(R.string.TotpCodeEntryScreen__couldnt_reach_signal) +} + @DayNightPreviews @Composable -private fun AuthenticatorCodeEntryScreenPreview() { +private fun TotpCodeEntryScreenPreview() { Previews.Preview { - AuthenticatorCodeEntryScreen( - state = AuthenticatorCodeEntryState(code = "123456"), + TotpCodeEntryScreen( + state = TotpCodeEntryState(code = "123456"), + onEvent = {} + ) + } +} + +@DayNightPreviews +@Composable +private fun TotpCodeEntryScreenErrorPreview() { + Previews.Preview { + TotpCodeEntryScreen( + state = TotpCodeEntryState(code = "123456", error = TotpCodeEntryState.Error.IncorrectCode), onEvent = {} ) } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryState.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryState.kt new file mode 100644 index 0000000000..a07ae4d451 --- /dev/null +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryState.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.appsettings.totpcodeentry + +data class TotpCodeEntryState( + val code: String = "", + val submitting: Boolean = false, + /** Why the last submission didn't work, shown under the code field and cleared as soon as the user types. */ + val error: Error = Error.None +) { + + val canSubmit: Boolean + get() = code.length == CODE_LENGTH && !submitting + + override fun toString(): String = "TotpCodeEntryState(codeLength=${code.length}, submitting=$submitting, error=$error)" + + sealed interface Error { + data object None : Error + + /** The service rejected the code, and we have no reason to think it was anything but a wrong code. */ + data object IncorrectCode : Error + + data object NetworkFailure : Error + } + + companion object { + const val CODE_LENGTH = 6 + } +} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameAction.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryAction.kt similarity index 51% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameAction.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryAction.kt index 346785bb11..4353f554e4 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameAction.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryAction.kt @@ -3,25 +3,28 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorname +package org.signal.appsettings.totpnameentry /** * One-shot side effects that need the nav graph, and therefore have to be carried out by the fragment hosting - * [AuthenticatorNameScreen] rather than the screen itself. + * [TotpNameEntryScreen] rather than the screen itself. * * Actions are logged, so be sure `toString()` contains nothing sensitive. */ -sealed interface AuthenticatorNameAction { +sealed interface TotpNameEntryAction { /** Leave the screen. */ - data object NavigateBack : AuthenticatorNameAction + data object NavigateBack : TotpNameEntryAction /** The app has a name now, so go back to the list of authenticator apps. */ - data object NavigateToAuthenticatorApps : AuthenticatorNameAction + data object NavigateToTotpAppList : TotpNameEntryAction /** Tell the user their authenticator app was set up. */ - data object ShowAuthenticatorAppSetUp : AuthenticatorNameAction + data object ShowTotpAppSetUp : TotpNameEntryAction /** Tell the user their authenticator app was renamed. */ - data object ShowAuthenticatorAppRenamed : AuthenticatorNameAction + data object ShowTotpAppRenamed : TotpNameEntryAction + + /** Tell the user the name didn't stick, so they know to try again. */ + data object ShowNameNotSaved : TotpNameEntryAction } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameEvent.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryEvent.kt similarity index 61% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameEvent.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryEvent.kt index 034eef5108..87b4287b6a 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameEvent.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryEvent.kt @@ -3,21 +3,21 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorname +package org.signal.appsettings.totpnameentry /** * Reminder that these events are logged, so don't include anything sensitive in the toString. */ -sealed interface AuthenticatorNameEvent { +sealed interface TotpNameEntryEvent { /** The user tapped the navigation (back) icon. */ - data object NavigateBackClicked : AuthenticatorNameEvent + data object NavigateBackClicked : TotpNameEntryEvent /** The user typed in the name field. */ - data class NameChanged(val name: String) : AuthenticatorNameEvent { + data class NameChanged(val name: String) : TotpNameEntryEvent { override fun toString(): String = "NameChanged(length=${name.length})" } /** The user submitted the name they entered. */ - data object NextClicked : AuthenticatorNameEvent + data object NextClicked : TotpNameEntryEvent } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameScreen.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreen.kt similarity index 72% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameScreen.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreen.kt index 5a7ff28fbc..9b88822202 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorname/AuthenticatorNameScreen.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreen.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorname +package org.signal.appsettings.totpnameentry import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.Column @@ -38,7 +38,7 @@ import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons @VisibleForTesting -object AuthenticatorNameTestTags { +object TotpNameEntryTestTags { const val NAME_INPUT = "name-input" const val BUTTON_NEXT = "button-next" } @@ -48,9 +48,9 @@ object AuthenticatorNameTestTags { * renaming one that already exists. */ @Composable -fun AuthenticatorNameScreen( - state: AuthenticatorNameState, - onEvent: (AuthenticatorNameEvent) -> Unit +fun TotpNameEntryScreen( + state: TotpNameEntryState, + onEvent: (TotpNameEntryEvent) -> Unit ) { val focusRequester = remember { FocusRequester() } @@ -59,8 +59,8 @@ fun AuthenticatorNameScreen( } Scaffolds.Settings( - title = stringResource(R.string.AuthenticatorNameScreen__choose_a_name), - onNavigationClick = { onEvent(AuthenticatorNameEvent.NavigateBackClicked) }, + title = stringResource(R.string.TotpNameEntryScreen__choose_a_name), + onNavigationClick = { onEvent(TotpNameEntryEvent.NavigateBackClicked) }, navigationIcon = SignalIcons.ArrowStart.imageVector ) { contentPadding -> Column( @@ -72,9 +72,9 @@ fun AuthenticatorNameScreen( ) { Text( text = if (state.renaming) { - stringResource(R.string.AuthenticatorNameScreen__choose_a_unique_name) + stringResource(R.string.TotpNameEntryScreen__choose_a_unique_name) } else { - stringResource(R.string.AuthenticatorNameScreen__choose_a_unique_name_to_help_you_identify_it) + stringResource(R.string.TotpNameEntryScreen__choose_a_unique_name_to_help_you_identify_it) }, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -85,23 +85,23 @@ fun AuthenticatorNameScreen( TextField( value = state.name, - onValueChange = { onEvent(AuthenticatorNameEvent.NameChanged(it)) }, - label = { Text(text = stringResource(R.string.AuthenticatorNameScreen__name)) }, + onValueChange = { onEvent(TotpNameEntryEvent.NameChanged(it)) }, + label = { Text(text = stringResource(R.string.TotpNameEntryScreen__name)) }, singleLine = true, enabled = !state.submitting, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { if (state.canSubmit) onEvent(AuthenticatorNameEvent.NextClicked) }), + keyboardActions = KeyboardActions(onDone = { if (state.canSubmit) onEvent(TotpNameEntryEvent.NextClicked) }), modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) .focusRequester(focusRequester) - .testTag(AuthenticatorNameTestTags.NAME_INPUT) + .testTag(TotpNameEntryTestTags.NAME_INPUT) ) Spacer(modifier = Modifier.weight(1f)) Buttons.LargeTonal( - onClick = { onEvent(AuthenticatorNameEvent.NextClicked) }, + onClick = { onEvent(TotpNameEntryEvent.NextClicked) }, enabled = state.canSubmit, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, @@ -109,9 +109,9 @@ fun AuthenticatorNameScreen( ), modifier = Modifier .padding(horizontal = 24.dp, vertical = 24.dp) - .testTag(AuthenticatorNameTestTags.BUTTON_NEXT) + .testTag(TotpNameEntryTestTags.BUTTON_NEXT) ) { - Text(text = stringResource(R.string.AuthenticatorNameScreen__next)) + Text(text = stringResource(R.string.TotpNameEntryScreen__next)) } } } @@ -119,10 +119,10 @@ fun AuthenticatorNameScreen( @DayNightPreviews @Composable -private fun AuthenticatorNameScreenPreview() { +private fun TotpNameEntryScreenPreview() { Previews.Preview { - AuthenticatorNameScreen( - state = AuthenticatorNameState(name = "Bitwarden Authenticator"), + TotpNameEntryScreen( + state = TotpNameEntryState(name = "Bitwarden Authenticator"), onEvent = {} ) } @@ -130,10 +130,10 @@ private fun AuthenticatorNameScreenPreview() { @DayNightPreviews @Composable -private fun AuthenticatorNameScreenRenamePreview() { +private fun TotpNameEntryScreenRenamePreview() { Previews.Preview { - AuthenticatorNameScreen( - state = AuthenticatorNameState(name = "Twilio Authy", renaming = true), + TotpNameEntryScreen( + state = TotpNameEntryState(name = "Twilio Authy", renaming = true), onEvent = {} ) } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryState.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryState.kt new file mode 100644 index 0000000000..66dac34ee0 --- /dev/null +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryState.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.appsettings.totpnameentry + +data class TotpNameEntryState( + /** + * The name so far, already capped in grapheme clusters and in UTF-8 bytes by whoever fed it in. Nothing here needs to + * know either limit: an over-length name is never a state this screen has to render or explain, because the field + * simply stops accepting one. + */ + val name: String = "", + /** True when an already-configured app is being renamed, false when one is being named for the first time. */ + val renaming: Boolean = false, + val submitting: Boolean = false +) { + + val canSubmit: Boolean + get() = name.isNotBlank() && !submitting + + override fun toString(): String = "TotpNameEntryState(nameLength=${name.length}, renaming=$renaming, submitting=$submitting)" +} diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupAction.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupAction.kt similarity index 53% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupAction.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupAction.kt index 11f293ddc2..744a012928 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupAction.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupAction.kt @@ -3,35 +3,35 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorsetup +package org.signal.appsettings.totpsetup /** * One-shot side effects that need an Activity or the nav graph, and therefore have to be carried out by the fragment - * hosting [AuthenticatorSetupScreen] rather than the screen itself. + * hosting [TotpSetupScreen] rather than the screen itself. * * Actions are logged, so be sure `toString()` contains nothing sensitive. */ -sealed interface AuthenticatorSetupAction { +sealed interface TotpSetupAction { /** Leave the screen. */ - data object NavigateBack : AuthenticatorSetupAction + data object NavigateBack : TotpSetupAction /** Hand [uri] off to whichever authenticator app the user has installed. */ - data class LaunchAuthenticatorApp(val uri: String) : AuthenticatorSetupAction { - override fun toString(): String = "LaunchAuthenticatorApp()" + data class LaunchTotpApp(val uri: String) : TotpSetupAction { + override fun toString(): String = "LaunchTotpApp()" } /** Put [key] on the clipboard. */ - data class CopyKeyToClipboard(val key: String) : AuthenticatorSetupAction { + data class CopyKeyToClipboard(val key: String) : TotpSetupAction { override fun toString(): String = "CopyKeyToClipboard()" } /** Tell the user the setup key was copied. */ - data object ShowKeyCopied : AuthenticatorSetupAction + data object ShowKeyCopied : TotpSetupAction /** Tell the user we couldn't find an app to hand the setup key to. */ - data object ShowNoAuthenticatorAppFound : AuthenticatorSetupAction + data object ShowNoTotpAppFound : TotpSetupAction /** Move on to the screen where the user enters a code from their authenticator app. */ - data object NavigateToCodeEntry : AuthenticatorSetupAction + data object NavigateToCodeEntry : TotpSetupAction } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupEvent.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupEvent.kt similarity index 53% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupEvent.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupEvent.kt index 99ec29866c..9231320912 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupEvent.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupEvent.kt @@ -3,25 +3,28 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorsetup +package org.signal.appsettings.totpsetup /** * Reminder that these events are logged, so don't include anything sensitive in the toString. */ -sealed interface AuthenticatorSetupEvent { +sealed interface TotpSetupEvent { /** The user tapped the navigation (back) icon. */ - data object NavigateBackClicked : AuthenticatorSetupEvent + data object NavigateBackClicked : TotpSetupEvent /** The user tapped the button that hands the setup key off to their authenticator app. */ - data object OpenAuthenticatorAppClicked : AuthenticatorSetupEvent + data object OpenTotpAppClicked : TotpSetupEvent /** The user tapped the button that copies the setup key. */ - data object CopyKeyClicked : AuthenticatorSetupEvent + data object CopyKeyClicked : TotpSetupEvent /** The fragment reported that no installed app could handle the setup link. */ - data object NoAuthenticatorAppFound : AuthenticatorSetupEvent + data object NoTotpAppFound : TotpSetupEvent /** The user finished the steps and is ready to enter a code. */ - data object ContinueClicked : AuthenticatorSetupEvent + data object ContinueClicked : TotpSetupEvent + + /** Dismisses whatever is in [TotpSetupState.dialog], which leaves the screen since none of them are recoverable here. */ + data object DialogDismissed : TotpSetupEvent } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupScreen.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupScreen.kt similarity index 67% rename from feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupScreen.kt rename to feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupScreen.kt index 1521aed636..98ade7788f 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupScreen.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupScreen.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorsetup +package org.signal.appsettings.totpsetup import androidx.annotation.DrawableRes import androidx.annotation.VisibleForTesting @@ -23,6 +23,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -45,31 +46,33 @@ import androidx.compose.ui.unit.sp import org.signal.appsettings.R import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons import org.signal.core.ui.compose.theme.SignalTheme @VisibleForTesting -object AuthenticatorSetupTestTags { +object TotpSetupTestTags { const val SCROLLER = "scroller" const val BUTTON_OPEN = "button-open" const val BUTTON_COPY = "button-copy" const val BUTTON_CONTINUE = "button-continue" const val SETUP_KEY = "setup-key" + const val SETUP_KEY_SPINNER = "setup-key-spinner" } /** * Walks the user through pairing an authenticator app with their account, ending in the code entry screen. */ @Composable -fun AuthenticatorSetupScreen( - state: AuthenticatorSetupState, - onEvent: (AuthenticatorSetupEvent) -> Unit +fun TotpSetupScreen( + state: TotpSetupState, + onEvent: (TotpSetupEvent) -> Unit ) { Scaffolds.Settings( - title = stringResource(R.string.AuthenticatorSetupScreen__set_up_your_authenticator_app), - onNavigationClick = { onEvent(AuthenticatorSetupEvent.NavigateBackClicked) }, + title = stringResource(R.string.TotpSetupScreen__set_up_your_authenticator_app), + onNavigationClick = { onEvent(TotpSetupEvent.NavigateBackClicked) }, navigationIcon = SignalIcons.ArrowStart.imageVector ) { contentPadding -> Column( @@ -81,16 +84,16 @@ fun AuthenticatorSetupScreen( modifier = Modifier .weight(1f) .verticalScroll(rememberScrollState()) - .testTag(AuthenticatorSetupTestTags.SCROLLER) + .testTag(TotpSetupTestTags.SCROLLER) ) { TextWithLearnMore( - text = stringResource(R.string.AuthenticatorSetupScreen__follow_these_steps), + text = stringResource(R.string.TotpSetupScreen__follow_these_steps), modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) ) StepCard( - title = stringResource(R.string.AuthenticatorSetupScreen__step_1), - body = stringResource(R.string.AuthenticatorSetupScreen__install_a_trusted_authenticator_app), + title = stringResource(R.string.TotpSetupScreen__step_1), + body = stringResource(R.string.TotpSetupScreen__install_a_trusted_authenticator_app), illustration = { StepImage( image = R.drawable.image_authenticator_install_app, @@ -101,8 +104,8 @@ fun AuthenticatorSetupScreen( ) StepCard( - title = stringResource(R.string.AuthenticatorSetupScreen__step_2), - body = stringResource(R.string.AuthenticatorSetupScreen__open_your_authenticator_app), + title = stringResource(R.string.TotpSetupScreen__step_2), + body = stringResource(R.string.TotpSetupScreen__open_your_authenticator_app), illustration = { StepImage( image = R.drawable.image_authenticator_open_app, @@ -112,12 +115,13 @@ fun AuthenticatorSetupScreen( } ) { SurfaceButton( - text = stringResource(R.string.AuthenticatorSetupScreen__open), + text = stringResource(R.string.TotpSetupScreen__open), icon = SignalIcons.Open, - onClick = { onEvent(AuthenticatorSetupEvent.OpenAuthenticatorAppClicked) }, + enabled = state.canContinue, + onClick = { onEvent(TotpSetupEvent.OpenTotpAppClicked) }, modifier = Modifier .padding(top = 16.dp) - .testTag(AuthenticatorSetupTestTags.BUTTON_OPEN) + .testTag(TotpSetupTestTags.BUTTON_OPEN) ) HorizontalDivider( @@ -127,38 +131,49 @@ fun AuthenticatorSetupScreen( ) Text( - text = stringResource(R.string.AuthenticatorSetupScreen__or_you_can_copy_this_key), + text = stringResource(R.string.TotpSetupScreen__or_you_can_copy_this_key), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 24.dp) ) - Text( - text = state.setupKey, - style = MaterialTheme.typography.bodyMedium.copy( - fontFamily = FontFamily.Monospace, - fontSize = 15.sp, - lineHeight = 28.sp, - letterSpacing = 0.9.sp - ), - modifier = Modifier - .padding(top = 4.dp) - .testTag(AuthenticatorSetupTestTags.SETUP_KEY) - ) + if (state.loading) { + CircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier + .padding(top = 8.dp) + .size(20.dp) + .testTag(TotpSetupTestTags.SETUP_KEY_SPINNER) + ) + } else { + Text( + text = state.setupKey, + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace, + fontSize = 15.sp, + lineHeight = 28.sp, + letterSpacing = 0.9.sp + ), + modifier = Modifier + .padding(top = 4.dp) + .testTag(TotpSetupTestTags.SETUP_KEY) + ) + } SurfaceButton( - text = stringResource(R.string.AuthenticatorSetupScreen__copy), + text = stringResource(R.string.TotpSetupScreen__copy), icon = SignalIcons.Copy, - onClick = { onEvent(AuthenticatorSetupEvent.CopyKeyClicked) }, + enabled = state.canContinue, + onClick = { onEvent(TotpSetupEvent.CopyKeyClicked) }, modifier = Modifier .padding(top = 16.dp) - .testTag(AuthenticatorSetupTestTags.BUTTON_COPY) + .testTag(TotpSetupTestTags.BUTTON_COPY) ) } StepCard( - title = stringResource(R.string.AuthenticatorSetupScreen__step_3), - body = stringResource(R.string.AuthenticatorSetupScreen__copy_the_code_thats_generated), + title = stringResource(R.string.TotpSetupScreen__step_3), + body = stringResource(R.string.TotpSetupScreen__copy_the_code_thats_generated), illustration = { StepImage( image = R.drawable.image_authenticator_copy_code, @@ -172,7 +187,8 @@ fun AuthenticatorSetupScreen( } Buttons.LargeTonal( - onClick = { onEvent(AuthenticatorSetupEvent.ContinueClicked) }, + onClick = { onEvent(TotpSetupEvent.ContinueClicked) }, + enabled = state.canContinue, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer @@ -180,14 +196,37 @@ fun AuthenticatorSetupScreen( modifier = Modifier .fillMaxWidth() .padding(horizontal = 40.dp, vertical = 16.dp) - .testTag(AuthenticatorSetupTestTags.BUTTON_CONTINUE) + .testTag(TotpSetupTestTags.BUTTON_CONTINUE) ) { - Text(text = stringResource(R.string.AuthenticatorSetupScreen__continue)) + Text(text = stringResource(R.string.TotpSetupScreen__continue)) } } + + SetupDialog(dialog = state.dialog, onEvent = onEvent) } } +/** + * Neither of these is recoverable on this screen, so dismissing either one leaves it. + */ +@Composable +private fun SetupDialog( + dialog: TotpSetupState.Dialog, + onEvent: (TotpSetupEvent) -> Unit +) { + val message = when (dialog) { + TotpSetupState.Dialog.None -> return + is TotpSetupState.Dialog.MaxAppsReached -> stringResource(R.string.TotpAppListScreen__you_cant_add_more_than_d, dialog.maxApps) + TotpSetupState.Dialog.NetworkFailure -> stringResource(R.string.TotpSetupScreen__couldnt_reach_signal) + } + + Dialogs.SimpleMessageDialog( + message = message, + dismiss = stringResource(android.R.string.ok), + onDismiss = { onEvent(TotpSetupEvent.DialogDismissed) } + ) +} + /** * Body text with a "Learn more" link appended, which has nowhere to go yet. */ @@ -196,7 +235,7 @@ private fun TextWithLearnMore( text: String, modifier: Modifier = Modifier ) { - val learnMore = stringResource(R.string.AuthenticatorSetupScreen__learn_more) + val learnMore = stringResource(R.string.TotpSetupScreen__learn_more) val primaryColor = MaterialTheme.colorScheme.primary Text( @@ -283,10 +322,12 @@ private fun SurfaceButton( text: String, icon: SignalIcons, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + enabled: Boolean = true ) { Buttons.MediumTonal( onClick = onClick, + enabled = enabled, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.onPrimaryContainer @@ -307,10 +348,10 @@ private fun SurfaceButton( @DayNightPreviews @Composable -private fun AuthenticatorSetupScreenPreview() { +private fun TotpSetupScreenPreview() { Previews.Preview { - AuthenticatorSetupScreen( - state = AuthenticatorSetupState(setupKey = "KVZ7WL3FDDWJZMTOB7PLZPKVRFD4LYSX"), + TotpSetupScreen( + state = TotpSetupState(setupKey = "KVZ7 WL3F DDWJ ZMTO B7PL ZPKV RFD4 LYSX", loading = false), onEvent = {} ) } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupState.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupState.kt new file mode 100644 index 0000000000..45a423e746 --- /dev/null +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpsetup/TotpSetupState.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.appsettings.totpsetup + +data class TotpSetupState( + /** The key the user hands to their authenticator app, grouped for reading rather than for pasting. */ + val setupKey: String = "", + /** True until the service has handed us a key, which is everything on this screen. */ + val loading: Boolean = true, + val dialog: Dialog = Dialog.None +) { + + /** Nothing on this screen works without a key, so the buttons wait for one. */ + val canContinue: Boolean + get() = !loading && setupKey.isNotEmpty() + + override fun toString(): String = "TotpSetupState(setupKey=${if (setupKey.isEmpty()) "empty" else "present"}, loading=$loading, dialog=$dialog)" + + sealed interface Dialog { + data object None : Dialog + + /** The account already has the [maxApps] authenticator apps it's allowed, so there's no key to be had. */ + data class MaxAppsReached(val maxApps: Int) : Dialog + + /** We couldn't reach the service to ask for a key. */ + data object NetworkFailure : Dialog + } +} diff --git a/feature/app-settings/src/main/res/values-af/strings.xml b/feature/app-settings/src/main/res/values-af/strings.xml index 33d9deabcc..8b398ffffa 100644 --- a/feature/app-settings/src/main/res/values-af/strings.xml +++ b/feature/app-settings/src/main/res/values-af/strings.xml @@ -80,37 +80,37 @@ Vind meer uit - + - Stel jou bevestigingstoepassing op + Stel jou bevestigingstoepassing op - Volg hierdie stappe om jou bevestigingstoepassing op te stel. + Volg hierdie stappe om jou bevestigingstoepassing op te stel. - Vind meer uit + Vind meer uit - Stap 1 + Stap 1 - Installeer \'n betroubare bevestigingstoepassing op jou toestel. + Installeer \'n betroubare bevestigingstoepassing op jou toestel. - Stap 2 + Stap 2 - Maak jou bevestigingstoepassing oop deur op die knoppie hier onder te tik om jou Signal-rekening by te voeg. + Maak jou bevestigingstoepassing oop deur op die knoppie hier onder te tik om jou Signal-rekening by te voeg. - Maak oop + Maak oop - Of jy kan hierdie sleutel kopieer om dit handmatig op te stel. + Of jy kan hierdie sleutel kopieer om dit handmatig op te stel. - Kopieer + Kopieer - Na knipbord gekopieer + Na knipbord gekopieer - Geen bevestigingstoepassing gevind nie + Geen bevestigingstoepassing gevind nie - Stap 3 + Stap 3 - Kopieer die kode wat gegenereer word en keer terug hierheen om voort te gaan. + Kopieer die kode wat gegenereer word en keer terug hierheen om voort te gaan. - Gaan voort + Gaan voort @@ -140,60 +140,60 @@ Verwyder - + - Voer jou sleutel in + Voer jou sleutel in - Voer die 6-syfer-kode vanaf jou bevestigingstoepassing in. + Voer die 6-syfer-kode vanaf jou bevestigingstoepassing in. - Kode + Kode - Klaar - + Klaar + - Bevestigingstoepassing + Bevestigingstoepassing - Stel \'n bevestigingstoepassing op om eenmalige verifiëringskodes te genereer + Stel \'n bevestigingstoepassing op om eenmalige verifiëringskodes te genereer - Vind meer uit + Vind meer uit - Voeg bevestigingstoepassing by + Voeg bevestigingstoepassing by - Bevestigingstoepassings + Bevestigingstoepassings - Geen bevestigingstoepassings nie + Geen bevestigingstoepassings nie - \"%1$s\" bygevoeg + \"%1$s\" bygevoeg - Maak bevestigingstoepassing-opsies oop + Maak bevestigingstoepassing-opsies oop - Hernoem + Hernoem - Verwyder + Verwyder - Verwyder bevestigingstoepassing? + Verwyder bevestigingstoepassing? - Om hierdie bevestigingsmetode te verwyder, word die 6-syfer-kode vanaf jou bevestigingstoepassing vereis. + Om hierdie bevestigingsmetode te verwyder, word die 6-syfer-kode vanaf jou bevestigingstoepassing vereis. - Kan nie bevestigingstoepassing byvoeg nie + Kan nie bevestigingstoepassing byvoeg nie - Jy kan nie meer as %1$d bevestigingstoepassings byvoeg nie. Probeer om eers een te verwyder. + Jy kan nie meer as %1$d bevestigingstoepassings byvoeg nie. Probeer om eers een te verwyder. - Bevestigingstoepassing verwyder + Bevestigingstoepassing verwyder - + - Kies \'n naam + Kies \'n naam - Kies \'n unieke naam vir hierdie bevestigingstoepassing. + Kies \'n unieke naam vir hierdie bevestigingstoepassing. - Kies \'n unieke naam vir hierdie bevestigingstoepassing om jou te help om dit later te identifiseer. + Kies \'n unieke naam vir hierdie bevestigingstoepassing om jou te help om dit later te identifiseer. - Naam + Naam - Volgende + Volgende - Bevestigingstoepassing opgestel + Bevestigingstoepassing opgestel - Bevestigingstoepassing hernoem + Bevestigingstoepassing hernoem diff --git a/feature/app-settings/src/main/res/values-ar/strings.xml b/feature/app-settings/src/main/res/values-ar/strings.xml index 1af42c4970..cf8929a9c3 100644 --- a/feature/app-settings/src/main/res/values-ar/strings.xml +++ b/feature/app-settings/src/main/res/values-ar/strings.xml @@ -92,37 +92,37 @@ اعرف المزيد - + - Set up your authenticator app + Set up your authenticator app - اتَّبِع هذه الخطوات لإعداد تطبيق المصادقة. + اتَّبِع هذه الخطوات لإعداد تطبيق المصادقة. - اعرف المزيد + اعرف المزيد - الخطوة 1 + الخطوة 1 - ثبِّت تطبيق مصادقة موثوق على جهازك. + ثبِّت تطبيق مصادقة موثوق على جهازك. - الخطوة 2 + الخطوة 2 - افتح تطبيق المصادقة بالضغط على الزر أدناه لإضافة حساب سيجنال الخاص بك. + افتح تطبيق المصادقة بالضغط على الزر أدناه لإضافة حساب سيجنال الخاص بك. - فتح + فتح - أو يمكنك نسخ المفتاح لإعداده يدويًا. + أو يمكنك نسخ المفتاح لإعداده يدويًا. - نسخ + نسخ - تمَّ النسخ إلى الحافظة + تمَّ النسخ إلى الحافظة - لم يتم العثور على أي تطبيق مصادقة + لم يتم العثور على أي تطبيق مصادقة - الخطوة 3 + الخطوة 3 - انسخ الرمز الذي تمَّ إنشاؤه وعُد إلى هنا للمواصلة. + انسخ الرمز الذي تمَّ إنشاؤه وعُد إلى هنا للمواصلة. - متابعة + متابعة @@ -152,60 +152,60 @@ إزالة - + - أدخِل الكود الخاص بك + أدخِل الكود الخاص بك - أدخِل الكود المُكوَّن من 6 أرقام من تطبيق المصادقة لديك. + أدخِل الكود المُكوَّن من 6 أرقام من تطبيق المصادقة لديك. - الكود + الكود - تم - + تم + - تطبيق المصادقة + تطبيق المصادقة - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - اعرف المزيد + اعرف المزيد - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - تمَّت إضافة \"%1$s\" + تمَّت إضافة \"%1$s\" - Open authenticator app options + Open authenticator app options - إعادة التسمية + إعادة التسمية - إزالة + إزالة - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - اختر اسمًا + اختر اسمًا - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - اِسم + اِسم - التالي + التالي - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-az/strings.xml b/feature/app-settings/src/main/res/values-az/strings.xml index 53626d2bef..f540f52f05 100644 --- a/feature/app-settings/src/main/res/values-az/strings.xml +++ b/feature/app-settings/src/main/res/values-az/strings.xml @@ -80,37 +80,37 @@ Daha ətraflı - + - Autentifikasiya tətbiqinizi quraşdırın + Autentifikasiya tətbiqinizi quraşdırın - Autentifikasiya tətbiqini quraşdırmaq üçün bu addımlara əməl edin: + Autentifikasiya tətbiqini quraşdırmaq üçün bu addımlara əməl edin: - Daha ətraflı + Daha ətraflı - Addım 1 + Addım 1 - Cihazınıza etibarlı bir autentifikasiya tətbiqi quraşdırın. + Cihazınıza etibarlı bir autentifikasiya tətbiqi quraşdırın. - Addım 2 + Addım 2 - Signal hesabınızı əlavə etmək üçün aşağıdakı düyməyə toxunaraq autentifikasiya tətbiqini açın. + Signal hesabınızı əlavə etmək üçün aşağıdakı düyməyə toxunaraq autentifikasiya tətbiqini açın. - + - Yaxud da əllə quraşdırma üçün bu parolu kopiyalaya bilərsiniz. + Yaxud da əllə quraşdırma üçün bu parolu kopiyalaya bilərsiniz. - Kopyala + Kopyala - Mübadilə buferinə kopyalandı + Mübadilə buferinə kopyalandı - Heç bir autentifikasiya tətbiqi tapılmadı + Heç bir autentifikasiya tətbiqi tapılmadı - Addım 3 + Addım 3 - Yaradılmış kodu kopiyalayıb davam etmək üçün buraya qayıdın. + Yaradılmış kodu kopiyalayıb davam etmək üçün buraya qayıdın. - Davam et + Davam et @@ -140,60 +140,60 @@ Çıxart - + - Kodunuzu daxil edin + Kodunuzu daxil edin - Autentifikasiya tətbiqində yaradılmış 6 rəqəmli kodu daxil edin. + Autentifikasiya tətbiqində yaradılmış 6 rəqəmli kodu daxil edin. - Koda bax + Koda bax - Bitdi - + Bitdi + - Autentifikasiya tətbiqi + Autentifikasiya tətbiqi - Birdəfəlik təsdiq kodları yaratmaq üçün autentifikasiya tətbiqi quraşdırın + Birdəfəlik təsdiq kodları yaratmaq üçün autentifikasiya tətbiqi quraşdırın - Daha ətraflı + Daha ətraflı - Autentifikasiya tətbiqi əlavə et + Autentifikasiya tətbiqi əlavə et - Autentifikasiya tətbiqləri + Autentifikasiya tətbiqləri - Autentifikasiya tətbiqi yoxdur + Autentifikasiya tətbiqi yoxdur - \"%1$s\" əlavə edildi + \"%1$s\" əlavə edildi - Autentifikasiya tətbiqi seçimlərini açın + Autentifikasiya tətbiqi seçimlərini açın - Adı dəyiş + Adı dəyiş - Çıxart + Çıxart - Autentifikasiya tətbiqi silinsin? + Autentifikasiya tətbiqi silinsin? - Bu autentifikasiya metodunu silmək üçün autentifikasiya tətbiqinizin yaratdığı 6 rəqəmli kod tələb olunur. + Bu autentifikasiya metodunu silmək üçün autentifikasiya tətbiqinizin yaratdığı 6 rəqəmli kod tələb olunur. - Autentifikasiya tətbiqi əlavə etmək mümkün olmadı + Autentifikasiya tətbiqi əlavə etmək mümkün olmadı - Maksimum %1$d autentifikasiya tətbiqi əlavə edə bilərsiniz. Əvvəl birini silməyə çalışın. + Maksimum %1$d autentifikasiya tətbiqi əlavə edə bilərsiniz. Əvvəl birini silməyə çalışın. - Autentifikasiya tətbiqi silindi + Autentifikasiya tətbiqi silindi - + - Bir ad seçin + Bir ad seçin - Bu autentifikasiya tətbiqi üçün unikal ad seçin. + Bu autentifikasiya tətbiqi üçün unikal ad seçin. - Daha sonra onu tanımağınız üçün bu autentifikasiya tətbiqinə unikal bir ad seçin. + Daha sonra onu tanımağınız üçün bu autentifikasiya tətbiqinə unikal bir ad seçin. - Ad + Ad - Növbəti + Növbəti - Autentifikasiya tətbiqi quraşdırıldı + Autentifikasiya tətbiqi quraşdırıldı - Autentifikaisya tətbiqinin adı dəyişdirildi + Autentifikaisya tətbiqinin adı dəyişdirildi diff --git a/feature/app-settings/src/main/res/values-be/strings.xml b/feature/app-settings/src/main/res/values-be/strings.xml index 73ca43b367..8e4310dae8 100644 --- a/feature/app-settings/src/main/res/values-be/strings.xml +++ b/feature/app-settings/src/main/res/values-be/strings.xml @@ -86,37 +86,37 @@ Даведацца больш - + - Наладзьце праграму аўтэнтыфікацыі + Наладзьце праграму аўтэнтыфікацыі - Зрабіце наступныя крокі, каб наладзіць праграму аўтэнтыфікацыі. + Зрабіце наступныя крокі, каб наладзіць праграму аўтэнтыфікацыі. - Даведацца больш + Даведацца больш - Крок 1 + Крок 1 - Усталюйце на сваю прыладу надзейную праграму аўтэнтыфікацыі. + Усталюйце на сваю прыладу надзейную праграму аўтэнтыфікацыі. - Крок 2 + Крок 2 - Адкрыйце праграму аўтэнтыфікацыі, націснуўшы кнопку ніжэй, каб дадаць свой уліковы запіс Signal. + Адкрыйце праграму аўтэнтыфікацыі, націснуўшы кнопку ніжэй, каб дадаць свой уліковы запіс Signal. - Адкрыць + Адкрыць - Або вы можаце скапіраваць гэты код, каб наладзіць яе ўручную. + Або вы можаце скапіраваць гэты код, каб наладзіць яе ўручную. - Капіраваць + Капіраваць - Скапіравана ў буфер абмену + Скапіравана ў буфер абмену - Праграма аўтэнтыфікацыі не знойдзена + Праграма аўтэнтыфікацыі не знойдзена - Крок 3 + Крок 3 - Скапіруйце згенераваны код і вяртайцеся сюды, каб працягнуць. + Скапіруйце згенераваны код і вяртайцеся сюды, каб працягнуць. - Працягнуць + Працягнуць @@ -146,60 +146,60 @@ Выдаліць - + - Увядзіце свой код + Увядзіце свой код - Увядзіце 6-значны код з праграмы аўтэнтыфікацыі. + Увядзіце 6-значны код з праграмы аўтэнтыфікацыі. - Код + Код - Гатова - + Гатова + - Праграма аўтэнтыфікацыі + Праграма аўтэнтыфікацыі - Наладзьце праграму аўтэнтыфікацыі для стварэння аднаразовых праверачных кодаў + Наладзьце праграму аўтэнтыфікацыі для стварэння аднаразовых праверачных кодаў - Даведацца больш + Даведацца больш - Дадаць праграму аўтэнтыфікацыі + Дадаць праграму аўтэнтыфікацыі - Праграмы аўтэнтыфікацыі + Праграмы аўтэнтыфікацыі - Праграмы аўтэнтыфікацыі не знойдзены + Праграмы аўтэнтыфікацыі не знойдзены - %1$s быў(-ла) даданы(-а) + %1$s быў(-ла) даданы(-а) - Адкрыць параметры праграмы аўтэнтыфікацыі + Адкрыць параметры праграмы аўтэнтыфікацыі - Перайменаваць + Перайменаваць - Выдаліць + Выдаліць - Выдаліць праграму аўтэнтыфікацыі? + Выдаліць праграму аўтэнтыфікацыі? - Каб выдаліць гэты метад аўтэнтыфікацыі, трэба ўвесці 6-значны код з вашай праграмы аўтэнтыфікацыі. + Каб выдаліць гэты метад аўтэнтыфікацыі, трэба ўвесці 6-значны код з вашай праграмы аўтэнтыфікацыі. - Немагчыма дадаць праграму аўтэнтыфікацыі + Немагчыма дадаць праграму аўтэнтыфікацыі - Вы не можаце дадаць больш за %1$d праграм аўтэнтыфікацыі. Паспрабуйце спачатку выдаліць адну. + Вы не можаце дадаць больш за %1$d праграм аўтэнтыфікацыі. Паспрабуйце спачатку выдаліць адну. - Праграма аўтэнтыфікацыі выдалена + Праграма аўтэнтыфікацыі выдалена - + - Выбраць назву + Выбраць назву - Выберыце ўнікальную назву для гэтай праграмы аўтэнтыфікацыі. + Выберыце ўнікальную назву для гэтай праграмы аўтэнтыфікацыі. - Выберыце ўнікальную назву для гэтай праграмы аўтэнтыфікацыі, каб пазней было лягчэй яе ідэнтыфікаваць. + Выберыце ўнікальную назву для гэтай праграмы аўтэнтыфікацыі, каб пазней было лягчэй яе ідэнтыфікаваць. - Імя + Імя - Далей + Далей - Налада праграмы аўтэнтыфікацыі + Налада праграмы аўтэнтыфікацыі - Праграма аўтэнтыфікацыі перайменавана + Праграма аўтэнтыфікацыі перайменавана diff --git a/feature/app-settings/src/main/res/values-bg/strings.xml b/feature/app-settings/src/main/res/values-bg/strings.xml index a034020039..f62b9ff503 100644 --- a/feature/app-settings/src/main/res/values-bg/strings.xml +++ b/feature/app-settings/src/main/res/values-bg/strings.xml @@ -80,37 +80,37 @@ Научете повече - + - Настройте вашето приложение за автентикация + Настройте вашето приложение за автентикация - Следвайте тези стъпки, за да настроите вашето приложение за автентикация. + Следвайте тези стъпки, за да настроите вашето приложение за автентикация. - Научете повече + Научете повече - Стъпка 1 + Стъпка 1 - Инсталирайте приложение за автентикация, на което имате доверие, на своето устройство. + Инсталирайте приложение за автентикация, на което имате доверие, на своето устройство. - Стъпка 2 + Стъпка 2 - Отворете вашето приложение за автентикация, като докоснете бутона по-долу, за да добавите акаунта ви в Signal. + Отворете вашето приложение за автентикация, като докоснете бутона по-долу, за да добавите акаунта ви в Signal. - Отваряне + Отваряне - Или можете да копирате този ключ за ръчна настройка. + Или можете да копирате този ключ за ръчна настройка. - Копиране + Копиране - Копирано + Копирано - Не е открито приложение за автентикация + Не е открито приложение за автентикация - Стъпка 3 + Стъпка 3 - Копирайте генерирания код и се върнете тук, за да продължите. + Копирайте генерирания код и се върнете тук, за да продължите. - Продължаване + Продължаване @@ -140,60 +140,60 @@ Премахване - + - Въведете вашия код + Въведете вашия код - Въведете 6-цифрения код от вашето приложение за автентикация. + Въведете 6-цифрения код от вашето приложение за автентикация. - Код + Код - Готово - + Готово + - Приложение за автентикация + Приложение за автентикация - Настройте приложение за автентикация за генериране на еднократни кодове за потвърждение + Настройте приложение за автентикация за генериране на еднократни кодове за потвърждение - Научете повече + Научете повече - Добавяне на приложение за автентикация + Добавяне на приложение за автентикация - Приложения за автентикация + Приложения за автентикация - Няма приложения за автентикация + Няма приложения за автентикация - Добавихте \"%1$s\" + Добавихте \"%1$s\" - Отваряне на опциите на приложението за автентикация + Отваряне на опциите на приложението за автентикация - Преименуване + Преименуване - Премахване + Премахване - Премахване на приложението за автентикация? + Премахване на приложението за автентикация? - За премахване на този метод за автентикация е необходим 6-цифреният код от вашето приложение за автентикация. + За премахване на този метод за автентикация е необходим 6-цифреният код от вашето приложение за автентикация. - Неуспешно добавяне на приложение за автентикация + Неуспешно добавяне на приложение за автентикация - Не можете да добавите повече от %1$d приложения за автентикация. Опитайте първо да премахнете някое. + Не можете да добавите повече от %1$d приложения за автентикация. Опитайте първо да премахнете някое. - Приложението за автентикация е премахнато + Приложението за автентикация е премахнато - + - Изберете име + Изберете име - Изберете уникално име за това приложение за автентикация. + Изберете уникално име за това приложение за автентикация. - Изберете уникално име за това приложение за автентикация, за да го разпознаете по-късно. + Изберете уникално име за това приложение за автентикация, за да го разпознаете по-късно. - Име + Име - Напред + Напред - Приложението за автентикация е настроено + Приложението за автентикация е настроено - Приложението за автентикация е преименувано + Приложението за автентикация е преименувано diff --git a/feature/app-settings/src/main/res/values-bn/strings.xml b/feature/app-settings/src/main/res/values-bn/strings.xml index 91ced9a74e..06cdc72c65 100644 --- a/feature/app-settings/src/main/res/values-bn/strings.xml +++ b/feature/app-settings/src/main/res/values-bn/strings.xml @@ -80,37 +80,37 @@ আরো জানুন - + - Set up your authenticator app + Set up your authenticator app - আপনার অথেন্টিকেটর অ্যাপ সেট আপ করতে এই ধাপগুলো অনুসরণ করুন। + আপনার অথেন্টিকেটর অ্যাপ সেট আপ করতে এই ধাপগুলো অনুসরণ করুন। - আরো জানুন + আরো জানুন - ধাপ ১ + ধাপ ১ - আপনার ডিভাইসে একটি আস্থাভাজন অথেন্টিকেটর অ্যাপ ইনস্টল করুন। + আপনার ডিভাইসে একটি আস্থাভাজন অথেন্টিকেটর অ্যাপ ইনস্টল করুন। - ধাপ ২ + ধাপ ২ - আপনার Signal অ্যাকাউন্ট যোগ করতে নিচের বাটনটি ট্যাপ করে আপনার অথেন্টিকেটর অ্যাপটি খুলুন। + আপনার Signal অ্যাকাউন্ট যোগ করতে নিচের বাটনটি ট্যাপ করে আপনার অথেন্টিকেটর অ্যাপটি খুলুন। - খুলুন + খুলুন - অথবা আপনি এই কী-টি কপি করে ম্যানুয়ালি সেট আপ করতে পারেন। + অথবা আপনি এই কী-টি কপি করে ম্যানুয়ালি সেট আপ করতে পারেন। - অনুলিপি + অনুলিপি - ক্লিপবোর্ডে কপি করা হয়েছে + ক্লিপবোর্ডে কপি করা হয়েছে - কোনো অথেন্টিকেটর অ্যাপ পাওয়া যায়নি + কোনো অথেন্টিকেটর অ্যাপ পাওয়া যায়নি - ধাপ ৩ + ধাপ ৩ - তৈরি হওয়া কোডটি কপি করুন এবং চালিয়ে যাওয়ার জন্য এখানে ফিরে আসুন। + তৈরি হওয়া কোডটি কপি করুন এবং চালিয়ে যাওয়ার জন্য এখানে ফিরে আসুন। - চলতে থাকুন + চলতে থাকুন @@ -140,60 +140,60 @@ সরিয়ে ফেলুন - + - আপনার কোড লিখুন + আপনার কোড লিখুন - আপনার অথেন্টিকেটর অ্যাপ থেকে ৬-সংখ্যার কোডটি লিখুন। + আপনার অথেন্টিকেটর অ্যাপ থেকে ৬-সংখ্যার কোডটি লিখুন। - কোড + কোড - সম্পন্ন হয়েছে - + সম্পন্ন হয়েছে + - অথেন্টিকেটর অ্যাপ + অথেন্টিকেটর অ্যাপ - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - আরো জানুন + আরো জানুন - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - \"%1$s\" যুক্ত হয়েছে + \"%1$s\" যুক্ত হয়েছে - Open authenticator app options + Open authenticator app options - রিনেম করুন + রিনেম করুন - সরিয়ে ফেলুন + সরিয়ে ফেলুন - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - একটি নাম চয়ন করুন + একটি নাম চয়ন করুন - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - নাম + নাম - পরবর্তী + পরবর্তী - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-bs/strings.xml b/feature/app-settings/src/main/res/values-bs/strings.xml index ae292417fb..6291a6df15 100644 --- a/feature/app-settings/src/main/res/values-bs/strings.xml +++ b/feature/app-settings/src/main/res/values-bs/strings.xml @@ -86,37 +86,37 @@ Saznaj više - + - Set up your authenticator app + Set up your authenticator app - Slijedite ove korake da postavite aplikaciju za autentifikaciju. + Slijedite ove korake da postavite aplikaciju za autentifikaciju. - Saznaj više + Saznaj više - 1. korak + 1. korak - Instalirajte pouzdanu aplikaciju za autentifikaciju na svoj uređaj. + Instalirajte pouzdanu aplikaciju za autentifikaciju na svoj uređaj. - 2. korak + 2. korak - Otvorite aplikaciju za autentifikaciju dodirom na dugme u nastavku kako biste dodali svoj Signal račun. + Otvorite aplikaciju za autentifikaciju dodirom na dugme u nastavku kako biste dodali svoj Signal račun. - Otvori + Otvori - Ili možete kopirati ovaj ključ za ručno postavljanje. + Ili možete kopirati ovaj ključ za ručno postavljanje. - Kopiraj + Kopiraj - Kopirano u međuspremnik + Kopirano u međuspremnik - Aplikacija za autentifikaciju nije pronađena + Aplikacija za autentifikaciju nije pronađena - 3. korak + 3. korak - Kopirajte generirani kôd i vratite se ovdje da nastavite. + Kopirajte generirani kôd i vratite se ovdje da nastavite. - Nastavi + Nastavi @@ -146,60 +146,60 @@ Ukloni - + - Unesite svoj kôd + Unesite svoj kôd - Unesite šestocifreni kôd iz aplikacije za autentifikaciju. + Unesite šestocifreni kôd iz aplikacije za autentifikaciju. - Kȏd + Kȏd - Gotovo - + Gotovo + - Aplikacija za autentifikaciju + Aplikacija za autentifikaciju - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Saznaj više + Saznaj više - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Uvršten/a \"%1$s\" + Uvršten/a \"%1$s\" - Open authenticator app options + Open authenticator app options - Rename + Rename - Ukloni + Ukloni - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Name + Name - Dalje + Dalje - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-ca/strings.xml b/feature/app-settings/src/main/res/values-ca/strings.xml index 7c8e70f289..8bc6fa4928 100644 --- a/feature/app-settings/src/main/res/values-ca/strings.xml +++ b/feature/app-settings/src/main/res/values-ca/strings.xml @@ -80,37 +80,37 @@ Més informació - + - Set up your authenticator app + Set up your authenticator app - Segueix aquests passos per configurar la teva app d\'autenticació. + Segueix aquests passos per configurar la teva app d\'autenticació. - Més informació + Més informació - Pas 1 + Pas 1 - Instal·la una app d\'autenticació de confiança al teu dispositiu. + Instal·la una app d\'autenticació de confiança al teu dispositiu. - Pas 2 + Pas 2 - Toca el botó d\'aquí baix per obrir la teva app d\'autenticació i afegir el teu compte de Signal. + Toca el botó d\'aquí baix per obrir la teva app d\'autenticació i afegir el teu compte de Signal. - Obrir + Obrir - O copia aquesta clau per configurar-la de forma manual. + O copia aquesta clau per configurar-la de forma manual. - Copia + Copia - Copiat al portapapers + Copiat al portapapers - No s\'ha trobat cap app d\'autenticació + No s\'ha trobat cap app d\'autenticació - Pas 3 + Pas 3 - Copia la clau que generada i torna aquí per continuar. + Copia la clau que generada i torna aquí per continuar. - Continua + Continua @@ -140,60 +140,60 @@ Suprimeix - + - Introdueix la teva clau + Introdueix la teva clau - Introdueix la clau de 6 dígits de la teva app d\'autenticació. + Introdueix la clau de 6 dígits de la teva app d\'autenticació. - Codi + Codi - Fet - + Fet + - App d\'autenticació + App d\'autenticació - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Més informació + Més informació - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - S\'hi ha afegit %1$s + S\'hi ha afegit %1$s - Open authenticator app options + Open authenticator app options - Canviar el nom + Canviar el nom - Suprimeix + Suprimeix - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Trieu un nom + Trieu un nom - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Nom + Nom - Següent + Següent - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-cs/strings.xml b/feature/app-settings/src/main/res/values-cs/strings.xml index 4aef20325c..91a503f3ce 100644 --- a/feature/app-settings/src/main/res/values-cs/strings.xml +++ b/feature/app-settings/src/main/res/values-cs/strings.xml @@ -86,37 +86,37 @@ Zjistit více - + - Nastavte si ověřovací aplikaci + Nastavte si ověřovací aplikaci - Pro nastavení ověřovací aplikace postupujte takto: + Pro nastavení ověřovací aplikace postupujte takto: - Zjistit více + Zjistit více - Krok 1 + Krok 1 - Nainstalujte si do zařízení důvěryhodnou ověřovací aplikaci. + Nainstalujte si do zařízení důvěryhodnou ověřovací aplikaci. - Krok 2 + Krok 2 - Klepnutím na tlačítko níže otevřete ověřovací aplikaci a přidejte svůj účet Signal. + Klepnutím na tlačítko níže otevřete ověřovací aplikaci a přidejte svůj účet Signal. - Otevřít + Otevřít - Případně můžete tento klíč zkopírovat a provést ruční nastavení. + Případně můžete tento klíč zkopírovat a provést ruční nastavení. - Kopírovat + Kopírovat - Zkopírováno do schránky + Zkopírováno do schránky - Nebyla nalezena žádná ověřovací aplikace + Nebyla nalezena žádná ověřovací aplikace - Krok 3 + Krok 3 - Zkopírujte vygenerovaný kód a zadejte sem, abyste mohli pokračovat. + Zkopírujte vygenerovaný kód a zadejte sem, abyste mohli pokračovat. - Pokračovat + Pokračovat @@ -146,60 +146,60 @@ Odstranit - + - Zadejte kód + Zadejte kód - Zadejte 6číselný kód ze své ověřovací aplikace. + Zadejte 6číselný kód ze své ověřovací aplikace. - Kód + Kód - Hotovo - + Hotovo + - Ověřovací aplikace + Ověřovací aplikace - Nastavte si ověřovací aplikaci pro generování jednorázových ověřovacích kódů + Nastavte si ověřovací aplikaci pro generování jednorázových ověřovacích kódů - Zjistit více + Zjistit více - Přidat ověřovací aplikaci + Přidat ověřovací aplikaci - Ověřovací aplikace + Ověřovací aplikace - Žádná ověřovací aplikace + Žádná ověřovací aplikace - Přidán %1$s + Přidán %1$s - Otevřít možnosti ověřovací aplikace + Otevřít možnosti ověřovací aplikace - Přejmenovat + Přejmenovat - Odstranit + Odstranit - Odstranit ověřovací aplikaci? + Odstranit ověřovací aplikaci? - K odstranění této ověřovací metody je potřeba 6místný kód z vaší ověřovací aplikace. + K odstranění této ověřovací metody je potřeba 6místný kód z vaší ověřovací aplikace. - Ověřovací aplikaci nelze přidat + Ověřovací aplikaci nelze přidat - Nelze přidat více než %1$d ověřovací aplikaci. Zkuste nejprve jednu odstranit. + Nelze přidat více než %1$d ověřovací aplikaci. Zkuste nejprve jednu odstranit. - Ověřovací aplikace odstraněna + Ověřovací aplikace odstraněna - + - Zvolte jméno + Zvolte jméno - Zvolte jedinečný název pro tuto ověřovací aplikaci. + Zvolte jedinečný název pro tuto ověřovací aplikaci. - Zvolte jedinečný název pro tuto ověřovací aplikaci, abyste ji později snadno rozpoznali. + Zvolte jedinečný název pro tuto ověřovací aplikaci, abyste ji později snadno rozpoznali. - Název + Název - Další + Další - Ověřovací aplikace nastavena + Ověřovací aplikace nastavena - Ověřovací aplikace přejmenována + Ověřovací aplikace přejmenována diff --git a/feature/app-settings/src/main/res/values-da/strings.xml b/feature/app-settings/src/main/res/values-da/strings.xml index 78a35e43e8..3f52205404 100644 --- a/feature/app-settings/src/main/res/values-da/strings.xml +++ b/feature/app-settings/src/main/res/values-da/strings.xml @@ -80,37 +80,37 @@ Få mere at vide - + - Opsæt din godkendelsesapp + Opsæt din godkendelsesapp - Følg disse trin til opsætning af din godkendelsesapp. + Følg disse trin til opsætning af din godkendelsesapp. - Få mere at vide + Få mere at vide - Trin 1 + Trin 1 - Installer en godkendt godkendelsesapp på din enhed. + Installer en godkendt godkendelsesapp på din enhed. - Trin 2 + Trin 2 - Åbn din godkendelsesapp ved at trykke på knappen nedenfor for at tilføje din Signal-konto. + Åbn din godkendelsesapp ved at trykke på knappen nedenfor for at tilføje din Signal-konto. - Åbn + Åbn - Eller du kan kopiere denne nøgle til manuel opsætning. + Eller du kan kopiere denne nøgle til manuel opsætning. - Kopiér + Kopiér - Kopieret til udklipsholder + Kopieret til udklipsholder - Ingen authenticator-app fundet + Ingen authenticator-app fundet - Trin 3 + Trin 3 - Kopiér den genererede kode, og vend tilbage hertil for at fortsætte. + Kopiér den genererede kode, og vend tilbage hertil for at fortsætte. - Fortsæt + Fortsæt @@ -140,60 +140,60 @@ Fjern - + - Angiv din kode + Angiv din kode - Angiv den sekscifrede kode fra din godkendelsesapp. + Angiv den sekscifrede kode fra din godkendelsesapp. - Kode + Kode - Færdig - + Færdig + - Godkendelsesapp + Godkendelsesapp - Opsæt en godkendelsesapp til at generere engangsbekræftelseskoder + Opsæt en godkendelsesapp til at generere engangsbekræftelseskoder - Få mere at vide + Få mere at vide - Tilføj godkendelsesapp + Tilføj godkendelsesapp - Godkendelsesapps + Godkendelsesapps - Ingen godkendelsesapps + Ingen godkendelsesapps - \"%1$s\" tilføjet + \"%1$s\" tilføjet - Åbn indstillinger for godkendelsesapp + Åbn indstillinger for godkendelsesapp - Omdøb + Omdøb - Fjern + Fjern - Vil du godkendelsesappen? + Vil du godkendelsesappen? - For at fjerne denne godkendelsesmetode skal du bruge den sekscifrede kode fra din godkendelsesapp. + For at fjerne denne godkendelsesmetode skal du bruge den sekscifrede kode fra din godkendelsesapp. - Kan ikke tilføje godkendelsesapp + Kan ikke tilføje godkendelsesapp - Du kan ikke tilføje mere end %1$d godkendelsesapps. Prøv at fjerne en først. + Du kan ikke tilføje mere end %1$d godkendelsesapps. Prøv at fjerne en først. - Godkendelsesapp fjernet + Godkendelsesapp fjernet - + - Skriv et navn + Skriv et navn - Vælg et unikt navn til denne godkendelsesapp. + Vælg et unikt navn til denne godkendelsesapp. - Vælg et unikt navn til denne godkendelsesapp, så du nemt kan genkende den senere. + Vælg et unikt navn til denne godkendelsesapp, så du nemt kan genkende den senere. - Navn + Navn - Næste + Næste - Godkendelsesapp er opsat + Godkendelsesapp er opsat - Godkendelsesapp er omdøbt + Godkendelsesapp er omdøbt diff --git a/feature/app-settings/src/main/res/values-de/strings.xml b/feature/app-settings/src/main/res/values-de/strings.xml index a5489e029f..fa24eb458e 100644 --- a/feature/app-settings/src/main/res/values-de/strings.xml +++ b/feature/app-settings/src/main/res/values-de/strings.xml @@ -80,37 +80,37 @@ Mehr erfahren - + - Set up your authenticator app + Set up your authenticator app - Befolge diese Schritte, um deine Authentifizierungs-App einzurichten. + Befolge diese Schritte, um deine Authentifizierungs-App einzurichten. - Mehr erfahren + Mehr erfahren - 1. Schritt + 1. Schritt - Installiere eine vertrauenswürdige Authentifizierungs-App auf deinem Gerät. + Installiere eine vertrauenswürdige Authentifizierungs-App auf deinem Gerät. - 2. Schritt + 2. Schritt - Öffne deine Authentifizierungs-App, indem du auf die Schaltfläche unten tippst, um dein Signal-Konto hinzuzufügen. + Öffne deine Authentifizierungs-App, indem du auf die Schaltfläche unten tippst, um dein Signal-Konto hinzuzufügen. - Öffnen + Öffnen - Oder kopiere diesen Schlüssel für eine manuelle Einrichtung. + Oder kopiere diesen Schlüssel für eine manuelle Einrichtung. - Kopieren + Kopieren - In Zwischenablage kopiert + In Zwischenablage kopiert - Keine Authentifizierungs-App gefunden + Keine Authentifizierungs-App gefunden - 3. Schritt + 3. Schritt - Kopiere den generierten Code und komm hierher zurück, um fortzufahren. + Kopiere den generierten Code und komm hierher zurück, um fortzufahren. - Weiter + Weiter @@ -140,60 +140,60 @@ Entfernen - + - Gib deinen Code ein + Gib deinen Code ein - Gib den 6-stelligen Code aus deiner Authentifizierungs-App ein. + Gib den 6-stelligen Code aus deiner Authentifizierungs-App ein. - Code + Code - Fertig - + Fertig + - Authentifizierungs-App + Authentifizierungs-App - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Mehr erfahren + Mehr erfahren - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - %1$s hinzugefügt + %1$s hinzugefügt - Open authenticator app options + Open authenticator app options - Umbenennen + Umbenennen - Entfernen + Entfernen - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Name wählen + Name wählen - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Name + Name - Weiter + Weiter - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-el/strings.xml b/feature/app-settings/src/main/res/values-el/strings.xml index b0ed69cae4..7ec5d16a05 100644 --- a/feature/app-settings/src/main/res/values-el/strings.xml +++ b/feature/app-settings/src/main/res/values-el/strings.xml @@ -80,37 +80,37 @@ Μάθε περισσότερα - + - Set up your authenticator app + Set up your authenticator app - Ακολούθησε αυτά τα βήματα για να ρυθμίσεις την εφαρμογή επαλήθευσης. + Ακολούθησε αυτά τα βήματα για να ρυθμίσεις την εφαρμογή επαλήθευσης. - Μάθε περισσότερα + Μάθε περισσότερα - Βήμα 1 + Βήμα 1 - Εγκατάστησε μια έμπιστη εφαρμογή επαλήθευσης στη συσκευή σου. + Εγκατάστησε μια έμπιστη εφαρμογή επαλήθευσης στη συσκευή σου. - Βήμα 2 + Βήμα 2 - Άνοιξε την εφαρμογή επαλήθευσης, πατώντας το κουμπί παρακάτω ώστε να προσθέσεις τον λογαριασμό σου Signal. + Άνοιξε την εφαρμογή επαλήθευσης, πατώντας το κουμπί παρακάτω ώστε να προσθέσεις τον λογαριασμό σου Signal. - Άνοιγμα + Άνοιγμα - Ή μπορείς να αντιγράψεις αυτό το κλειδί χειροκίνητα για να τον προσθέσεις. + Ή μπορείς να αντιγράψεις αυτό το κλειδί χειροκίνητα για να τον προσθέσεις. - Αντιγραφή + Αντιγραφή - Αντιγράφηκε στο πρόχειρο + Αντιγράφηκε στο πρόχειρο - Δεν βρέθηκε εφαρμογή επαλήθευσης + Δεν βρέθηκε εφαρμογή επαλήθευσης - Βήμα 3 + Βήμα 3 - Αντίγραψε τον κωδικό που δημιουργήθηκε και επίστρεψε εδώ για να συνεχίσεις. + Αντίγραψε τον κωδικό που δημιουργήθηκε και επίστρεψε εδώ για να συνεχίσεις. - Συνέχεια + Συνέχεια @@ -140,60 +140,60 @@ Αφαίρεση - + - Γράψε τον κωδικό σου + Γράψε τον κωδικό σου - Γράψε τον 6-ψηφιο κωδικό από την εφαρμογή επαλήθευσης. + Γράψε τον 6-ψηφιο κωδικό από την εφαρμογή επαλήθευσης. - Κωδικός + Κωδικός - Τέλος - + Τέλος + - Εφαρμογή επαλήθευσης + Εφαρμογή επαλήθευσης - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Μάθε περισσότερα + Μάθε περισσότερα - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Ο/Η \"%1$s\" προστέθηκε + Ο/Η \"%1$s\" προστέθηκε - Open authenticator app options + Open authenticator app options - Μετονομασία + Μετονομασία - Αφαίρεση + Αφαίρεση - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Διάλεξε όνομα + Διάλεξε όνομα - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Όνομα + Όνομα - Επόμενο + Επόμενο - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-es/strings.xml b/feature/app-settings/src/main/res/values-es/strings.xml index e84f3c9a5a..56181b6fb6 100644 --- a/feature/app-settings/src/main/res/values-es/strings.xml +++ b/feature/app-settings/src/main/res/values-es/strings.xml @@ -80,37 +80,37 @@ Más información - + - Set up your authenticator app + Set up your authenticator app - Sigue estos pasos para configurar tu aplicación de autenticación. + Sigue estos pasos para configurar tu aplicación de autenticación. - Más información + Más información - Paso 1 + Paso 1 - Instala una aplicación de autenticación de confianza en tu dispositivo. + Instala una aplicación de autenticación de confianza en tu dispositivo. - Paso 2 + Paso 2 - Toca el botón de abajo para abrir tu aplicación de autenticación y añadir tu cuenta de Signal. + Toca el botón de abajo para abrir tu aplicación de autenticación y añadir tu cuenta de Signal. - Abrir + Abrir - También puedes copiar esta clave para configurar la app de forma manual. + También puedes copiar esta clave para configurar la app de forma manual. - Copiar + Copiar - Copiado al portapapeles + Copiado al portapapeles - No se ha encontrado ninguna aplicación de autenticación + No se ha encontrado ninguna aplicación de autenticación - Paso 3 + Paso 3 - Copia la clave que se genere y vuelve aquí para continuar. + Copia la clave que se genere y vuelve aquí para continuar. - Continuar + Continuar @@ -140,60 +140,60 @@ Eliminar - + - Introduce tu clave + Introduce tu clave - Introduce la clave de 6 dígitos de tu aplicación de autenticación. + Introduce la clave de 6 dígitos de tu aplicación de autenticación. - Código + Código - Listo - + Listo + - Aplicación de autenticación + Aplicación de autenticación - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Más información + Más información - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Se ha añadido a %1$s + Se ha añadido a %1$s - Open authenticator app options + Open authenticator app options - Cambiar nombre + Cambiar nombre - Eliminar + Eliminar - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Elige un nombre + Elige un nombre - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Nombre + Nombre - Siguiente + Siguiente - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-et/strings.xml b/feature/app-settings/src/main/res/values-et/strings.xml index 39dfd64a5f..f4268afd21 100644 --- a/feature/app-settings/src/main/res/values-et/strings.xml +++ b/feature/app-settings/src/main/res/values-et/strings.xml @@ -80,37 +80,37 @@ Rohkem teavet - + - Seadista oma autentimise äpp + Seadista oma autentimise äpp - Järgi neid samme, et oma autentimise äppi seadistada. + Järgi neid samme, et oma autentimise äppi seadistada. - Rohkem teavet + Rohkem teavet - Samm 1 + Samm 1 - Paigalda oma seadmesse usaldusväärne autentimise äpp. + Paigalda oma seadmesse usaldusväärne autentimise äpp. - Samm 2 + Samm 2 - Ava oma autentimise äpp, puudutades allolevat nuppu, et oma Signali konto lisada. + Ava oma autentimise äpp, puudutades allolevat nuppu, et oma Signali konto lisada. - Ava + Ava - Võid ka selle võtme kopeerida, et käsitsi seadistada. + Võid ka selle võtme kopeerida, et käsitsi seadistada. - Kopeeri + Kopeeri - Lõikelauale kopeeritud + Lõikelauale kopeeritud - Ühtegi autentimise äppi ei leitud + Ühtegi autentimise äppi ei leitud - Samm 3 + Samm 3 - Kopeeri loodud kood ja tule siia tagasi, et jätkata. + Kopeeri loodud kood ja tule siia tagasi, et jätkata. - Jätka + Jätka @@ -140,60 +140,60 @@ Eemalda - + - Sisesta oma kood + Sisesta oma kood - Sisesta oma autentimise äpist 6-kohaline kood. + Sisesta oma autentimise äpist 6-kohaline kood. - Kood + Kood - Valmis - + Valmis + - Autentimise äpp + Autentimise äpp - Seadista autentimise äpp ühekordsete kinnituskoodide loomiseks + Seadista autentimise äpp ühekordsete kinnituskoodide loomiseks - Rohkem teavet + Rohkem teavet - Lisa autentimise äpp + Lisa autentimise äpp - Autentimise äpid + Autentimise äpid - Ühtegi autentimise äppi ei ole + Ühtegi autentimise äppi ei ole - \"%1$s\" lisatud + \"%1$s\" lisatud - Ava autentimise äpi valikud + Ava autentimise äpi valikud - Nimeta ümber + Nimeta ümber - Eemalda + Eemalda - Kas eemaldada autentimise äpp? + Kas eemaldada autentimise äpp? - Selle autentimise meetodi eemaldamiseks on vaja 6-kohalist koodi su autentimise äpist. + Selle autentimise meetodi eemaldamiseks on vaja 6-kohalist koodi su autentimise äpist. - Autentimise äppi ei saa lisada + Autentimise äppi ei saa lisada - Sa ei saa lisada rohkem kui %1$d autentimise äppi. Proovi enne üks eemaldada. + Sa ei saa lisada rohkem kui %1$d autentimise äppi. Proovi enne üks eemaldada. - Autentimise äpp on eemaldatud + Autentimise äpp on eemaldatud - + - Vali nimi + Vali nimi - Vali selle autentimise äpi jaoks kordumatu nimi. + Vali selle autentimise äpi jaoks kordumatu nimi. - Vali selle autentimise äpi jaoks kordumatu nimi, mis aitab sul seda hiljem leida. + Vali selle autentimise äpi jaoks kordumatu nimi, mis aitab sul seda hiljem leida. - Nimi + Nimi - Edasi + Edasi - Autentimise äpp on seadistatud + Autentimise äpp on seadistatud - Autentimise äpp on ümber nimetatud + Autentimise äpp on ümber nimetatud diff --git a/feature/app-settings/src/main/res/values-eu/strings.xml b/feature/app-settings/src/main/res/values-eu/strings.xml index 741975cf81..c0be4a8dfa 100644 --- a/feature/app-settings/src/main/res/values-eu/strings.xml +++ b/feature/app-settings/src/main/res/values-eu/strings.xml @@ -80,37 +80,37 @@ Informazio gehiago - + - Set up your authenticator app + Set up your authenticator app - Jarraitu urrats hauek autentifikatzeko aplikazioan konfiguratzeko. + Jarraitu urrats hauek autentifikatzeko aplikazioan konfiguratzeko. - Informazio gehiago + Informazio gehiago - 1. urratsa + 1. urratsa - Instalatu autentifikatzeko aplikazio fidagarri bat zure gailuan. + Instalatu autentifikatzeko aplikazio fidagarri bat zure gailuan. - 2. urratsa + 2. urratsa - Ireki autentifikatzeko aplikazioa beheko botoia sakatuz Signal-eko kontua gehitzeko. + Ireki autentifikatzeko aplikazioa beheko botoia sakatuz Signal-eko kontua gehitzeko. - Ireki + Ireki - Edo gako hau kopia dezakezu eskuz konfiguratzeko. + Edo gako hau kopia dezakezu eskuz konfiguratzeko. - Kopiatu + Kopiatu - Arbelera kopiatuta + Arbelera kopiatuta - Ez da autentifikatzeko aplikaziorik aurkitu + Ez da autentifikatzeko aplikaziorik aurkitu - 3. urratsa + 3. urratsa - Kopiatu sortu den kodea eta itzuli hona jarraitzeko. + Kopiatu sortu den kodea eta itzuli hona jarraitzeko. - Jarraitu + Jarraitu @@ -140,60 +140,60 @@ Kendu - + - Idatzi kodea + Idatzi kodea - Idatzi 6 digituko kodea autentifikatzeko aplikaziotik. + Idatzi 6 digituko kodea autentifikatzeko aplikaziotik. - Kodea + Kodea - Eginda - + Eginda + - Autentifikatzeko aplikazioa + Autentifikatzeko aplikazioa - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Informazio gehiago + Informazio gehiago - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - \"%1$s\" gehitua + \"%1$s\" gehitua - Open authenticator app options + Open authenticator app options - Rename + Rename - Kendu + Kendu - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Name + Name - Hurrengoa + Hurrengoa - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-fa/strings.xml b/feature/app-settings/src/main/res/values-fa/strings.xml index 5ab79bacce..cdd9ff661d 100644 --- a/feature/app-settings/src/main/res/values-fa/strings.xml +++ b/feature/app-settings/src/main/res/values-fa/strings.xml @@ -80,37 +80,37 @@ اطلاعات بیشتر - + - راه‌اندازی برنامه احراز هویت + راه‌اندازی برنامه احراز هویت - برای راه‌اندازی برنامه احراز هویت خود، این مراحل را دنبال کنید. + برای راه‌اندازی برنامه احراز هویت خود، این مراحل را دنبال کنید. - اطلاعات بیشتر + اطلاعات بیشتر - مرحله ۱ + مرحله ۱ - یک برنامه احراز هویت معتبر را روی دستگاهتان نصب کنید. + یک برنامه احراز هویت معتبر را روی دستگاهتان نصب کنید. - مرحله ۲ + مرحله ۲ - با ضربه زدن روی دکمه زیر، برنامه احراز هویت را باز کنید تا حساب سیگنال خود را اضافه کنید. + با ضربه زدن روی دکمه زیر، برنامه احراز هویت را باز کنید تا حساب سیگنال خود را اضافه کنید. - باز کردن + باز کردن - یا می‌توانید این رمز را کپی کنید تا آن را به‌صورت دستی راه‌اندازی کنید. + یا می‌توانید این رمز را کپی کنید تا آن را به‌صورت دستی راه‌اندازی کنید. - کپی + کپی - در کلیپ‌بورد کپی شد + در کلیپ‌بورد کپی شد - هیچ برنامه احراز هویتی پیدا نشد + هیچ برنامه احراز هویتی پیدا نشد - مرحله ۳ + مرحله ۳ - کد تولیدشده را کپی کنید و برای ادامه به اینجا برگردید. + کد تولیدشده را کپی کنید و برای ادامه به اینجا برگردید. - ادامه + ادامه @@ -140,60 +140,60 @@ حذف - + - کد خود را وارد کنید + کد خود را وارد کنید - کد ۶ رقمی را از برنامه احراز هویت خود وارد کنید. + کد ۶ رقمی را از برنامه احراز هویت خود وارد کنید. - کد + کد - انجام شد - + انجام شد + - برنامه احراز هویت + برنامه احراز هویت - یک برنامه احراز هویت برای تولید کدهای تأیید یکبارمصرف راه‌اندازی کنید + یک برنامه احراز هویت برای تولید کدهای تأیید یکبارمصرف راه‌اندازی کنید - اطلاعات بیشتر + اطلاعات بیشتر - افزودن برنامه احراز هویت + افزودن برنامه احراز هویت - برنامه‌های احراز هویت + برنامه‌های احراز هویت - هیچ برنامه احراز هویتی وجود ندارد + هیچ برنامه احراز هویتی وجود ندارد - «%1$s» اضافه شد + «%1$s» اضافه شد - باز کردن گزینه‌های برنامه احراز هویت + باز کردن گزینه‌های برنامه احراز هویت - تغییر نام + تغییر نام - حذف + حذف - برنامه احراز هویت حذف شود؟ + برنامه احراز هویت حذف شود؟ - برای حذف این روش احراز هویت، کد ۶ رقمی از برنامه احراز هویت شما مورد نیاز است. + برای حذف این روش احراز هویت، کد ۶ رقمی از برنامه احراز هویت شما مورد نیاز است. - افزودن برنامه احراز هویت امکان‌پذیر نیست + افزودن برنامه احراز هویت امکان‌پذیر نیست - نمی‌توانید بیش از %1$d برنامه احراز هویت اضافه کنید. ابتدا یکی را حذف کنید. + نمی‌توانید بیش از %1$d برنامه احراز هویت اضافه کنید. ابتدا یکی را حذف کنید. - برنامه احراز هویت حذف شد + برنامه احراز هویت حذف شد - + - انتخاب یک نام + انتخاب یک نام - یک نام منحصربه‌فرد برای این برنامه احراز هویت انتخاب کنید. + یک نام منحصربه‌فرد برای این برنامه احراز هویت انتخاب کنید. - یک نام منحصربه‌فرد برای این برنامه احراز هویت انتخاب کنید تا بعداً بتوانید آن را شناسایی کنید. + یک نام منحصربه‌فرد برای این برنامه احراز هویت انتخاب کنید تا بعداً بتوانید آن را شناسایی کنید. - نام + نام - بعدی + بعدی - راه‌اندازی برنامه احراز هویت + راه‌اندازی برنامه احراز هویت - برنامه احراز هویت تغییر نام داد + برنامه احراز هویت تغییر نام داد diff --git a/feature/app-settings/src/main/res/values-fi/strings.xml b/feature/app-settings/src/main/res/values-fi/strings.xml index 6166e68205..57ed641f98 100644 --- a/feature/app-settings/src/main/res/values-fi/strings.xml +++ b/feature/app-settings/src/main/res/values-fi/strings.xml @@ -80,37 +80,37 @@ Lue lisää - + - Set up your authenticator app + Set up your authenticator app - Seuraa näitä ohjeita autentikointisovelluksen määrittämiseksi: + Seuraa näitä ohjeita autentikointisovelluksen määrittämiseksi: - Lue lisää + Lue lisää - Vaihe 1 + Vaihe 1 - Asenna laitteelle luotettu autentikointisovellus. + Asenna laitteelle luotettu autentikointisovellus. - Vaihe 2 + Vaihe 2 - Avaa autentikointisovellus ja lisää se Signal-tilliisi napauttamalla alla olevaa painiketta. + Avaa autentikointisovellus ja lisää se Signal-tilliisi napauttamalla alla olevaa painiketta. - Avaa + Avaa - Voit myös kopioida avaimen ja määrittää sovelluksen käsin. + Voit myös kopioida avaimen ja määrittää sovelluksen käsin. - Kopioi + Kopioi - Kopioitu leikepöydälle + Kopioitu leikepöydälle - Autentikointisovellusta ei löytynyt + Autentikointisovellusta ei löytynyt - Vaihe 3 + Vaihe 3 - Kopioi sovelluksen luoma koodi ja palaa tänne jatkaaksesi. + Kopioi sovelluksen luoma koodi ja palaa tänne jatkaaksesi. - Jatka + Jatka @@ -140,60 +140,60 @@ Poista - + - Anna koodi + Anna koodi - Anna autentikointisovelluksen antama kuusinumeroinen koodi. + Anna autentikointisovelluksen antama kuusinumeroinen koodi. - Koodi + Koodi - Valmis - + Valmis + - Autentikointisovellus + Autentikointisovellus - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Lue lisää + Lue lisää - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Käyttäjä %1$s lisätty + Käyttäjä %1$s lisätty - Open authenticator app options + Open authenticator app options - Nimeä uudelleen + Nimeä uudelleen - Poista + Poista - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Valitse nimi + Valitse nimi - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Nimi + Nimi - Seuraava + Seuraava - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-fr/strings.xml b/feature/app-settings/src/main/res/values-fr/strings.xml index a436c7c1b0..7f461081f0 100644 --- a/feature/app-settings/src/main/res/values-fr/strings.xml +++ b/feature/app-settings/src/main/res/values-fr/strings.xml @@ -80,37 +80,37 @@ En savoir plus - + - Set up your authenticator app + Set up your authenticator app - Suivez les étapes ci-dessous pour configurer votre appli d\'authentification. + Suivez les étapes ci-dessous pour configurer votre appli d\'authentification. - En savoir plus + En savoir plus - Étape 1 + Étape 1 - Installez une appli d\'authentification fiable sur votre appareil. + Installez une appli d\'authentification fiable sur votre appareil. - Étape 2 + Étape 2 - Appuyez sur le bouton ci-dessous pour ouvrir l\'appli d\'authentification et y ajouter votre compte Signal. + Appuyez sur le bouton ci-dessous pour ouvrir l\'appli d\'authentification et y ajouter votre compte Signal. - Ouvrir + Ouvrir - Vous pouvez aussi copier cette clé si vous préférez configurer l\'appli manuellement. + Vous pouvez aussi copier cette clé si vous préférez configurer l\'appli manuellement. - Copier + Copier - Copié dans le presse-papiers + Copié dans le presse-papiers - Aucune appli d\'authentification n\'est disponible + Aucune appli d\'authentification n\'est disponible - Étape 3 + Étape 3 - Copiez le code généré par l\'appli d\'authentification et revenez à cet écran pour continuer. + Copiez le code généré par l\'appli d\'authentification et revenez à cet écran pour continuer. - Continuer + Continuer @@ -140,60 +140,60 @@ Retirer - + - Saisissez votre code + Saisissez votre code - Saisissez le code à 6 chiffres généré par votre appli d\'authentification. + Saisissez le code à 6 chiffres généré par votre appli d\'authentification. - Code + Code - OK - + OK + - Appli d\'authentification + Appli d\'authentification - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - En savoir plus + En savoir plus - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - %1$s a été ajouté. + %1$s a été ajouté. - Open authenticator app options + Open authenticator app options - Renommer + Renommer - Retirer + Retirer - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choisissez un nom + Choisissez un nom - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Nom + Nom - Suivant + Suivant - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-ga/strings.xml b/feature/app-settings/src/main/res/values-ga/strings.xml index a7dda2cf3f..45d4b3a71f 100644 --- a/feature/app-settings/src/main/res/values-ga/strings.xml +++ b/feature/app-settings/src/main/res/values-ga/strings.xml @@ -89,37 +89,37 @@ Tuilleadh faisnéise - + - Set up your authenticator app + Set up your authenticator app - Lean na céimeanna seo a leanas le d\'aip fíordheimhnitheora a chumrú. + Lean na céimeanna seo a leanas le d\'aip fíordheimhnitheora a chumrú. - Tuilleadh faisnéise + Tuilleadh faisnéise - Céim 1 + Céim 1 - Suiteáil aip fíordheimhnitheora iontaofa ar do ghléas. + Suiteáil aip fíordheimhnitheora iontaofa ar do ghléas. - Céim 2 + Céim 2 - Oscail d\'aip fíordheimhnitheora tríd an gcnaipe thíos a thapáil lena cur le do chuntas Signal. + Oscail d\'aip fíordheimhnitheora tríd an gcnaipe thíos a thapáil lena cur le do chuntas Signal. - Oscail + Oscail - Nó is féidir leat an eochair seo a chóipeáil lena chumrú de láimh. + Nó is féidir leat an eochair seo a chóipeáil lena chumrú de láimh. - Cóipeáil + Cóipeáil - Cóipeáilte chuig an ngearrthaisce + Cóipeáilte chuig an ngearrthaisce - Níor aimsíodh aip fíordheimhnitheora + Níor aimsíodh aip fíordheimhnitheora - Céim 3 + Céim 3 - Cóipeáil an cód a gineadh agus fill anseo le leanúint leis. + Cóipeáil an cód a gineadh agus fill anseo le leanúint leis. - Lean ar aghaidh + Lean ar aghaidh @@ -149,60 +149,60 @@ Bain é - + - Cuir isteach do chód + Cuir isteach do chód - Cuir isteach an cód 6 dhigit ó d\'aip fíordheimhnitheora. + Cuir isteach an cód 6 dhigit ó d\'aip fíordheimhnitheora. - Cód + Cód - Déanta - + Déanta + - Aip fíordheimhnitheora + Aip fíordheimhnitheora - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Tuilleadh faisnéise + Tuilleadh faisnéise - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - %1$s curtha leis + %1$s curtha leis - Open authenticator app options + Open authenticator app options - Athainmnigh + Athainmnigh - Bain é + Bain é - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Roghnaigh ainm + Roghnaigh ainm - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Ainm + Ainm - Ar aghaidh + Ar aghaidh - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-gl/strings.xml b/feature/app-settings/src/main/res/values-gl/strings.xml index ea3fc5e15a..281e91c936 100644 --- a/feature/app-settings/src/main/res/values-gl/strings.xml +++ b/feature/app-settings/src/main/res/values-gl/strings.xml @@ -80,37 +80,37 @@ Máis información - + - Configura a aplicación de autenticación + Configura a aplicación de autenticación - Sigue estes pasos para configurar a aplicación de autenticación. + Sigue estes pasos para configurar a aplicación de autenticación. - Máis información + Máis información - Primeiro paso + Primeiro paso - Instala unha aplicación de autenticación de confianza no teu dispositivo. + Instala unha aplicación de autenticación de confianza no teu dispositivo. - Segundo paso + Segundo paso - Abre a aplicación de autenticación premendo o botón de máis abaixo para engadir a túa conta de Signal. + Abre a aplicación de autenticación premendo o botón de máis abaixo para engadir a túa conta de Signal. - Abrir + Abrir - Ou podes copiar esta clave para configurala de forma manual. + Ou podes copiar esta clave para configurala de forma manual. - Copiar + Copiar - Copiado no portapapeis + Copiado no portapapeis - Non se atopou unha aplicación de autenticación + Non se atopou unha aplicación de autenticación - Terceiro paso + Terceiro paso - Copia o código que se xerou e volve aquí para continuar. + Copia o código que se xerou e volve aquí para continuar. - Continuar + Continuar @@ -140,60 +140,60 @@ Eliminar - + - Escribe o teu código + Escribe o teu código - Introduce o código de 6 díxitos da aplicación de autenticación. + Introduce o código de 6 díxitos da aplicación de autenticación. - Código + Código - Feito - + Feito + - Aplicación de autenticación + Aplicación de autenticación - Configura unha aplicación de autenticación para xerar códigos de verificación dun só uso + Configura unha aplicación de autenticación para xerar códigos de verificación dun só uso - Máis información + Máis información - Engadir aplicación de autenticación + Engadir aplicación de autenticación - Aplicacións de autenticación + Aplicacións de autenticación - Non se atoparon aplicacións de autenticación + Non se atoparon aplicacións de autenticación - Engadido «%1$s» + Engadido «%1$s» - Abrir opcións da aplicación de autenticación + Abrir opcións da aplicación de autenticación - Renomear + Renomear - Eliminar + Eliminar - Eliminar aplicación de autenticación? + Eliminar aplicación de autenticación? - Para eliminar este método de autenticación necesitas o código de 6 díxitos da aplicación de autenticación. + Para eliminar este método de autenticación necesitas o código de 6 díxitos da aplicación de autenticación. - Non se pode engadir a aplicación de autenticación + Non se pode engadir a aplicación de autenticación - Non podes engadir máis de %1$d aplicacións de autenticación. Tes que eliminar antes unha. + Non podes engadir máis de %1$d aplicacións de autenticación. Tes que eliminar antes unha. - Aplicación de autenticación eliminada + Aplicación de autenticación eliminada - + - Escolle un nome + Escolle un nome - Ponlle un nome único a esta aplicación de autenticación. + Ponlle un nome único a esta aplicación de autenticación. - Escolle un nome único para esta aplicación de autenticación que che axude a identificala máis adiante. + Escolle un nome único para esta aplicación de autenticación que che axude a identificala máis adiante. - Nome + Nome - Seguinte + Seguinte - Configuración da aplicación de autenticación + Configuración da aplicación de autenticación - Cambiouse o nome da aplicación de autenticación + Cambiouse o nome da aplicación de autenticación diff --git a/feature/app-settings/src/main/res/values-gu/strings.xml b/feature/app-settings/src/main/res/values-gu/strings.xml index d72e733d69..1026575121 100644 --- a/feature/app-settings/src/main/res/values-gu/strings.xml +++ b/feature/app-settings/src/main/res/values-gu/strings.xml @@ -80,37 +80,37 @@ વધુ જાણો - + - તમારી પ્રમાણીકરણ ઍપ સેટ કરો + તમારી પ્રમાણીકરણ ઍપ સેટ કરો - તમારી પ્રમાણીકરણ ઍપ સેટ કરવા આ પગલાં અનુસરો. + તમારી પ્રમાણીકરણ ઍપ સેટ કરવા આ પગલાં અનુસરો. - વધુ જાણો + વધુ જાણો - પગલું 1 + પગલું 1 - તમારા ડિવાઇસ પર વિશ્વસનીય પ્રમાણીકરણ ઍપ ઇન્સ્ટોલ કરો. + તમારા ડિવાઇસ પર વિશ્વસનીય પ્રમાણીકરણ ઍપ ઇન્સ્ટોલ કરો. - પગલું 2 + પગલું 2 - તમારું Signal એકાઉન્ટ ઉમેરવા માટે નીચેના બટન પર ટેપ કરીને તમારી પ્રમાણીકરણ ઍપ ખોલો. + તમારું Signal એકાઉન્ટ ઉમેરવા માટે નીચેના બટન પર ટેપ કરીને તમારી પ્રમાણીકરણ ઍપ ખોલો. - ખોલો + ખોલો - અથવા તમે જાતે સેટ અપ કરવા આ કી કૉપિ કરી શકો છો. + અથવા તમે જાતે સેટ અપ કરવા આ કી કૉપિ કરી શકો છો. - કૉપિ + કૉપિ - ક્લિપબોર્ડ પર કૉપિ કર્યું + ક્લિપબોર્ડ પર કૉપિ કર્યું - કોઈ પ્રમાણીકરણ ઍપ મળી નહીં + કોઈ પ્રમાણીકરણ ઍપ મળી નહીં - પગલું 3 + પગલું 3 - જનરેટ થયેલો કોડ કૉપિ કરો અને ચાલુ રાખવા અહીં પાછા આવો. + જનરેટ થયેલો કોડ કૉપિ કરો અને ચાલુ રાખવા અહીં પાછા આવો. - ચાલુ રાખો + ચાલુ રાખો @@ -140,60 +140,60 @@ દૂર કરો - + - તમારો કોડ દાખલ કરો + તમારો કોડ દાખલ કરો - તમારી પ્રમાણીકરણ ઍપમાંથી 6-અંકનો કોડ દાખલ કરો. + તમારી પ્રમાણીકરણ ઍપમાંથી 6-અંકનો કોડ દાખલ કરો. - કોડ + કોડ - થઈ ગયું - + થઈ ગયું + - પ્રમાણીકરણ ઍપ + પ્રમાણીકરણ ઍપ - વન-ટાઇમ વેરિફિકેશન કોડ જનરેટ કરવા પ્રમાણીકરણ ઍપને સેટઅપ કરો + વન-ટાઇમ વેરિફિકેશન કોડ જનરેટ કરવા પ્રમાણીકરણ ઍપને સેટઅપ કરો - વધુ જાણો + વધુ જાણો - પ્રમાણીકરણ ઍપ ઉમેરો + પ્રમાણીકરણ ઍપ ઉમેરો - પ્રમાણીકરણ ઍપ + પ્રમાણીકરણ ઍપ - કોઈ પ્રમાણીકરણ ઍપ નથી + કોઈ પ્રમાણીકરણ ઍપ નથી - \"%1$s\" ઉમેર્યા + \"%1$s\" ઉમેર્યા - પ્રમાણીકરણ ઍપ વિકલ્પો ખોલો + પ્રમાણીકરણ ઍપ વિકલ્પો ખોલો - નામ બદલો + નામ બદલો - દૂર કરો + દૂર કરો - પ્રમાણીકરણ ઍપ દૂર કરવી છે? + પ્રમાણીકરણ ઍપ દૂર કરવી છે? - આ પ્રમાણીકરણ પદ્ધતિને દૂર કરવા માટે તમારી પ્રમાણીકરણ ઍપમાંથી 6-અંકનો કોડ જરૂરી છે. + આ પ્રમાણીકરણ પદ્ધતિને દૂર કરવા માટે તમારી પ્રમાણીકરણ ઍપમાંથી 6-અંકનો કોડ જરૂરી છે. - પ્રમાણીકરણ ઍપ ઉમેરી શકતા નથી + પ્રમાણીકરણ ઍપ ઉમેરી શકતા નથી - તમે %1$dથી વધુ પ્રમાણીકરણ ઍપ ઉમેરી શકતા નથી. પહેલા એકને દૂર કરી જુઓ. + તમે %1$dથી વધુ પ્રમાણીકરણ ઍપ ઉમેરી શકતા નથી. પહેલા એકને દૂર કરી જુઓ. - પ્રમાણીકરણ ઍપ દૂર કરી + પ્રમાણીકરણ ઍપ દૂર કરી - + - નામ પસંદ કરો + નામ પસંદ કરો - આ પ્રમાણીકરણ ઍપ માટે એક વિશિષ્ટ નામ પસંદ કરો. + આ પ્રમાણીકરણ ઍપ માટે એક વિશિષ્ટ નામ પસંદ કરો. - આ પ્રમાણીકરણ ઍપ માટે એક વિશિષ્ટ નામ પસંદ કરો જેથી તમે તેને પછીથી ઓળખી શકો. + આ પ્રમાણીકરણ ઍપ માટે એક વિશિષ્ટ નામ પસંદ કરો જેથી તમે તેને પછીથી ઓળખી શકો. - નામ + નામ - આગળ + આગળ - પ્રમાણીકરણ ઍપ સેટ અપ + પ્રમાણીકરણ ઍપ સેટ અપ - પ્રમાણીકરણ ઍપનું નામ બદલ્યું + પ્રમાણીકરણ ઍપનું નામ બદલ્યું diff --git a/feature/app-settings/src/main/res/values-hi/strings.xml b/feature/app-settings/src/main/res/values-hi/strings.xml index 231a5b2366..cae6703e0d 100644 --- a/feature/app-settings/src/main/res/values-hi/strings.xml +++ b/feature/app-settings/src/main/res/values-hi/strings.xml @@ -80,37 +80,37 @@ और जानें - + - अपना ऑथेंटिकेटर ऐप सेटअप करें + अपना ऑथेंटिकेटर ऐप सेटअप करें - अपना ऑथेंटिकेटर ऐप सेटअप करने के लिए इन चरणों का पालन करें। + अपना ऑथेंटिकेटर ऐप सेटअप करने के लिए इन चरणों का पालन करें। - और जानें + और जानें - चरण 1 + चरण 1 - अपने डिवाइस में कोई भरोसेमंद ऑथेंटिकेटर ऐप इंस्टॉल करें। + अपने डिवाइस में कोई भरोसेमंद ऑथेंटिकेटर ऐप इंस्टॉल करें। - चरण 2 + चरण 2 - अपना Signal अकाउंट जोड़ने के लिए नीचे दिए गए बटन पर टैप करके अपना ऑथेंटिकेटर ऐप खोलें। + अपना Signal अकाउंट जोड़ने के लिए नीचे दिए गए बटन पर टैप करके अपना ऑथेंटिकेटर ऐप खोलें। - खोलें + खोलें - या फिर इसे मैन्युअल तरीके से सेटअप करने के लिए यह \'की\' कॉपी करें। + या फिर इसे मैन्युअल तरीके से सेटअप करने के लिए यह \'की\' कॉपी करें। - कॉपी करें + कॉपी करें - क्लिपबोर्ड पर कॉपी किया गया + क्लिपबोर्ड पर कॉपी किया गया - कोई ऑथेंटिकेटर ऐप नहीं मिला + कोई ऑथेंटिकेटर ऐप नहीं मिला - चरण 3 + चरण 3 - जनरेट हुआ कोड कॉपी करें और यहां वापस आकर जारी रखें। + जनरेट हुआ कोड कॉपी करें और यहां वापस आकर जारी रखें। - जारी रखें + जारी रखें @@ -140,60 +140,60 @@ हटाएं - + - अपना कोड डालें + अपना कोड डालें - अपने ऑथेंटिकेटर ऐप में दिख रहा 6-अंकों वाला कोड डालें। + अपने ऑथेंटिकेटर ऐप में दिख रहा 6-अंकों वाला कोड डालें। - कोड + कोड - हो गया - + हो गया + - ऑथेंटिकेटर ऐप + ऑथेंटिकेटर ऐप - एक बार इस्तेमाल होने वाला वेरिफ़िकेशन कोड जनरेट करने के लिए, ऑथेंटिकेटर ऐप सेटअप करें + एक बार इस्तेमाल होने वाला वेरिफ़िकेशन कोड जनरेट करने के लिए, ऑथेंटिकेटर ऐप सेटअप करें - और जानें + और जानें - ऑथेंटिकेटर ऐप जोड़ें + ऑथेंटिकेटर ऐप जोड़ें - ऑथेंटिकेटर ऐप + ऑथेंटिकेटर ऐप - कोई ऑथेंटिकेटर ऐप मौजूद नहीं है + कोई ऑथेंटिकेटर ऐप मौजूद नहीं है - \'%1$s\' को जोड़ा गया + \'%1$s\' को जोड़ा गया - ऑथेंटिकेटर ऐप के विकल्प खोलें + ऑथेंटिकेटर ऐप के विकल्प खोलें - नाम बदलें + नाम बदलें - हटाएं + हटाएं - ऑथेंटिकेटर ऐप हटाना है? + ऑथेंटिकेटर ऐप हटाना है? - ऑथेंटिकेशन के इस तरीके को हटाने के लिए अपने ऑथेंटिकेटर ऐप में दिख रहा 6-अंकों वाला कोड डालें। + ऑथेंटिकेशन के इस तरीके को हटाने के लिए अपने ऑथेंटिकेटर ऐप में दिख रहा 6-अंकों वाला कोड डालें। - ऑथेंटिकेटर ऐप जोड़ना संभव नही है + ऑथेंटिकेटर ऐप जोड़ना संभव नही है - %1$d से ज़्यादा ऑथेंटिकेटर ऐप नहीं जोड़े जा सकते। पहले एक को हटाकर देखें। + %1$d से ज़्यादा ऑथेंटिकेटर ऐप नहीं जोड़े जा सकते। पहले एक को हटाकर देखें। - ऑथेंटिकेटर ऐप हटाया गया + ऑथेंटिकेटर ऐप हटाया गया - + - कोई नाम चुनें + कोई नाम चुनें - इस ऑथेंटिकेटर ऐप के लिए एक यूनीक नाम चुनें। + इस ऑथेंटिकेटर ऐप के लिए एक यूनीक नाम चुनें। - इस ऑथेंटिकेटर ऐप के लिए एक यूनीक नाम चुनें, ताकि आप बाद में इसे पहचान सकें। + इस ऑथेंटिकेटर ऐप के लिए एक यूनीक नाम चुनें, ताकि आप बाद में इसे पहचान सकें। - नाम + नाम - अगला + अगला - ऑथेंटिकेटर ऐप सेटअप करें + ऑथेंटिकेटर ऐप सेटअप करें - ऑथेंटिकेटर ऐप का नाम बदला गया + ऑथेंटिकेटर ऐप का नाम बदला गया diff --git a/feature/app-settings/src/main/res/values-hr/strings.xml b/feature/app-settings/src/main/res/values-hr/strings.xml index 6b752e2245..9a62fb5568 100644 --- a/feature/app-settings/src/main/res/values-hr/strings.xml +++ b/feature/app-settings/src/main/res/values-hr/strings.xml @@ -86,37 +86,37 @@ Saznajte više - + - Set up your authenticator app + Set up your authenticator app - Slijedite ove korake za postavljanje aplikacije za provjeru autentičnosti. + Slijedite ove korake za postavljanje aplikacije za provjeru autentičnosti. - Saznajte više + Saznajte više - 1. korak + 1. korak - Instalirajte pouzdanu aplikaciju za provjeru autentičnosti na svom uređaju. + Instalirajte pouzdanu aplikaciju za provjeru autentičnosti na svom uređaju. - 2. korak + 2. korak - Otvorite aplikaciju za provjeru autentičnosti dodirom na gumb ispod za dodavanje svog Signal računa. + Otvorite aplikaciju za provjeru autentičnosti dodirom na gumb ispod za dodavanje svog Signal računa. - Otvori + Otvori - Ili kopirajte ovaj ključ za ručno postavljanje. + Ili kopirajte ovaj ključ za ručno postavljanje. - Kopiraj + Kopiraj - Kopirano u međuspremnik + Kopirano u međuspremnik - Nije pronađena aplikacija za provjeru autentičnosti + Nije pronađena aplikacija za provjeru autentičnosti - 3. korak + 3. korak - Kopirajte generirani kôd i vratite se ovdje za nastavak. + Kopirajte generirani kôd i vratite se ovdje za nastavak. - Nastavi + Nastavi @@ -146,60 +146,60 @@ Ukloni - + - Unesite svoj kôd + Unesite svoj kôd - Unesite šesteroznamenkasti kôd iz aplikacije za provjeru autentičnosti. + Unesite šesteroznamenkasti kôd iz aplikacije za provjeru autentičnosti. - Kôd + Kôd - Gotovo - + Gotovo + - Aplikacija za provjeru autentičnosti + Aplikacija za provjeru autentičnosti - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Saznajte više + Saznajte više - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Dodan/a je \"%1$s\" + Dodan/a je \"%1$s\" - Open authenticator app options + Open authenticator app options - Preimenuj + Preimenuj - Ukloni + Ukloni - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Odaberite naziv + Odaberite naziv - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Ime + Ime - Sljedeće + Sljedeće - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-hu/strings.xml b/feature/app-settings/src/main/res/values-hu/strings.xml index e77d2a2c69..9868e94905 100644 --- a/feature/app-settings/src/main/res/values-hu/strings.xml +++ b/feature/app-settings/src/main/res/values-hu/strings.xml @@ -80,37 +80,37 @@ Tudj meg többet - + - Állítsd be a hitelesítő alkalmazásodat + Állítsd be a hitelesítő alkalmazásodat - A hitelesítő alkalmazás beállításához kövesd az alábbi lépéseket. + A hitelesítő alkalmazás beállításához kövesd az alábbi lépéseket. - Tudj meg többet + Tudj meg többet - 1. lépés + 1. lépés - Telepíts egy megbízható hitelesítő alkalmazást az eszközödre. + Telepíts egy megbízható hitelesítő alkalmazást az eszközödre. - 2. lépés + 2. lépés - A Signal fiókod hozzáadásához nyisd meg a hitelesítő alkalmazást az alábbi gombra koppintva. + A Signal fiókod hozzáadásához nyisd meg a hitelesítő alkalmazást az alábbi gombra koppintva. - Megnyitás + Megnyitás - Vagy másold ki ezt a kulcsot a manuális beállításhoz. + Vagy másold ki ezt a kulcsot a manuális beállításhoz. - Másolás + Másolás - Vágólapra másolva + Vágólapra másolva - Nem található hitelesítő alkalmazás + Nem található hitelesítő alkalmazás - 3. lépés + 3. lépés - Másold ki a generált kódot, és térj vissza ide a folytatáshoz. + Másold ki a generált kódot, és térj vissza ide a folytatáshoz. - Folytatás + Folytatás @@ -140,60 +140,60 @@ Eltávolítás - + - Add meg a kódod + Add meg a kódod - Add meg a 6 jegyű kódot a hitelesítő alkalmazásból. + Add meg a 6 jegyű kódot a hitelesítő alkalmazásból. - Kód + Kód - Kész - + Kész + - Hitelesítő alkalmazás + Hitelesítő alkalmazás - Egyszer használatos ellenőrző kódok generálásához állíts be egy hitelesítő alkalmazást + Egyszer használatos ellenőrző kódok generálásához állíts be egy hitelesítő alkalmazást - Tudj meg többet + Tudj meg többet - Hitelesítő alkalmazás hozzáadása + Hitelesítő alkalmazás hozzáadása - Hitelesítő alkalmazások + Hitelesítő alkalmazások - Nincs hitelesítő alkalmazás + Nincs hitelesítő alkalmazás - \"%1$s\" hozzáadva + \"%1$s\" hozzáadva - Hitelesítő alkalmazás opciók megnyitása + Hitelesítő alkalmazás opciók megnyitása - Átnevezés + Átnevezés - Eltávolítás + Eltávolítás - Hitelesítő alkalmazás eltávolítása? + Hitelesítő alkalmazás eltávolítása? - A hitelesítési módszer eltávolításához a hitelesítő alkalmazás 6 számjegyű kódjára van szükség. + A hitelesítési módszer eltávolításához a hitelesítő alkalmazás 6 számjegyű kódjára van szükség. - Nem lehet hitelesítő alkalmazást hozzáadni + Nem lehet hitelesítő alkalmazást hozzáadni - Legfeljebb %1$d hitelesítő alkalmazást adhatsz hozzá. Először próbálj egyet eltávolítani. + Legfeljebb %1$d hitelesítő alkalmazást adhatsz hozzá. Először próbálj egyet eltávolítani. - Hitelesítő alkalmazás eltávolítva + Hitelesítő alkalmazás eltávolítva - + - Válassz egy nevet + Válassz egy nevet - Válassz egy egyedi nevet ennek a hitelesítő alkalmazásnak. + Válassz egy egyedi nevet ennek a hitelesítő alkalmazásnak. - Válassz egy egyedi nevet ennek a hitelesítő alkalmazásnak, hogy később könnyebben azonosíthasd. + Válassz egy egyedi nevet ennek a hitelesítő alkalmazásnak, hogy később könnyebben azonosíthasd. - Név + Név - Tovább + Tovább - Hitelesítő alkalmazás beállítása + Hitelesítő alkalmazás beállítása - Hitelesítő alkalmazás átnevezve + Hitelesítő alkalmazás átnevezve diff --git a/feature/app-settings/src/main/res/values-in/strings.xml b/feature/app-settings/src/main/res/values-in/strings.xml index e8999efddf..4be8fb5b30 100644 --- a/feature/app-settings/src/main/res/values-in/strings.xml +++ b/feature/app-settings/src/main/res/values-in/strings.xml @@ -77,37 +77,37 @@ Pelajari selengkapnya - + - Siapkan aplikasi autentikator + Siapkan aplikasi autentikator - Ikuti langkah berikut untuk menyiapkan aplikasi autentikator. + Ikuti langkah berikut untuk menyiapkan aplikasi autentikator. - Pelajari selengkapnya + Pelajari selengkapnya - Langkah 1 + Langkah 1 - Instal aplikasi autentikator tepercaya di perangkat Anda. + Instal aplikasi autentikator tepercaya di perangkat Anda. - Langkah 2 + Langkah 2 - Buka aplikasi autentikator dengan mengetuk tombol di bawah ini untuk menambahkan akun Signal Anda. + Buka aplikasi autentikator dengan mengetuk tombol di bawah ini untuk menambahkan akun Signal Anda. - Buka + Buka - Anda juga dapat menyalin kunci ini untuk menyiapkannya secara manual. + Anda juga dapat menyalin kunci ini untuk menyiapkannya secara manual. - Salin + Salin - Disalin ke papan klip + Disalin ke papan klip - Tidak ditemukan aplikasi autentikator + Tidak ditemukan aplikasi autentikator - Langkah 3 + Langkah 3 - Salin kode yang tersedia, lalu kembali ke sini untuk melanjutkan. + Salin kode yang tersedia, lalu kembali ke sini untuk melanjutkan. - Lanjut + Lanjut @@ -137,60 +137,60 @@ Hapus - + - Masukkan kode Anda + Masukkan kode Anda - Masukkan kode 6 digit dari aplikasi autentikator Anda. + Masukkan kode 6 digit dari aplikasi autentikator Anda. - Kode + Kode - Selesai - + Selesai + - Aplikasi autentikator + Aplikasi autentikator - Siapkan aplikasi autentikator untuk mendapatkan kode verifikasi sekali pakai + Siapkan aplikasi autentikator untuk mendapatkan kode verifikasi sekali pakai - Pelajari selengkapnya + Pelajari selengkapnya - Tambahkan aplikasi autentikator + Tambahkan aplikasi autentikator - Aplikasi autentikator + Aplikasi autentikator - Tidak ada aplikasi autentikator + Tidak ada aplikasi autentikator - Ditambahkan %1$s + Ditambahkan %1$s - Buka opsi aplikasi autentikator + Buka opsi aplikasi autentikator - Ubah nama + Ubah nama - Hapus + Hapus - Hapus aplikasi autentikator? + Hapus aplikasi autentikator? - Untuk menghapus metode autentikasi ini, diperlukan kode 6 digit dari aplikasi autentikator Anda. + Untuk menghapus metode autentikasi ini, diperlukan kode 6 digit dari aplikasi autentikator Anda. - Tidak bisa menambahkan aplikasi autentikator + Tidak bisa menambahkan aplikasi autentikator - Anda tidak bisa menambahkan lebih dari %1$d aplikasi autentikator. Coba hapus salah satu terlebih dahulu. + Anda tidak bisa menambahkan lebih dari %1$d aplikasi autentikator. Coba hapus salah satu terlebih dahulu. - Aplikasi autentikator dihapus + Aplikasi autentikator dihapus - + - Pilih nama + Pilih nama - Pilih nama unik untuk aplikasi autentikator ini. + Pilih nama unik untuk aplikasi autentikator ini. - Pilih nama unik untuk aplikasi autentikator ini agar mudah dikenali nanti. + Pilih nama unik untuk aplikasi autentikator ini agar mudah dikenali nanti. - Nama + Nama - Berikutnya + Berikutnya - Aplikasi autentikator telah disiapkan + Aplikasi autentikator telah disiapkan - Nama aplikasi autentikator diganti + Nama aplikasi autentikator diganti diff --git a/feature/app-settings/src/main/res/values-it/strings.xml b/feature/app-settings/src/main/res/values-it/strings.xml index 5093912bb2..e33f65ed07 100644 --- a/feature/app-settings/src/main/res/values-it/strings.xml +++ b/feature/app-settings/src/main/res/values-it/strings.xml @@ -80,37 +80,37 @@ Scopri di più - + - Set up your authenticator app + Set up your authenticator app - Segui questi passaggi per impostare l\'app di autenticazione. + Segui questi passaggi per impostare l\'app di autenticazione. - Scopri di più + Scopri di più - Passaggio 1 + Passaggio 1 - Installa un\'app sicura per l\'autenticazione sul tuo dispositivo. + Installa un\'app sicura per l\'autenticazione sul tuo dispositivo. - Passaggio 2 + Passaggio 2 - Per aggiungere l\'account Signal alla tua app di autenticazione, tocca il pulsante in basso. + Per aggiungere l\'account Signal alla tua app di autenticazione, tocca il pulsante in basso. - Apri + Apri - Oppure puoi copiare questa chiave per configurarla manualmente. + Oppure puoi copiare questa chiave per configurarla manualmente. - Copia + Copia - Copiato negli appunti + Copiato negli appunti - Nessuna app di autenticazione trovata + Nessuna app di autenticazione trovata - Passaggio 3 + Passaggio 3 - Copia il codice che viene generato dall\'app e torna qui per continuare. + Copia il codice che viene generato dall\'app e torna qui per continuare. - Continua + Continua @@ -140,60 +140,60 @@ Rimuovi - + - Inserisci il tuo codice + Inserisci il tuo codice - Inserisci il codice a 6 cifre dalla tua app di autenticazione. + Inserisci il codice a 6 cifre dalla tua app di autenticazione. - Codice + Codice - Fatto - + Fatto + - App per l\'autenticazione + App per l\'autenticazione - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Scopri di più + Scopri di più - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Aggiunto \"%1$s\" + Aggiunto \"%1$s\" - Open authenticator app options + Open authenticator app options - Rinomina + Rinomina - Rimuovi + Rimuovi - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Scegli un nome + Scegli un nome - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Nome + Nome - Avanti + Avanti - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-iw/strings.xml b/feature/app-settings/src/main/res/values-iw/strings.xml index 1fe59317f7..28b283fe01 100644 --- a/feature/app-settings/src/main/res/values-iw/strings.xml +++ b/feature/app-settings/src/main/res/values-iw/strings.xml @@ -86,37 +86,37 @@ למידע נוסף - + - הגדרת אפליקציית האימות שלך + הגדרת אפליקציית האימות שלך - יש לבצע את השלבים הבאים כדי להגדיר אפליקציית אימות דו-שלבי: + יש לבצע את השלבים הבאים כדי להגדיר אפליקציית אימות דו-שלבי: - למידע נוסף + למידע נוסף - שלב 1 + שלב 1 - להתקין אפליקציה אמינה של אימות דו-שלבי במכשיר שלך. + להתקין אפליקציה אמינה של אימות דו-שלבי במכשיר שלך. - שלב 2 + שלב 2 - לפתוח את אפליקציית האימות באמצעות לחיצה על הכפתור שלמטה כדי להוסיף את חשבון Signal שלך. + לפתוח את אפליקציית האימות באמצעות לחיצה על הכפתור שלמטה כדי להוסיף את חשבון Signal שלך. - פתיחה + פתיחה - אפשר גם להעתיק את הקישור הזה כדי להגדיר באופן ידני. + אפשר גם להעתיק את הקישור הזה כדי להגדיר באופן ידני. - העתק + העתק - הועתק ללוח + הועתק ללוח - לא נמצאה אפליקציית אימות + לא נמצאה אפליקציית אימות - שלב 3 + שלב 3 - להעתיק את הקוד שנוצר ולחזור לכאן כדי להמשיך. + להעתיק את הקוד שנוצר ולחזור לכאן כדי להמשיך. - המשך + המשך @@ -146,60 +146,60 @@ הסרה - + - הזנת הקוד שלך + הזנת הקוד שלך - יש להזין קוד בן 6 ספרות מאפליקציית האימות שלך. + יש להזין קוד בן 6 ספרות מאפליקציית האימות שלך. - קוד + קוד - סיום - + סיום + - אפליקציית אימות דו-שלבי + אפליקציית אימות דו-שלבי - להגדיר אפליקציית אימות ליצירת קודי אימות חד–פעמיים + להגדיר אפליקציית אימות ליצירת קודי אימות חד–פעמיים - למידע נוסף + למידע נוסף - הוספת אפליקציית אימות + הוספת אפליקציית אימות - אפליקציות אימות דו–שלבי + אפליקציות אימות דו–שלבי - אין אפליקציות אימות + אין אפליקציות אימות - \"%1$s\" התווסף/ה + \"%1$s\" התווסף/ה - פתיחת אפשרויות אפליקציית אימות + פתיחת אפשרויות אפליקציית אימות - שינוי שם + שינוי שם - הסרה + הסרה - להסיר אפליקציית אימות? + להסיר אפליקציית אימות? - כדי להסיר את שיטת האימות הזו, נדרש קוד בן 6 ספרות מאפליקציית האימות שלך. + כדי להסיר את שיטת האימות הזו, נדרש קוד בן 6 ספרות מאפליקציית האימות שלך. - לא ניתן להוסיף אפליקציית אימות + לא ניתן להוסיף אפליקציית אימות - לא ניתן להוסיף יותר מ–%1$d אפליקציות אימות. יש לנסות להסיר אחת קודם. + לא ניתן להוסיף יותר מ–%1$d אפליקציות אימות. יש לנסות להסיר אחת קודם. - אפליקציית אימות הוסרה + אפליקציית אימות הוסרה - + - בחר שם + בחר שם - בחירת שם ייחודי לאפליקציית האימות הזו. + בחירת שם ייחודי לאפליקציית האימות הזו. - בחירת שם ייחודי לאפליקציית האימות הזו שיעזור לך לזהות אותה בהמשך. + בחירת שם ייחודי לאפליקציית האימות הזו שיעזור לך לזהות אותה בהמשך. - שם + שם - הבא + הבא - הגדרות אפליקציית אימות + הגדרות אפליקציית אימות - שונה שם אפליקציית האימות + שונה שם אפליקציית האימות diff --git a/feature/app-settings/src/main/res/values-ja/strings.xml b/feature/app-settings/src/main/res/values-ja/strings.xml index e0ce84813f..7b715a9b89 100644 --- a/feature/app-settings/src/main/res/values-ja/strings.xml +++ b/feature/app-settings/src/main/res/values-ja/strings.xml @@ -77,37 +77,37 @@ 詳しく見る - + - 認証アプリを設定しましょう + 認証アプリを設定しましょう - 以下の手順に従って認証アプリを設定してください。 + 以下の手順に従って認証アプリを設定してください。 - 詳しく見る + 詳しく見る - ステップ1 + ステップ1 - ご利用の端末に、信頼できる認証アプリをインストールしてください。 + ご利用の端末に、信頼できる認証アプリをインストールしてください。 - ステップ2 + ステップ2 - 下のボタンをタップして認証アプリを開き、Signalアカウントを追加してください。 + 下のボタンをタップして認証アプリを開き、Signalアカウントを追加してください。 - 開く + 開く - または、以下のキーをコピーして手動でセットアップすることもできます。 + または、以下のキーをコピーして手動でセットアップすることもできます。 - コピー + コピー - クリップボードにコピーしました + クリップボードにコピーしました - 認証アプリが見つかりませんでした + 認証アプリが見つかりませんでした - ステップ3 + ステップ3 - 生成されたコードをコピーして、ここに戻り続行してください。 + 生成されたコードをコピーして、ここに戻り続行してください。 - 続行 + 続行 @@ -137,60 +137,60 @@ 削除 - + - コードを入力してください + コードを入力してください - 認証アプリに表示された6桁のコードを入力してください。 + 認証アプリに表示された6桁のコードを入力してください。 - コード + コード - 完了 - + 完了 + - 認証アプリ + 認証アプリ - ワンタイム確認コードを生成するために認証アプリを設定してください + ワンタイム確認コードを生成するために認証アプリを設定してください - 詳しく見る + 詳しく見る - 認証アプリを追加 + 認証アプリを追加 - 認証アプリ + 認証アプリ - 認証アプリが設定されていません + 認証アプリが設定されていません - 「%1$s」を追加しました + 「%1$s」を追加しました - 認証アプリのオプションを開く + 認証アプリのオプションを開く - 名前を変更する + 名前を変更する - 削除 + 削除 - 認証アプリを削除しますか? + 認証アプリを削除しますか? - この認証方法を削除するには、認証アプリの6桁のコードが必要です。 + この認証方法を削除するには、認証アプリの6桁のコードが必要です。 - 認証アプリを追加できません + 認証アプリを追加できません - %1$dつを超える数の認証アプリは追加できません。まず1つを削除してください。 + %1$dつを超える数の認証アプリは追加できません。まず1つを削除してください。 - 認証アプリが削除されました + 認証アプリが削除されました - + - 名前を選択してください + 名前を選択してください - この認証アプリには覚えやすい名称をつけてください。 + この認証アプリには覚えやすい名称をつけてください。 - この認証アプリには、後で分かりやすいように覚えやすい名称をつけてください。 + この認証アプリには、後で分かりやすいように覚えやすい名称をつけてください。 - 名前 + 名前 - 次へ + 次へ - 認証アプリを設定しました + 認証アプリを設定しました - 認証アプリの名前を変更しました + 認証アプリの名前を変更しました diff --git a/feature/app-settings/src/main/res/values-ka/strings.xml b/feature/app-settings/src/main/res/values-ka/strings.xml index c6c7f14c58..6f8bbfa112 100644 --- a/feature/app-settings/src/main/res/values-ka/strings.xml +++ b/feature/app-settings/src/main/res/values-ka/strings.xml @@ -80,37 +80,37 @@ გაიგე მეტი - + - Set up your authenticator app + Set up your authenticator app - შენი ავტორიზაციის აპის შესაქმნელად ამ ნაბიჯებს მიჰყევი. + შენი ავტორიზაციის აპის შესაქმნელად ამ ნაბიჯებს მიჰყევი. - გაიგე მეტი + გაიგე მეტი - 1-ელი ნაბიჯი + 1-ელი ნაბიჯი - გადმოიწერე ავტორიზაციის სანდო აპი შენს მოწყობილობაზე. + გადმოიწერე ავტორიზაციის სანდო აპი შენს მოწყობილობაზე. - მე-2 ნაბიჯი + მე-2 ნაბიჯი - გახსენი შენი ავტორიზაციის აპი ქვემოთ მოცემულ ღილაკზე დაჭერით, რათა Signal-ის ანგარიში დაამატო. + გახსენი შენი ავტორიზაციის აპი ქვემოთ მოცემულ ღილაკზე დაჭერით, რათა Signal-ის ანგარიში დაამატო. - გახსნა + გახსნა - ან, შეგიძლია ეს გასაღები დააკოპირო, რომ ის ხელით შექმნა. + ან, შეგიძლია ეს გასაღები დააკოპირო, რომ ის ხელით შექმნა. - დაკოპირება + დაკოპირება - დაკოპირებულია ბუფერში + დაკოპირებულია ბუფერში - ავტორიზაციის აპი ვერ მოიძებნა + ავტორიზაციის აპი ვერ მოიძებნა - მე-3 ნაბიჯი + მე-3 ნაბიჯი - დააკოპირე გენერირებული კოდი და გასაგრძელებლად აქ დაბრუნდი. + დააკოპირე გენერირებული კოდი და გასაგრძელებლად აქ დაბრუნდი. - გაგრძელება + გაგრძელება @@ -140,60 +140,60 @@ წაშლა - + - შეიყვანე შენი კოდი + შეიყვანე შენი კოდი - შეიყვანე 6-ციფრიანი კოდი შენი ავტორიზაციის აპიდან. + შეიყვანე 6-ციფრიანი კოდი შენი ავტორიზაციის აპიდან. - კოდი + კოდი - შესრულებულია - + შესრულებულია + - ავტორიზაციის აპი + ავტორიზაციის აპი - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - გაიგე მეტი + გაიგე მეტი - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - \"%1$s\" დამატებულია + \"%1$s\" დამატებულია - Open authenticator app options + Open authenticator app options - Rename + Rename - წაშლა + წაშლა - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - სახელი + სახელი - შემდეგი + შემდეგი - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-kk/strings.xml b/feature/app-settings/src/main/res/values-kk/strings.xml index bbcccb4a79..497823afd3 100644 --- a/feature/app-settings/src/main/res/values-kk/strings.xml +++ b/feature/app-settings/src/main/res/values-kk/strings.xml @@ -80,37 +80,37 @@ Толық ақпарат - + - Set up your authenticator app + Set up your authenticator app - Аутентификатор қолданбасының параметрлерін реттеу үшін осы қадамдарды орындаңыз. + Аутентификатор қолданбасының параметрлерін реттеу үшін осы қадамдарды орындаңыз. - Толық ақпарат + Толық ақпарат - 1-қадам + 1-қадам - Құрылғыңызда сенімді аутентификатор қолданбасын орнатыңыз. + Құрылғыңызда сенімді аутентификатор қолданбасын орнатыңыз. - 2-қадам + 2-қадам - Төмендегі түймені басу арқылы аутентификатор қолданбасын ашып, Signal аккаунтын қосыңыз. + Төмендегі түймені басу арқылы аутентификатор қолданбасын ашып, Signal аккаунтын қосыңыз. - Ашу + Ашу - Болмаса осы кілтті көшіріп, қолмен реттеуге болады. + Болмаса осы кілтті көшіріп, қолмен реттеуге болады. - Көшірме жасау + Көшірме жасау - Буферге көшірілді + Буферге көшірілді - Аутентификатор қолданбасы табылмады + Аутентификатор қолданбасы табылмады - 3-қадам + 3-қадам - Жасалған кодты көшіріп, жалғастыру үшін осы жерге оралыңыз. + Жасалған кодты көшіріп, жалғастыру үшін осы жерге оралыңыз. - Жалғастыру + Жалғастыру @@ -140,60 +140,60 @@ Өшіру - + - Кодыңызды енгізіңіз + Кодыңызды енгізіңіз - Аутентификатор қолданбасындағы 6 цифрлық кодты енгізіңіз. + Аутентификатор қолданбасындағы 6 цифрлық кодты енгізіңіз. - Код + Код - Дайын - + Дайын + - Аутентификатор қолданбасы + Аутентификатор қолданбасы - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Толық ақпарат + Толық ақпарат - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - \"%1$s\" қосылды + \"%1$s\" қосылды - Open authenticator app options + Open authenticator app options - Rename + Rename - Өшіру + Өшіру - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Name + Name - Келесі + Келесі - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-km/strings.xml b/feature/app-settings/src/main/res/values-km/strings.xml index 1fd98f4c7b..9350d76a52 100644 --- a/feature/app-settings/src/main/res/values-km/strings.xml +++ b/feature/app-settings/src/main/res/values-km/strings.xml @@ -77,37 +77,37 @@ ស្វែងយល់បន្ថែម - + - រៀបចំកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នក + រៀបចំកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នក - សូមអនុវត្តតាមជំហានទាំងនេះ ដើម្បីរៀបចំកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នក។ + សូមអនុវត្តតាមជំហានទាំងនេះ ដើម្បីរៀបចំកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នក។ - ស្វែងយល់បន្ថែម + ស្វែងយល់បន្ថែម - ជំហានទី 1 + ជំហានទី 1 - ដំឡើងកម្មវិធីផ្ទៀងផ្ទាត់ដែលទុកចិត្តនៅលើឧបករណ៍របស់អ្នក។ + ដំឡើងកម្មវិធីផ្ទៀងផ្ទាត់ដែលទុកចិត្តនៅលើឧបករណ៍របស់អ្នក។ - ជំហានទី 2 + ជំហានទី 2 - បើកកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នកដោយចុចប៊ូតុងខាងក្រោមដើម្បីបញ្ចូលគណនី Signal របស់អ្នក។ + បើកកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នកដោយចុចប៊ូតុងខាងក្រោមដើម្បីបញ្ចូលគណនី Signal របស់អ្នក។ - បើក + បើក - ឬអ្នកអាចចម្លងសោនេះដើម្បីរៀបចំវាដោយផ្ទាល់ដៃ។ + ឬអ្នកអាចចម្លងសោនេះដើម្បីរៀបចំវាដោយផ្ទាល់ដៃ។ - ចម្លង + ចម្លង - បានចម្លងទៅឃ្លីបបត + បានចម្លងទៅឃ្លីបបត - រកមិនឃើញកម្មវិធីផ្ទៀងផ្ទាត់ទេ + រកមិនឃើញកម្មវិធីផ្ទៀងផ្ទាត់ទេ - ជំហានទី 3 + ជំហានទី 3 - ចម្លងកូដដែលបានបង្កើត ហើយត្រឡប់មកទីនេះវិញដើម្បីបន្ត។ + ចម្លងកូដដែលបានបង្កើត ហើយត្រឡប់មកទីនេះវិញដើម្បីបន្ត។ - បន្ត + បន្ត @@ -137,60 +137,60 @@ ដកចេញ - + - បញ្ចូលលេខកូដរបស់អ្នក + បញ្ចូលលេខកូដរបស់អ្នក - សូមបញ្ចូលលេខកូដ 6 ខ្ទង់ពីកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នក។ + សូមបញ្ចូលលេខកូដ 6 ខ្ទង់ពីកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នក។ - លេខកូដ + លេខកូដ - រួចរាល់ - + រួចរាល់ + - កម្មវិធីផ្ទៀងផ្ទាត់ + កម្មវិធីផ្ទៀងផ្ទាត់ - រៀបចំកម្មវិធីផ្ទៀងផ្ទាត់ដើម្បីបង្កើតលេខកូដផ្ទៀងផ្ទាត់ដែលប្រើតែម្តង + រៀបចំកម្មវិធីផ្ទៀងផ្ទាត់ដើម្បីបង្កើតលេខកូដផ្ទៀងផ្ទាត់ដែលប្រើតែម្តង - ស្វែងយល់បន្ថែម + ស្វែងយល់បន្ថែម - បញ្ចូលកម្មវិធីផ្ទៀងផ្ទាត់ + បញ្ចូលកម្មវិធីផ្ទៀងផ្ទាត់ - កម្មវិធីផ្ទៀងផ្ទាត់ + កម្មវិធីផ្ទៀងផ្ទាត់ - មិនមានកម្មវិធីផ្ទៀងផ្ទាត់ + មិនមានកម្មវិធីផ្ទៀងផ្ទាត់ - បានបញ្ចូល %1$s + បានបញ្ចូល %1$s - បើកជម្រើសកម្មវិធីផ្ទៀងផ្ទាត់ + បើកជម្រើសកម្មវិធីផ្ទៀងផ្ទាត់ - ប្តូរឈ្មោះ + ប្តូរឈ្មោះ - ដកចេញ + ដកចេញ - ដកកម្មវិធីផ្ទៀងផ្ទាត់ចេញ? + ដកកម្មវិធីផ្ទៀងផ្ទាត់ចេញ? - ដើម្បីដកវិធីផ្ទៀងផ្ទាត់នេះចេញ តម្រូវឱ្យមានលេខកូដ 6 ខ្ទង់ពីកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នក។ + ដើម្បីដកវិធីផ្ទៀងផ្ទាត់នេះចេញ តម្រូវឱ្យមានលេខកូដ 6 ខ្ទង់ពីកម្មវិធីផ្ទៀងផ្ទាត់របស់អ្នក។ - មិនអាចបញ្ចូលកម្មវិធីផ្ទៀងផ្ទាត់បានទេ + មិនអាចបញ្ចូលកម្មវិធីផ្ទៀងផ្ទាត់បានទេ - អ្នកមិនអាចបញ្ចូលកម្មវិធីផ្ទៀងផ្ទាត់លើសពី %1$d បានទេ។ សាកល្បងដកមួយចេញសិន។ + អ្នកមិនអាចបញ្ចូលកម្មវិធីផ្ទៀងផ្ទាត់លើសពី %1$d បានទេ។ សាកល្បងដកមួយចេញសិន។ - បានលុបកម្មវិធីផ្ទៀងផ្ទាត់ + បានលុបកម្មវិធីផ្ទៀងផ្ទាត់ - + - ជ្រើសរើសឈ្មោះ + ជ្រើសរើសឈ្មោះ - ជ្រើសរើសឈ្មោះខុសប្លែកពីគេសម្រាប់កម្មវិធីផ្ទៀងផ្ទាត់នេះ។ + ជ្រើសរើសឈ្មោះខុសប្លែកពីគេសម្រាប់កម្មវិធីផ្ទៀងផ្ទាត់នេះ។ - ជ្រើសរើសឈ្មោះខុសប្លែកពីគេឱ្យកម្មវិធីផ្ទៀងផ្ទាត់នេះ ដើម្បីជួយឱ្យអ្នកស្រួលរកវានៅពេលក្រោយ។ + ជ្រើសរើសឈ្មោះខុសប្លែកពីគេឱ្យកម្មវិធីផ្ទៀងផ្ទាត់នេះ ដើម្បីជួយឱ្យអ្នកស្រួលរកវានៅពេលក្រោយ។ - ឈ្មោះ + ឈ្មោះ - បន្ទាប់ + បន្ទាប់ - បានរៀបចំកម្មវិធីផ្ទៀងផ្ទាត់ + បានរៀបចំកម្មវិធីផ្ទៀងផ្ទាត់ - បានប្តូរឈ្មោះកម្មវិធីផ្ទៀងផ្ទាត់ + បានប្តូរឈ្មោះកម្មវិធីផ្ទៀងផ្ទាត់ diff --git a/feature/app-settings/src/main/res/values-kn/strings.xml b/feature/app-settings/src/main/res/values-kn/strings.xml index 3cfd553bd7..de2b0c0f6e 100644 --- a/feature/app-settings/src/main/res/values-kn/strings.xml +++ b/feature/app-settings/src/main/res/values-kn/strings.xml @@ -80,37 +80,37 @@ ಇನ್ನಷ್ಟು ತಿಳಿಯಿರಿ - + - ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ಸೆಟಪ್ ಮಾಡಿ + ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ಸೆಟಪ್ ಮಾಡಿ - ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ಸೆಟಪ್ ಮಾಡಲು ಈ ಹಂತಗಳನ್ನು ಅನುಸರಿಸಿ. + ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ಸೆಟಪ್ ಮಾಡಲು ಈ ಹಂತಗಳನ್ನು ಅನುಸರಿಸಿ. - ಇನ್ನಷ್ಟು ತಿಳಿಯಿರಿ + ಇನ್ನಷ್ಟು ತಿಳಿಯಿರಿ - ಹಂತ 1 + ಹಂತ 1 - ನಿಮ್ಮ ಸಾಧನದಲ್ಲಿ ವಿಶ್ವಾಸಾರ್ಹ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಿ. + ನಿಮ್ಮ ಸಾಧನದಲ್ಲಿ ವಿಶ್ವಾಸಾರ್ಹ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಿ. - ಹಂತ 2 + ಹಂತ 2 - ನಿಮ್ಮ Signal ಖಾತೆಯನ್ನು ಸೇರಿಸಲು ಕೆಳಗಿನ ಬಟನ್ ಅನ್ನು ಟ್ಯಾಪ್ ಮಾಡುವ ಮೂಲಕ ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ತೆರೆಯಿರಿ. + ನಿಮ್ಮ Signal ಖಾತೆಯನ್ನು ಸೇರಿಸಲು ಕೆಳಗಿನ ಬಟನ್ ಅನ್ನು ಟ್ಯಾಪ್ ಮಾಡುವ ಮೂಲಕ ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ತೆರೆಯಿರಿ. - ತೆರೆಯಿರಿ + ತೆರೆಯಿರಿ - ಅಥವಾ ಅದನ್ನು ಹಸ್ತಚಾಲಿತವಾಗಿ ಸೆಟ್ ಮಾಡಲು ನೀವು ಈ ಕೀ ಅನ್ನು ಕಾಪಿ ಮಾಡಬಹುದು. + ಅಥವಾ ಅದನ್ನು ಹಸ್ತಚಾಲಿತವಾಗಿ ಸೆಟ್ ಮಾಡಲು ನೀವು ಈ ಕೀ ಅನ್ನು ಕಾಪಿ ಮಾಡಬಹುದು. - ನಕಲಿಸು + ನಕಲಿಸು - ಕ್ಲಿಪ್‌‌ಬೋರ್ಡಿಗೆ ನಕಲು ಮಾಡಲಾಗಿದೆ + ಕ್ಲಿಪ್‌‌ಬೋರ್ಡಿಗೆ ನಕಲು ಮಾಡಲಾಗಿದೆ - ಯಾವುದೇ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಕಂಡುಬಂದಿಲ್ಲ + ಯಾವುದೇ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಕಂಡುಬಂದಿಲ್ಲ - ಹಂತ 3 + ಹಂತ 3 - ಜನರೇಟ್ ಆಗುವ ಕೋಡ್ ಅನ್ನು ಕಾಪಿ ಮಾಡಿ ಮತ್ತು ಮುಂದುವರಿಯಲು ಇಲ್ಲಿಗೆ ಹಿಂದಿರುಗಿ. + ಜನರೇಟ್ ಆಗುವ ಕೋಡ್ ಅನ್ನು ಕಾಪಿ ಮಾಡಿ ಮತ್ತು ಮುಂದುವರಿಯಲು ಇಲ್ಲಿಗೆ ಹಿಂದಿರುಗಿ. - ಮುಂದುವರಿಸಿ + ಮುಂದುವರಿಸಿ @@ -140,60 +140,60 @@ ತೆಗೆದುಹಾಕಿ - + - ನಿಮ್ಮ ಕೋಡ್ ನಮೂದಿಸಿ + ನಿಮ್ಮ ಕೋಡ್ ನಮೂದಿಸಿ - ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ನಿಂದ 6-ಅಂಕಿಯ ಕೋಡ್ ಅನ್ನು ನಮೂದಿಸಿ. + ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ನಿಂದ 6-ಅಂಕಿಯ ಕೋಡ್ ಅನ್ನು ನಮೂದಿಸಿ. - ಕೋಡ್ + ಕೋಡ್ - ಮುಗಿದಿದೆ - + ಮುಗಿದಿದೆ + - ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ + ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ - ಒಂದು ಬಾರಿಯ ದೃಢೀಕರಣ ಕೋಡ್‌ಗಳನ್ನು ಜನರೇಟ್ ಮಾಡಲು ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ಸೆಟಪ್ ಮಾಡಿ + ಒಂದು ಬಾರಿಯ ದೃಢೀಕರಣ ಕೋಡ್‌ಗಳನ್ನು ಜನರೇಟ್ ಮಾಡಲು ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಅನ್ನು ಸೆಟಪ್ ಮಾಡಿ - ಇನ್ನಷ್ಟು ತಿಳಿಯಿರಿ + ಇನ್ನಷ್ಟು ತಿಳಿಯಿರಿ - ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಸೇರಿಸಿ + ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಸೇರಿಸಿ - ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗಳು + ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗಳು - ಯಾವುದೇ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗಳಿಲ್ಲ + ಯಾವುದೇ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗಳಿಲ್ಲ - \"%1$s\" ಸೇರಿಸಲಾಗಿದೆ + \"%1$s\" ಸೇರಿಸಲಾಗಿದೆ - ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಆಯ್ಕೆಗಳನ್ನು ತೆರೆಯಿರಿ + ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಆಯ್ಕೆಗಳನ್ನು ತೆರೆಯಿರಿ - ಮರುಹೆಸರಿಸಿ + ಮರುಹೆಸರಿಸಿ - ತೆಗೆದುಹಾಕಿ + ತೆಗೆದುಹಾಕಿ - ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ತೆಗೆದುಹಾಕಬೇಕೇ? + ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ತೆಗೆದುಹಾಕಬೇಕೇ? - ಈ ಅಥೆಂಟಿಕೇಟರ್ ವಿಧಾನವನ್ನು ತೆಗೆದುಹಾಕಲು ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ನಿಂದ 6-ಅಂಕಿಯ ಕೋಡ್ ಅಗತ್ಯವಿದೆ. + ಈ ಅಥೆಂಟಿಕೇಟರ್ ವಿಧಾನವನ್ನು ತೆಗೆದುಹಾಕಲು ನಿಮ್ಮ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ನಿಂದ 6-ಅಂಕಿಯ ಕೋಡ್ ಅಗತ್ಯವಿದೆ. - ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಸೇರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ + ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಸೇರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ - ನೀವು %1$d ಕ್ಕಿಂತ ಹೆಚ್ಚು ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗಳನ್ನು ಸೇರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ. ಮೊದಲಿಗೆ, ಒಂದು ಆ್ಯಪ್ ಅನ್ನು ತೆಗೆದುಹಾಕಲು ಪ್ರಯತ್ನಿಸಿ. + ನೀವು %1$d ಕ್ಕಿಂತ ಹೆಚ್ಚು ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗಳನ್ನು ಸೇರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ. ಮೊದಲಿಗೆ, ಒಂದು ಆ್ಯಪ್ ಅನ್ನು ತೆಗೆದುಹಾಕಲು ಪ್ರಯತ್ನಿಸಿ. - ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ತೆಗೆದುಹಾಕಲಾಗಿದೆ + ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ತೆಗೆದುಹಾಕಲಾಗಿದೆ - + - ಹೆಸರು ಆರಿಸಿ + ಹೆಸರು ಆರಿಸಿ - ಈ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗೆ ಒಂದು ಅನನ್ಯವಾದ ಹೆಸರನ್ನು ಆರಿಸಿ. + ಈ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗೆ ಒಂದು ಅನನ್ಯವಾದ ಹೆಸರನ್ನು ಆರಿಸಿ. - ನಂತರದಲ್ಲಿ ಈ ಆ್ಯಪ್ ಅನ್ನು ಸುಲಭವಾಗಿ ಗುರುತಿಸಲು ಸಹಾಯವಾಗಲು ಈ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗೆ ಒಂದು ಅನನ್ಯವಾದ ಹೆಸರನ್ನು ಆರಿಸಿ. + ನಂತರದಲ್ಲಿ ಈ ಆ್ಯಪ್ ಅನ್ನು ಸುಲಭವಾಗಿ ಗುರುತಿಸಲು ಸಹಾಯವಾಗಲು ಈ ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್‌ಗೆ ಒಂದು ಅನನ್ಯವಾದ ಹೆಸರನ್ನು ಆರಿಸಿ. - ಹೆಸರು + ಹೆಸರು - ಮುಂದೆ + ಮುಂದೆ - ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಸೆಟಪ್ + ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಸೆಟಪ್ - ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಮರುಹೆಸರಿಸಲಾಗಿದೆ + ಅಥೆಂಟಿಕೇಟರ್ ಆ್ಯಪ್ ಮರುಹೆಸರಿಸಲಾಗಿದೆ diff --git a/feature/app-settings/src/main/res/values-ko/strings.xml b/feature/app-settings/src/main/res/values-ko/strings.xml index bddf493105..6a56a29565 100644 --- a/feature/app-settings/src/main/res/values-ko/strings.xml +++ b/feature/app-settings/src/main/res/values-ko/strings.xml @@ -77,37 +77,37 @@ 자세히 알아보기 - + - 인증 앱 설정하기 + 인증 앱 설정하기 - 인증 앱을 설정하려면 다음 단계를 따르세요. + 인증 앱을 설정하려면 다음 단계를 따르세요. - 자세히 알아보기 + 자세히 알아보기 - 1단계 + 1단계 - 기기에 신뢰할 수 있는 인증 앱을 설치하세요. + 기기에 신뢰할 수 있는 인증 앱을 설치하세요. - 2단계 + 2단계 - 아래 버튼을 탭하여 인증 앱을 열고 Signal 계정을 추가하세요. + 아래 버튼을 탭하여 인증 앱을 열고 Signal 계정을 추가하세요. - 열기 + 열기 - 또는 이 키를 복사하여 직접 설정할 수도 있습니다. + 또는 이 키를 복사하여 직접 설정할 수도 있습니다. - 복사 + 복사 - 클립보드로 복사했습니다. + 클립보드로 복사했습니다. - 인증 앱을 찾을 수 없음 + 인증 앱을 찾을 수 없음 - 3단계 + 3단계 - 생성된 코드를 복사한 후 여기에 돌아와 계속 진행하세요. + 생성된 코드를 복사한 후 여기에 돌아와 계속 진행하세요. - 계속 + 계속 @@ -137,60 +137,60 @@ 삭제 - + - 코드 입력 + 코드 입력 - 인증 앱에 표시된 6자리 코드를 입력하세요. + 인증 앱에 표시된 6자리 코드를 입력하세요. - 코드 + 코드 - 확인 - + 확인 + - 인증 앱 + 인증 앱 - 1회용 인증 코드를 생성하려면 인증 앱을 설정하세요. + 1회용 인증 코드를 생성하려면 인증 앱을 설정하세요. - 자세히 알아보기 + 자세히 알아보기 - 인증 앱 추가하기 + 인증 앱 추가하기 - 인증 앱 + 인증 앱 - 인증 앱 없음 + 인증 앱 없음 - %1$s 추가 완료 + %1$s 추가 완료 - 인증 앱 옵션 열기 + 인증 앱 옵션 열기 - 이름 변경 + 이름 변경 - 삭제 + 삭제 - 인증 앱을 삭제할까요? + 인증 앱을 삭제할까요? - 이 인증 수단을 삭제하려면 인증 앱의 6자리 코드가 필요합니다. + 이 인증 수단을 삭제하려면 인증 앱의 6자리 코드가 필요합니다. - 인증 앱을 추가할 수 없음 + 인증 앱을 추가할 수 없음 - 인증 앱은 최대 %1$d개까지만 등록할 수 있습니다. 기존 앱 하나를 삭제한 후 다시 시도해 주세요. + 인증 앱은 최대 %1$d개까지만 등록할 수 있습니다. 기존 앱 하나를 삭제한 후 다시 시도해 주세요. - 인증 앱이 삭제됨 + 인증 앱이 삭제됨 - + - 이름을 선택하세요 + 이름을 선택하세요 - 이 인증 앱에 해당하는 이름을 설정해 주세요. + 이 인증 앱에 해당하는 이름을 설정해 주세요. - 나중에 알아보기 쉽도록 이 인증 앱에 이름을 설정해 주세요. + 나중에 알아보기 쉽도록 이 인증 앱에 이름을 설정해 주세요. - 이름 + 이름 - 다음 + 다음 - 인증 앱 설정 + 인증 앱 설정 - 인증 앱 이름이 변경됨 + 인증 앱 이름이 변경됨 diff --git a/feature/app-settings/src/main/res/values-ky/strings.xml b/feature/app-settings/src/main/res/values-ky/strings.xml index ca0f6dbbe5..d2bb80eb68 100644 --- a/feature/app-settings/src/main/res/values-ky/strings.xml +++ b/feature/app-settings/src/main/res/values-ky/strings.xml @@ -80,37 +80,37 @@ Кененирээк маалымат - + - Set up your authenticator app + Set up your authenticator app - Төмөнкү кадамдар менен авторизация колдонмосун коюңуз. + Төмөнкү кадамдар менен авторизация колдонмосун коюңуз. - Кененирээк маалымат + Кененирээк маалымат - 1-кадам + 1-кадам - Түзмөгүңүзгө ишеничтүү авторизация колдонмосун орнотуңуз. + Түзмөгүңүзгө ишеничтүү авторизация колдонмосун орнотуңуз. - 2-кадам + 2-кадам - Авторизация колдонмосун ачуу үчүн төмөнкү баскычты басып, Signal аккаунтуңузду кошуңуз. + Авторизация колдонмосун ачуу үчүн төмөнкү баскычты басып, Signal аккаунтуңузду кошуңуз. - Ачуу + Ачуу - Кааласаңыз, ушул код аркылуу кол менен койсоңуз болот. + Кааласаңыз, ушул код аркылуу кол менен койсоңуз болот. - Көчүрүү + Көчүрүү - Буферге көчүрүлдү + Буферге көчүрүлдү - Авторизация колдонмосу табылган жок + Авторизация колдонмосу табылган жок - 3-кадам + 3-кадам - Пайда болгон кодду көчүрүп, бул жерге кайтып улантыңыз. + Пайда болгон кодду көчүрүп, бул жерге кайтып улантыңыз. - Улантуу + Улантуу @@ -140,60 +140,60 @@ Алып салуу - + - Кодуңузду киргизиңиз + Кодуңузду киргизиңиз - Авторизация колдонмоңуздагы 6 орундуу кодду киргизиңиз. + Авторизация колдонмоңуздагы 6 орундуу кодду киргизиңиз. - Код + Код - Бүттү - + Бүттү + - Авторизация колдонмосу + Авторизация колдонмосу - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Кененирээк маалымат + Кененирээк маалымат - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - \"%1$s\" кошулду + \"%1$s\" кошулду - Open authenticator app options + Open authenticator app options - Rename + Rename - Алып салуу + Алып салуу - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Ысым + Ысым - Кийинки + Кийинки - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-lt/strings.xml b/feature/app-settings/src/main/res/values-lt/strings.xml index d1312c47fd..039803fdf4 100644 --- a/feature/app-settings/src/main/res/values-lt/strings.xml +++ b/feature/app-settings/src/main/res/values-lt/strings.xml @@ -86,37 +86,37 @@ Sužinoti daugiau - + - Nustatykite savo autentifikavimo programėlę + Nustatykite savo autentifikavimo programėlę - Nustatykite savo autentifikavimo programėlę vadovaudamiesi šiais veiksmais. + Nustatykite savo autentifikavimo programėlę vadovaudamiesi šiais veiksmais. - Sužinoti daugiau + Sužinoti daugiau - 1 žingsnis + 1 žingsnis - Įrenginyje įdiekite patikimą autentifikavimo programėlę. + Įrenginyje įdiekite patikimą autentifikavimo programėlę. - 2 žingsnis + 2 žingsnis - Bakstelėkite tolesnį mygtuką ir atverkite autentifikavimo programėlę, kad pridėtumėte „Signal“ paskyrą. + Bakstelėkite tolesnį mygtuką ir atverkite autentifikavimo programėlę, kad pridėtumėte „Signal“ paskyrą. - Atverti + Atverti - Taip pat galite nukopijuoti šį raktą ir ją nustatyti rankiniu būdu. + Taip pat galite nukopijuoti šį raktą ir ją nustatyti rankiniu būdu. - Kopijuoti + Kopijuoti - Nukopijuota į iškarpinę + Nukopijuota į iškarpinę - Autentifikavimo programėlė nerasta + Autentifikavimo programėlė nerasta - 3 žingsnis + 3 žingsnis - Nukopijuokite sugeneruotą kodą ir grįžkite čia, kad galėtumėte tęsti. + Nukopijuokite sugeneruotą kodą ir grįžkite čia, kad galėtumėte tęsti. - Tęsti + Tęsti @@ -146,60 +146,60 @@ Šalinti - + - Įveskite kodą + Įveskite kodą - Įveskite 6 skaitmenų kodą iš autentifikavimo programėlės. + Įveskite 6 skaitmenų kodą iš autentifikavimo programėlės. - Kodas + Kodas - Atlikta - + Atlikta + - Autentifikavimo programėlė + Autentifikavimo programėlė - Nustatykite autentifikavimo programėlę generuoti vienkartinius patvirtinimo kodus + Nustatykite autentifikavimo programėlę generuoti vienkartinius patvirtinimo kodus - Sužinoti daugiau + Sužinoti daugiau - Pridėti autentifikavimo programėlę + Pridėti autentifikavimo programėlę - Autentifikavimo programėlės + Autentifikavimo programėlės - Autentifikavimo programėlių nėra + Autentifikavimo programėlių nėra - Pridėtas naudotojas „%1$s“ + Pridėtas naudotojas „%1$s“ - Atverti autentifikavimo programėlės parinktis + Atverti autentifikavimo programėlės parinktis - Pervadinti + Pervadinti - Šalinti + Šalinti - Pašalinti autentifikavimo programėlę? + Pašalinti autentifikavimo programėlę? - Norint pašalinti šį autentifikavimo būdą, reikia nurodyti autentifikavimo programėlėje pateiktą 6 skaitmenų kodą. + Norint pašalinti šį autentifikavimo būdą, reikia nurodyti autentifikavimo programėlėje pateiktą 6 skaitmenų kodą. - Negalima pridėti autentifikavimo programėlės + Negalima pridėti autentifikavimo programėlės - Negalite pridėti daugiau kaip %1$d autentifikavimo programėlių. Pirmiausia pabandykite pašalinti vieną jų. + Negalite pridėti daugiau kaip %1$d autentifikavimo programėlių. Pirmiausia pabandykite pašalinti vieną jų. - Autentifikavimo programėlė pašalinta + Autentifikavimo programėlė pašalinta - + - Pasirinkite pavadinimą + Pasirinkite pavadinimą - Šiai autentifikavimo programėlei parinkite unikalų pavadinimą. + Šiai autentifikavimo programėlei parinkite unikalų pavadinimą. - Šiai autentifikavimo programėlei parinkite unikalų pavadinimą, kad vėliau galėtumėte ją identifikuoti. + Šiai autentifikavimo programėlei parinkite unikalų pavadinimą, kad vėliau galėtumėte ją identifikuoti. - Vardas + Vardas - Kitas + Kitas - Autentifikavimo programėlės nustatymas + Autentifikavimo programėlės nustatymas - Autentifikavimo programėlės pavadinimas pakeistas + Autentifikavimo programėlės pavadinimas pakeistas diff --git a/feature/app-settings/src/main/res/values-lv/strings.xml b/feature/app-settings/src/main/res/values-lv/strings.xml index a464ed629d..01fae290ca 100644 --- a/feature/app-settings/src/main/res/values-lv/strings.xml +++ b/feature/app-settings/src/main/res/values-lv/strings.xml @@ -83,37 +83,37 @@ Uzzināt vairāk - + - Set up your authenticator app + Set up your authenticator app - Izpildiet šos soļus, lai uzstādītu autentifikācijas lietotni. + Izpildiet šos soļus, lai uzstādītu autentifikācijas lietotni. - Uzzināt vairāk + Uzzināt vairāk - 1. solis + 1. solis - Ieinstalējiet uzticamu autentifikācijas lietotni savā ierīcē. + Ieinstalējiet uzticamu autentifikācijas lietotni savā ierīcē. - 2. solis + 2. solis - Lai pievienotu savu Signal kontu, atveriet autentifikācijas lietotni, pieskaroties pogai zemāk. + Lai pievienotu savu Signal kontu, atveriet autentifikācijas lietotni, pieskaroties pogai zemāk. - Atvērt + Atvērt - Tāpat varat nokopēt šo atslēgu un iestatīt kontu manuāli. + Tāpat varat nokopēt šo atslēgu un iestatīt kontu manuāli. - Kopēt + Kopēt - Iekopēts starpliktuvē + Iekopēts starpliktuvē - Autentifikācijas lietotne nav atrasta + Autentifikācijas lietotne nav atrasta - 3. solis + 3. solis - Nokopējiet ģenerēto kodu un atgriezieties šeit, lai turpinātu. + Nokopējiet ģenerēto kodu un atgriezieties šeit, lai turpinātu. - Turpināt + Turpināt @@ -143,60 +143,60 @@ Noņemt - + - Ievadiet savu kodu + Ievadiet savu kodu - Ievadiet autentifikācijas lietotnē redzamo sešciparu kodu. + Ievadiet autentifikācijas lietotnē redzamo sešciparu kodu. - Kods + Kods - Gatavs - + Gatavs + - Autentifikācijas lietotne + Autentifikācijas lietotne - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Uzzināt vairāk + Uzzināt vairāk - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Pievienota \"%1$s\" + Pievienota \"%1$s\" - Open authenticator app options + Open authenticator app options - Rename + Rename - Noņemt + Noņemt - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Vārds + Vārds - Tālāk + Tālāk - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-mk/strings.xml b/feature/app-settings/src/main/res/values-mk/strings.xml index e726c0c717..324260317c 100644 --- a/feature/app-settings/src/main/res/values-mk/strings.xml +++ b/feature/app-settings/src/main/res/values-mk/strings.xml @@ -80,37 +80,37 @@ Дознајте повеќе - + - Set up your authenticator app + Set up your authenticator app - Следете ги овие чекори за да ја поставите вашата апликација за автентикација. + Следете ги овие чекори за да ја поставите вашата апликација за автентикација. - Дознајте повеќе + Дознајте повеќе - Прв чекор + Прв чекор - На вашиот уред инсталирајте апликација за автентикација од доверлив извор. + На вашиот уред инсталирајте апликација за автентикација од доверлив извор. - Втор чекор + Втор чекор - Отворете ја апликацијата за автентикација со допирање на копчето подолу за да ја додадете вашата корисничка сметка на Signal. + Отворете ја апликацијата за автентикација со допирање на копчето подолу за да ја додадете вашата корисничка сметка на Signal. - Отворете + Отворете - Или можете да го ископирате овој клуч за да ја поставите рачно. + Или можете да го ископирате овој клуч за да ја поставите рачно. - Копирај + Копирај - Копирано на таблата со исечоци + Копирано на таблата со исечоци - Не е пронајдена апликација за автентикација + Не е пронајдена апликација за автентикација - Трет чекор + Трет чекор - Ископирајте го кодот којшто е генериран и вратете се овде за да продолжите. + Ископирајте го кодот којшто е генериран и вратете се овде за да продолжите. - Продолжи + Продолжи @@ -140,60 +140,60 @@ Отстрани - + - Внесете го вашиот код + Внесете го вашиот код - Внесете го кодот од 6 цифри од вашата апликација за автентикација. + Внесете го кодот од 6 цифри од вашата апликација за автентикација. - Код + Код - Готово - + Готово + - Апликација за автентикација + Апликација за автентикација - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Дознајте повеќе + Дознајте повеќе - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - „%1$s“ е додаден/а + „%1$s“ е додаден/а - Open authenticator app options + Open authenticator app options - Rename + Rename - Отстрани + Отстрани - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Name + Name - Следно + Следно - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-ml/strings.xml b/feature/app-settings/src/main/res/values-ml/strings.xml index d1afd11a0e..50b920990a 100644 --- a/feature/app-settings/src/main/res/values-ml/strings.xml +++ b/feature/app-settings/src/main/res/values-ml/strings.xml @@ -80,37 +80,37 @@ കൂടുതലറിയുക - + - നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പ് സജ്ജീകരിക്കുക + നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പ് സജ്ജീകരിക്കുക - നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പ് സജ്ജീകരിക്കാൻ ഈ ഘട്ടങ്ങൾ പാലിക്കുക. + നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പ് സജ്ജീകരിക്കാൻ ഈ ഘട്ടങ്ങൾ പാലിക്കുക. - കൂടുതലറിയുക + കൂടുതലറിയുക - ഘട്ടം 1 + ഘട്ടം 1 - നിങ്ങളുടെ ഉപകരണത്തിൽ വിശ്വസനീയമായ ഒരു ഓതന്റിക്കേറ്റർ ആപ്പ് ഇൻസ്റ്റാൾ ചെയ്യുക. + നിങ്ങളുടെ ഉപകരണത്തിൽ വിശ്വസനീയമായ ഒരു ഓതന്റിക്കേറ്റർ ആപ്പ് ഇൻസ്റ്റാൾ ചെയ്യുക. - ഘട്ടം 2 + ഘട്ടം 2 - നിങ്ങളുടെ Signal അക്കൗണ്ട് ചേർക്കാൻ താഴെയുള്ള ബട്ടൺ ടാപ്പ് ചെയ്ത് നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പ് തുറക്കുക. + നിങ്ങളുടെ Signal അക്കൗണ്ട് ചേർക്കാൻ താഴെയുള്ള ബട്ടൺ ടാപ്പ് ചെയ്ത് നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പ് തുറക്കുക. - തുറക്കുക + തുറക്കുക - അല്ലെങ്കിൽ സ്വമേധയാ സജ്ജീകരിക്കുന്നതിന് നിങ്ങൾക്ക് ഈ കീ പകർത്താം. + അല്ലെങ്കിൽ സ്വമേധയാ സജ്ജീകരിക്കുന്നതിന് നിങ്ങൾക്ക് ഈ കീ പകർത്താം. - പകർത്തൂ + പകർത്തൂ - ക്ലിപ്പ്ബോർഡിലേക്ക് പകർത്തി + ക്ലിപ്പ്ബോർഡിലേക്ക് പകർത്തി - ഓതന്റിക്കേറ്റർ ആപ്പുകളൊന്നും കണ്ടെത്തിയില്ല + ഓതന്റിക്കേറ്റർ ആപ്പുകളൊന്നും കണ്ടെത്തിയില്ല - ഘട്ടം 3 + ഘട്ടം 3 - ജനറേറ്റ് ചെയ്ത കോഡ് പകർത്തി, തുടരുന്നതിനായി ഇവിടെ തിരികെ വരിക. + ജനറേറ്റ് ചെയ്ത കോഡ് പകർത്തി, തുടരുന്നതിനായി ഇവിടെ തിരികെ വരിക. - തുടരുക + തുടരുക @@ -140,60 +140,60 @@ നീക്കം ചെയ്യൂ - + - നിങ്ങളുടെ കോഡ് നൽകുക + നിങ്ങളുടെ കോഡ് നൽകുക - നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പിൽ നിന്നുള്ള 6 അക്ക കോഡ് നൽകുക. + നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പിൽ നിന്നുള്ള 6 അക്ക കോഡ് നൽകുക. - കോഡ് + കോഡ് - പൂർത്തിയായി - + പൂർത്തിയായി + - ഓതന്റിക്കേറ്റർ ആപ്പ് + ഓതന്റിക്കേറ്റർ ആപ്പ് - ഒറ്റത്തവണ പരിശോധിച്ചുറപ്പിക്കൽ കോഡുകൾ ജനറേറ്റ് ചെയ്യാൻ ഒരു ഓതന്റിക്കേറ്റർ ആപ്പ് സജ്ജീകരിക്കുക + ഒറ്റത്തവണ പരിശോധിച്ചുറപ്പിക്കൽ കോഡുകൾ ജനറേറ്റ് ചെയ്യാൻ ഒരു ഓതന്റിക്കേറ്റർ ആപ്പ് സജ്ജീകരിക്കുക - കൂടുതലറിയുക + കൂടുതലറിയുക - ഓതന്റിക്കേറ്റർ ആപ്പ് ചേർക്കുക + ഓതന്റിക്കേറ്റർ ആപ്പ് ചേർക്കുക - ഓതന്റിക്കേറ്റർ ആപ്പുകൾ + ഓതന്റിക്കേറ്റർ ആപ്പുകൾ - ഓതന്റിക്കേറ്റർ ആപ്പുകൾ ഒന്നുമില്ല + ഓതന്റിക്കേറ്റർ ആപ്പുകൾ ഒന്നുമില്ല - \"%1$s\" ചേർത്തു + \"%1$s\" ചേർത്തു - ഓതന്റിക്കേറ്റർ ആപ്പ് ഓപ്ഷനുകൾ തുറക്കുക + ഓതന്റിക്കേറ്റർ ആപ്പ് ഓപ്ഷനുകൾ തുറക്കുക - പേര് മാറ്റുക + പേര് മാറ്റുക - നീക്കം ചെയ്യൂ + നീക്കം ചെയ്യൂ - ഓതന്റിക്കേറ്റർ ആപ്പ് നീക്കം ചെയ്യണോ? + ഓതന്റിക്കേറ്റർ ആപ്പ് നീക്കം ചെയ്യണോ? - ഈ ആധികാരികത ഉറപ്പാക്കൽ രീതി നീക്കം ചെയ്യുന്നതിന് നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പിൽ നിന്നുള്ള 6 അക്ക കോഡ് ആവശ്യമാണ്. + ഈ ആധികാരികത ഉറപ്പാക്കൽ രീതി നീക്കം ചെയ്യുന്നതിന് നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പിൽ നിന്നുള്ള 6 അക്ക കോഡ് ആവശ്യമാണ്. - ഓതന്റിക്കേറ്റർ ആപ്പ് ചേർക്കാൻ കഴിയില്ല + ഓതന്റിക്കേറ്റർ ആപ്പ് ചേർക്കാൻ കഴിയില്ല - നിങ്ങൾക്ക് %1$d-ൽ കൂടുതൽ ഓതന്റിക്കേറ്റർ ആപ്പുകൾ ചേർക്കാൻ കഴിയില്ല. ആദ്യം ഒരെണ്ണം നീക്കം ചെയ്യാൻ ശ്രമിക്കുക. + നിങ്ങൾക്ക് %1$d-ൽ കൂടുതൽ ഓതന്റിക്കേറ്റർ ആപ്പുകൾ ചേർക്കാൻ കഴിയില്ല. ആദ്യം ഒരെണ്ണം നീക്കം ചെയ്യാൻ ശ്രമിക്കുക. - ഓതന്റിക്കേറ്റർ ആപ്പ് നീക്കം ചെയ്‌തു + ഓതന്റിക്കേറ്റർ ആപ്പ് നീക്കം ചെയ്‌തു - + - ഒരു പേര് തിരഞ്ഞെടുക്കുക + ഒരു പേര് തിരഞ്ഞെടുക്കുക - ഈ ഓതന്റിക്കേറ്റർ ആപ്പിനായി ഒരു തനത് പേര് തിരഞ്ഞെടുക്കുക. + ഈ ഓതന്റിക്കേറ്റർ ആപ്പിനായി ഒരു തനത് പേര് തിരഞ്ഞെടുക്കുക. - പിന്നീട് തിരിച്ചറിയാൻ സഹായിക്കുന്നതിന് ഈ ഓതന്റിക്കേറ്റർ ആപ്പിനായി ഒരു തനത് പേര് തിരഞ്ഞെടുക്കുക. + പിന്നീട് തിരിച്ചറിയാൻ സഹായിക്കുന്നതിന് ഈ ഓതന്റിക്കേറ്റർ ആപ്പിനായി ഒരു തനത് പേര് തിരഞ്ഞെടുക്കുക. - പേര് + പേര് - അടുത്തത് + അടുത്തത് - ഓതന്റിക്കേറ്റർ ആപ്പ് സജ്ജീകരിച്ചു + ഓതന്റിക്കേറ്റർ ആപ്പ് സജ്ജീകരിച്ചു - ഓതന്റിക്കേറ്റർ ആപ്പിന്റെ പേര് മാറ്റി + ഓതന്റിക്കേറ്റർ ആപ്പിന്റെ പേര് മാറ്റി diff --git a/feature/app-settings/src/main/res/values-mr/strings.xml b/feature/app-settings/src/main/res/values-mr/strings.xml index 446caa12cf..114e8dac33 100644 --- a/feature/app-settings/src/main/res/values-mr/strings.xml +++ b/feature/app-settings/src/main/res/values-mr/strings.xml @@ -80,37 +80,37 @@ अधिक जाणून घ्या - + - तुमचे ऑथेंटिकेटर ॲप सेट अप करा + तुमचे ऑथेंटिकेटर ॲप सेट अप करा - तुमचे ऑथेंटिकेटर ॲप सेट अप करण्यासाठी या पायऱ्या पाळा. + तुमचे ऑथेंटिकेटर ॲप सेट अप करण्यासाठी या पायऱ्या पाळा. - अधिक जाणून घ्या + अधिक जाणून घ्या - पायरी 1 + पायरी 1 - तुमच्या डिव्हाईस वर एक विश्वासार्ह ऑथेंटिकेटर ॲप इन्स्टॉल करा. + तुमच्या डिव्हाईस वर एक विश्वासार्ह ऑथेंटिकेटर ॲप इन्स्टॉल करा. - पायरी 2 + पायरी 2 - तुमचे Signal खाते समाविष्ट करण्यासाठी खालचे बटण दाबून तुमचे ऑथेंटिकेटर ॲप उघडा. + तुमचे Signal खाते समाविष्ट करण्यासाठी खालचे बटण दाबून तुमचे ऑथेंटिकेटर ॲप उघडा. - उघडा + उघडा - किंवा तुम्ही स्वतःहून ते सेट करण्यासाठी ही की कॉपी करू शकता. + किंवा तुम्ही स्वतःहून ते सेट करण्यासाठी ही की कॉपी करू शकता. - कॉपी करा + कॉपी करा - क्लिपबोर्ड वर कॉपी केले + क्लिपबोर्ड वर कॉपी केले - कोणतेही ऑथेंटिकेटर ॲप सापडले नाही + कोणतेही ऑथेंटिकेटर ॲप सापडले नाही - पायरी 3 + पायरी 3 - तयार झालेला कोड कॉपी करा आणि पुढे जात राहण्यासाठी इथे परत या. + तयार झालेला कोड कॉपी करा आणि पुढे जात राहण्यासाठी इथे परत या. - सुरू ठेवा + सुरू ठेवा @@ -140,60 +140,60 @@ काढून टाका - + - तुमचा कोड प्रविष्ट करा + तुमचा कोड प्रविष्ट करा - तुमच्या ऑथेंटिकेटर ॲपमधील 6-अंकी कोड प्रविष्ट करा. + तुमच्या ऑथेंटिकेटर ॲपमधील 6-अंकी कोड प्रविष्ट करा. - कोड + कोड - झाले - + झाले + - ऑथेंटिकेटर ॲप + ऑथेंटिकेटर ॲप - एकवेळचे पडताळणी कोड तयार करण्यासाठी ऑथेंटिकेटर ॲप सेट अप करा + एकवेळचे पडताळणी कोड तयार करण्यासाठी ऑथेंटिकेटर ॲप सेट अप करा - अधिक जाणून घ्या + अधिक जाणून घ्या - ऑथेंटिकेटर ॲप समाविष्ट करा + ऑथेंटिकेटर ॲप समाविष्ट करा - ऑथेंटिकेटर ॲप्स + ऑथेंटिकेटर ॲप्स - कोणतेही ऑथेंटिकेटर ॲप नाही + कोणतेही ऑथेंटिकेटर ॲप नाही - \"%1$s\" ला जोडले + \"%1$s\" ला जोडले - ऑथेंटिकेटर ॲपमधील पर्याय उघडा + ऑथेंटिकेटर ॲपमधील पर्याय उघडा - नाव बदला + नाव बदला - काढून टाका + काढून टाका - ऑथेंटिकेटर ॲप हटवायचे का? + ऑथेंटिकेटर ॲप हटवायचे का? - अधिस्वीकृतीची ही पद्धत हटवण्यासाठी तुमच्या ऑथेंटिकेटर ॲपमधील 6 अंकी कोड आवश्यक आहे. + अधिस्वीकृतीची ही पद्धत हटवण्यासाठी तुमच्या ऑथेंटिकेटर ॲपमधील 6 अंकी कोड आवश्यक आहे. - ऑथेंटिकेटर ॲप समाविष्ट करता येत नाही + ऑथेंटिकेटर ॲप समाविष्ट करता येत नाही - तुम्ही %1$d पेक्षा अधिक ऑथेंटिकेटर ॲप्स समाविष्ट करू शकत नाही. आधी एक हटवून पाहा. + तुम्ही %1$d पेक्षा अधिक ऑथेंटिकेटर ॲप्स समाविष्ट करू शकत नाही. आधी एक हटवून पाहा. - ऑथेंटिकेटर ॲप हटवून टाकले + ऑथेंटिकेटर ॲप हटवून टाकले - + - एक नाव निवडा + एक नाव निवडा - या ऑथेंटिकेटर ॲपसाठी एक अनोखे नाव निवडा. + या ऑथेंटिकेटर ॲपसाठी एक अनोखे नाव निवडा. - या ऑथेंटिकेटर ॲपसाठी एक अनोखा आकडा निवडा ज्यामुळे ते नंतर ओळखायला तुम्हाला मदत होईल. + या ऑथेंटिकेटर ॲपसाठी एक अनोखा आकडा निवडा ज्यामुळे ते नंतर ओळखायला तुम्हाला मदत होईल. - नाव + नाव - पुढे + पुढे - ऑथेंटिकेटर ॲप सेट अप करा + ऑथेंटिकेटर ॲप सेट अप करा - ऑथेंटिकेटर ॲपचे नाव बदलले + ऑथेंटिकेटर ॲपचे नाव बदलले diff --git a/feature/app-settings/src/main/res/values-ms/strings.xml b/feature/app-settings/src/main/res/values-ms/strings.xml index e64ec3ab8b..16822d16dc 100644 --- a/feature/app-settings/src/main/res/values-ms/strings.xml +++ b/feature/app-settings/src/main/res/values-ms/strings.xml @@ -77,37 +77,37 @@ Ketahui lebih lanjut - + - Sediakan aplikasi pengesah anda + Sediakan aplikasi pengesah anda - Ikut langkah ini untuk menyediakan aplikasi pengesah anda. + Ikut langkah ini untuk menyediakan aplikasi pengesah anda. - Ketahui lebih lanjut + Ketahui lebih lanjut - Langkah 1 + Langkah 1 - Pasang aplikasi pengesah yang dipercayai pada peranti anda. + Pasang aplikasi pengesah yang dipercayai pada peranti anda. - Langkah 2 + Langkah 2 - Buka aplikasi pengesah anda dengan mengetik butang di bawah untuk menambahkan akaun Signal anda. + Buka aplikasi pengesah anda dengan mengetik butang di bawah untuk menambahkan akaun Signal anda. - Buka + Buka - Atau anda boleh menyalin kunci ini untuk menyediakan secara manual. + Atau anda boleh menyalin kunci ini untuk menyediakan secara manual. - Salin + Salin - Disalin ke papan klip + Disalin ke papan klip - Tiada aplikasi pengesah ditemukan + Tiada aplikasi pengesah ditemukan - Langkah 3 + Langkah 3 - Salin kod yang dijana dan kembali ke sini untuk meneruskan. + Salin kod yang dijana dan kembali ke sini untuk meneruskan. - Teruskan + Teruskan @@ -137,60 +137,60 @@ Alih keluar - + - Masukkan kod anda + Masukkan kod anda - Masukkan kod 6 digit daripada aplikasi pengesah anda. + Masukkan kod 6 digit daripada aplikasi pengesah anda. - Kod + Kod - Selesai - + Selesai + - Aplikasi pengesah + Aplikasi pengesah - Sediakan aplikasi pengesah untuk menjana kod pengesahan satu kali + Sediakan aplikasi pengesah untuk menjana kod pengesahan satu kali - Ketahui lebih lanjut + Ketahui lebih lanjut - Tambah aplikasi pengesah + Tambah aplikasi pengesah - Aplikasi pengesah + Aplikasi pengesah - Tiada aplikasi pengesah + Tiada aplikasi pengesah - Menambah \"%1$s\" + Menambah \"%1$s\" - Buka pilihan aplikasi pengesah + Buka pilihan aplikasi pengesah - Namakan semula + Namakan semula - Alih keluar + Alih keluar - Alih keluar aplikasi pengesah? + Alih keluar aplikasi pengesah? - Untuk mengalih keluar kaedah pengesahan ini, kod 6 digit daripada aplikasi pengesah anda diperlukan. + Untuk mengalih keluar kaedah pengesahan ini, kod 6 digit daripada aplikasi pengesah anda diperlukan. - Tidak dapat menambah aplikasi pengesah + Tidak dapat menambah aplikasi pengesah - Anda tidak boleh menambah lebih daripada %1$d aplikasi pengesah. Cuba alih keluar satu dahulu. + Anda tidak boleh menambah lebih daripada %1$d aplikasi pengesah. Cuba alih keluar satu dahulu. - Aplikasi pengesah dialih keluar + Aplikasi pengesah dialih keluar - + - Pilih nama + Pilih nama - Pilih nama unik untuk aplikasi pengesah ini. + Pilih nama unik untuk aplikasi pengesah ini. - Pilih nama unik untuk aplikasi pengesah ini untuk membantu anda mengenal pastinya nanti. + Pilih nama unik untuk aplikasi pengesah ini untuk membantu anda mengenal pastinya nanti. - Nama + Nama - Seterusnya + Seterusnya - Persediaan aplikasi pengesah + Persediaan aplikasi pengesah - Aplikasi pengesah dinamakan semula + Aplikasi pengesah dinamakan semula diff --git a/feature/app-settings/src/main/res/values-my/strings.xml b/feature/app-settings/src/main/res/values-my/strings.xml index e1552a6b89..c1257842b0 100644 --- a/feature/app-settings/src/main/res/values-my/strings.xml +++ b/feature/app-settings/src/main/res/values-my/strings.xml @@ -77,37 +77,37 @@ ပိုမိုလေ့လာရန် - + - သင့်အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို သတ်မှတ်ပါ + သင့်အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို သတ်မှတ်ပါ - သင့် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို စတင်အသုံးပြုရန် အောက်ပါအဆင့်များအတိုင်း လုပ်ဆောင်ပါ။ + သင့် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို စတင်အသုံးပြုရန် အောက်ပါအဆင့်များအတိုင်း လုပ်ဆောင်ပါ။ - ပိုမိုလေ့လာရန် + ပိုမိုလေ့လာရန် - အဆင့် ၁ + အဆင့် ၁ - သင့်စက်တွင် ယုံကြည်စိတ်ချရသော အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ထည့်သွင်းပါ။ + သင့်စက်တွင် ယုံကြည်စိတ်ချရသော အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ထည့်သွင်းပါ။ - အဆင့် ၂ + အဆင့် ၂ - သင့် Signal အကောင့်ကို ထည့်သွင်းရန် အောက်ပါခလုတ်ကို နှိပ်ပြီး သင့် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ဖွင့်ပါ။ + သင့် Signal အကောင့်ကို ထည့်သွင်းရန် အောက်ပါခလုတ်ကို နှိပ်ပြီး သင့် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ဖွင့်ပါ။ - ဖွင့်ရန် + ဖွင့်ရန် - သို့မဟုတ် ဤကီးကို ကူးယူပြီး ကိုယ်တိုင် ထည့်သွင်းနိုင်ပါသည်။ + သို့မဟုတ် ဤကီးကို ကူးယူပြီး ကိုယ်တိုင် ထည့်သွင်းနိုင်ပါသည်။ - ကူးယူမည် + ကူးယူမည် - ကလစ်ဘုတ်သို့ ကူးယူပြီး + ကလစ်ဘုတ်သို့ ကူးယူပြီး - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို မတွေ့ပါ + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို မတွေ့ပါ - အဆင့် ၃ + အဆင့် ၃ - ထုတ်ပေးထားသော ကုဒ်ကို ကူးယူပြီး ဆက်လက်လုပ်ဆောင်ရန် ဤနေရာသို့ ပြန်လာပါ။ + ထုတ်ပေးထားသော ကုဒ်ကို ကူးယူပြီး ဆက်လက်လုပ်ဆောင်ရန် ဤနေရာသို့ ပြန်လာပါ။ - ဆက်လုပ်ရန် + ဆက်လုပ်ရန် @@ -137,60 +137,60 @@ ဖယ်ရှားရန် - + - သင့်ကုဒ်ကို ထည့်ပါ + သင့်ကုဒ်ကို ထည့်ပါ - သင့် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်မှ ရရှိသော ဂဏန်း 6 လုံးပါ ကုဒ်ကို ထည့်ပါ။ + သင့် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်မှ ရရှိသော ဂဏန်း 6 လုံးပါ ကုဒ်ကို ထည့်ပါ။ - ကုဒ် + ကုဒ် - ပြီးပါပြီ - + ပြီးပါပြီ + - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ် + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ် - တစ်ကြိမ်သာ အသုံးပြုနိုင်သော အတည်ပြုကုဒ်များကို ထုတ်ပေးရန် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို အသုံးပြုပါ + တစ်ကြိမ်သာ အသုံးပြုနိုင်သော အတည်ပြုကုဒ်များကို ထုတ်ပေးရန် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို အသုံးပြုပါ - ပိုမိုလေ့လာရန် + ပိုမိုလေ့လာရန် - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ် ထည့်ပါ + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ် ထည့်ပါ - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်များ + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်များ - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်မရှိပါ + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်မရှိပါ - \"%1$s\" ကို ပေါင်းထည့်ပြီး + \"%1$s\" ကို ပေါင်းထည့်ပြီး - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ် ရွေးချယ်စရာများကို ဖွင့်ရန် + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ် ရွေးချယ်စရာများကို ဖွင့်ရန် - အမည်ပြောင်းရန် + အမည်ပြောင်းရန် - ဖယ်ရှားရန် + ဖယ်ရှားရန် - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ဖယ်ရှားမလား။ + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ဖယ်ရှားမလား။ - ဤအတည်ပြုနည်းလမ်းကို ဖယ်ရှားရန် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်မှ ဂဏန်း ၆ လုံးပါ ကုဒ် လိုအပ်ပါသည်။ + ဤအတည်ပြုနည်းလမ်းကို ဖယ်ရှားရန် အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်မှ ဂဏန်း ၆ လုံးပါ ကုဒ် လိုအပ်ပါသည်။ - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ထည့်၍မရပါ + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ထည့်၍မရပါ - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ် %1$d ခုထက်ပို၍ ထည့်၍မရပါ။ တစ်ခုကို အရင်ဆုံး ဖယ်ရှားကြည့်ပါ။ + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ် %1$d ခုထက်ပို၍ ထည့်၍မရပါ။ တစ်ခုကို အရင်ဆုံး ဖယ်ရှားကြည့်ပါ။ - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ဖယ်ရှားပြီးပါပြီ + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို ဖယ်ရှားပြီးပါပြီ - + - အမည်တစ်ခုကို ရွေးပါ + အမည်တစ်ခုကို ရွေးပါ - ဤအတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်အတွက် ထူးခြားသောအမည်တစ်ခုကို ရွေးပါ။ + ဤအတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်အတွက် ထူးခြားသောအမည်တစ်ခုကို ရွေးပါ။ - ဤအတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို နောက်ပိုင်းတွင် ခွဲခြားသိရှိနိုင်ရန် ထူးခြားသောအမည်တစ်ခုကို ရွေးပါ။ + ဤအတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို နောက်ပိုင်းတွင် ခွဲခြားသိရှိနိုင်ရန် ထူးခြားသောအမည်တစ်ခုကို ရွေးပါ။ - အမည် + အမည် - ဆက်သွားမည် + ဆက်သွားမည် - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို သတ်မှတ်ရန် + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို သတ်မှတ်ရန် - အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို အမည်ပြောင်းပြီးပါပြီ + အတည်ပြုကုဒ်ထုတ်ပေးသည့် အက်ပ်ကို အမည်ပြောင်းပြီးပါပြီ diff --git a/feature/app-settings/src/main/res/values-nb/strings.xml b/feature/app-settings/src/main/res/values-nb/strings.xml index c773a7f6cd..cc730115f8 100644 --- a/feature/app-settings/src/main/res/values-nb/strings.xml +++ b/feature/app-settings/src/main/res/values-nb/strings.xml @@ -80,37 +80,37 @@ Les mer - + - Konfigurer autentiseringsappen + Konfigurer autentiseringsappen - Følg denne veiledningen for å konfigurere autentiseringsappen din. + Følg denne veiledningen for å konfigurere autentiseringsappen din. - Les mer + Les mer - Trinn 1 + Trinn 1 - Last ned en pålitelig autentiseringsapp på enheten din. + Last ned en pålitelig autentiseringsapp på enheten din. - Trinn 2 + Trinn 2 - Åpne autentiseringsappen via knappen nedenfor og koble den til Signal-kontoen din. + Åpne autentiseringsappen via knappen nedenfor og koble den til Signal-kontoen din. - Åpne + Åpne - eller kopier denne koden for å konfigurere appen manuelt. + eller kopier denne koden for å konfigurere appen manuelt. - Kopier + Kopier - Kopiert til utklippstavlen + Kopiert til utklippstavlen - Fant ingen autentiseringsapp + Fant ingen autentiseringsapp - Trinn 3 + Trinn 3 - Kopier koden som genereres, og kom tilbake hit for å gå videre. + Kopier koden som genereres, og kom tilbake hit for å gå videre. - Fortsett + Fortsett @@ -140,60 +140,60 @@ Fjern - + - Angi koden din + Angi koden din - Skriv inn den sekssifrede koden fra autentiseringsappen din. + Skriv inn den sekssifrede koden fra autentiseringsappen din. - Kode + Kode - Ferdig - + Ferdig + - Autentiseringsapp + Autentiseringsapp - Konfigurer en autentiseringsapp for å generere unike bekreftelseskoder + Konfigurer en autentiseringsapp for å generere unike bekreftelseskoder - Les mer + Les mer - Legg til en autentiseringsapp + Legg til en autentiseringsapp - Autentiseringsapper + Autentiseringsapper - Ingen autentiseringsapper + Ingen autentiseringsapper - La til «%1$s» + La til «%1$s» - Se alternativer for autentisering + Se alternativer for autentisering - Gi nytt navn + Gi nytt navn - Fjern + Fjern - Vil du fjerne autentiseringsappen? + Vil du fjerne autentiseringsappen? - Du må angi den sekssifrede koden fra autentiseringsappen din for å fjerne denne autentiseringsmetoden. + Du må angi den sekssifrede koden fra autentiseringsappen din for å fjerne denne autentiseringsmetoden. - Autentiseringsappen kan ikke legges til + Autentiseringsappen kan ikke legges til - Du kan legge til maks %1$d autentiseringsapper. Fjern en app for å legge til en ny. + Du kan legge til maks %1$d autentiseringsapper. Fjern en app for å legge til en ny. - Autentiseringsappen ble fjernet + Autentiseringsappen ble fjernet - + - Velg et navn + Velg et navn - Gi autentiseringsappen et unikt navn. + Gi autentiseringsappen et unikt navn. - Gi autentiseringsappen et unikt navn for å finne den igjen senere. + Gi autentiseringsappen et unikt navn for å finne den igjen senere. - Navn + Navn - Neste + Neste - Autentiseringsappen er konfigurert + Autentiseringsappen er konfigurert - Navnet på autentiseringsappen ble endret + Navnet på autentiseringsappen ble endret diff --git a/feature/app-settings/src/main/res/values-nl/strings.xml b/feature/app-settings/src/main/res/values-nl/strings.xml index 469146c15e..492e958dbf 100644 --- a/feature/app-settings/src/main/res/values-nl/strings.xml +++ b/feature/app-settings/src/main/res/values-nl/strings.xml @@ -80,37 +80,37 @@ Meer lezen - + - Authenticatie-app instellen + Authenticatie-app instellen - Volg deze stappen om je authenticatie-app in te stellen. + Volg deze stappen om je authenticatie-app in te stellen. - Meer lezen + Meer lezen - Stap 1 + Stap 1 - Installeer een betrouwbare authenticatie-app op je apparaat. + Installeer een betrouwbare authenticatie-app op je apparaat. - Stap 2 + Stap 2 - Open je authenticatie-app door op de onderstaande knop te tikken om je Signal-account toe te voegen. + Open je authenticatie-app door op de onderstaande knop te tikken om je Signal-account toe te voegen. - Openen + Openen - Of kopieer deze sleutel en stel handmatig in. + Of kopieer deze sleutel en stel handmatig in. - Kopiëren + Kopiëren - Gekopieerd naar klembord + Gekopieerd naar klembord - Geen authenticatie-app gevonden + Geen authenticatie-app gevonden - Stap 3 + Stap 3 - Kopieer de gegenereerde code en ga terug naar dit scherm om verder te gaan. + Kopieer de gegenereerde code en ga terug naar dit scherm om verder te gaan. - Doorgaan + Doorgaan @@ -140,60 +140,60 @@ Verwijderen - + - Voer je code in + Voer je code in - Voer de 6-cijferige code uit je authenticatie-app in. + Voer de 6-cijferige code uit je authenticatie-app in. - Code + Code - Klaar - + Klaar + - Authenticatie-app + Authenticatie-app - Stel een authenticatie-app in om eenmalige verificatiecodes te genereren + Stel een authenticatie-app in om eenmalige verificatiecodes te genereren - Meer lezen + Meer lezen - Authenticatie-app toevoegen + Authenticatie-app toevoegen - Authenticatie-apps + Authenticatie-apps - Geen authenticatie-apps + Geen authenticatie-apps - ‘%1$s’ toegevoegd + ‘%1$s’ toegevoegd - Opties authenticatie-app openen + Opties authenticatie-app openen - Naam bewerken + Naam bewerken - Verwijderen + Verwijderen - Authenticatie-app verwijderen? + Authenticatie-app verwijderen? - Om deze authenticatie-app te verwijderen, heb je de 6-cijferige code uit je authenticatie-app nodig. + Om deze authenticatie-app te verwijderen, heb je de 6-cijferige code uit je authenticatie-app nodig. - Authenticatie-app kan niet worden toegevoegd + Authenticatie-app kan niet worden toegevoegd - Je kunt niet meer dan %1$d authenticatie-apps toevoegen. Probeer eerst een app te verwijderen. + Je kunt niet meer dan %1$d authenticatie-apps toevoegen. Probeer eerst een app te verwijderen. - Authenticatie-app verwijderd + Authenticatie-app verwijderd - + - Kies een naam + Kies een naam - Geef deze authenticatie-app een unieke naam. + Geef deze authenticatie-app een unieke naam. - Geef de authenticatie-app een unieke naam, zodat je deze gemakkelijk kunt herkennen. + Geef de authenticatie-app een unieke naam, zodat je deze gemakkelijk kunt herkennen. - Profielnaam + Profielnaam - Volgende + Volgende - Authenticatie-app ingesteld + Authenticatie-app ingesteld - Naam authenticatie-app gewijzigd + Naam authenticatie-app gewijzigd diff --git a/feature/app-settings/src/main/res/values-pa/strings.xml b/feature/app-settings/src/main/res/values-pa/strings.xml index bcb45d4e05..de6c205736 100644 --- a/feature/app-settings/src/main/res/values-pa/strings.xml +++ b/feature/app-settings/src/main/res/values-pa/strings.xml @@ -80,37 +80,37 @@ ਹੋਰ ਜਾਣੋ - + - ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨੂੰ ਸੈੱਟ ਅੱਪ ਕਰੋ + ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨੂੰ ਸੈੱਟ ਅੱਪ ਕਰੋ - ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨੂੰ ਸੈੱਟ ਅੱਪ ਕਰਨ ਲਈ ਇਹਨਾਂ ਕਦਮਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ। + ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨੂੰ ਸੈੱਟ ਅੱਪ ਕਰਨ ਲਈ ਇਹਨਾਂ ਕਦਮਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ। - ਹੋਰ ਜਾਣੋ + ਹੋਰ ਜਾਣੋ - ਕਦਮ 1 + ਕਦਮ 1 - ਆਪਣੇ ਡਿਵਾਈਸ ਉੱਤੇ ਭਰੋਸੇਯੋਗ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਇੰਸਟਾਲ ਕਰੋ। + ਆਪਣੇ ਡਿਵਾਈਸ ਉੱਤੇ ਭਰੋਸੇਯੋਗ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਇੰਸਟਾਲ ਕਰੋ। - ਕਦਮ 2 + ਕਦਮ 2 - ਆਪਣੇ Signal ਖਾਤੇ ਨੂੰ ਸ਼ਾਮਲ ਕਰਨ ਲਈ ਹੇਠਾਂ ਦਿੱਤੇ ਬਟਨ ਉੱਤੇ ਟੈਪ ਕਰਕੇ ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਖੋਲ੍ਹੋ। + ਆਪਣੇ Signal ਖਾਤੇ ਨੂੰ ਸ਼ਾਮਲ ਕਰਨ ਲਈ ਹੇਠਾਂ ਦਿੱਤੇ ਬਟਨ ਉੱਤੇ ਟੈਪ ਕਰਕੇ ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਖੋਲ੍ਹੋ। - ਖੋਲ੍ਹੋ + ਖੋਲ੍ਹੋ - ਜਾਂ ਤੁਸੀਂ ਇਸ ਕੁੰਜੀ ਨੂੰ ਖੁਦ ਸੈੱਟ ਅਪ ਕਰਨ ਲਈ ਕਾਪੀ ਕਰ ਸਕਦੇ ਹੋ। + ਜਾਂ ਤੁਸੀਂ ਇਸ ਕੁੰਜੀ ਨੂੰ ਖੁਦ ਸੈੱਟ ਅਪ ਕਰਨ ਲਈ ਕਾਪੀ ਕਰ ਸਕਦੇ ਹੋ। - ਕਾਪੀ ਕਰੋ + ਕਾਪੀ ਕਰੋ - ਕਲਿੱਪਬੋਰਡ \'ਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ + ਕਲਿੱਪਬੋਰਡ \'ਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ - ਕੋਈ ਵੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨਹੀਂ ਲੱਭੀ + ਕੋਈ ਵੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨਹੀਂ ਲੱਭੀ - ਕਦਮ 3 + ਕਦਮ 3 - ਤਿਆਰ ਕੀਤੇ ਗਏ ਕੋਡ ਨੂੰ ਕਾਪੀ ਕਰੋ ਅਤੇ ਜਾਰੀ ਰੱਖਣ ਲਈ ਇੱਥੇ ਵਾਪਸ ਆਓ। + ਤਿਆਰ ਕੀਤੇ ਗਏ ਕੋਡ ਨੂੰ ਕਾਪੀ ਕਰੋ ਅਤੇ ਜਾਰੀ ਰੱਖਣ ਲਈ ਇੱਥੇ ਵਾਪਸ ਆਓ। - ਜਾਰੀ ਰੱਖੋ + ਜਾਰੀ ਰੱਖੋ @@ -140,60 +140,60 @@ ਹਟਾਓ - + - ਆਪਣਾ ਕੋਡ ਦਰਜ ਕਰੋ + ਆਪਣਾ ਕੋਡ ਦਰਜ ਕਰੋ - ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਤੋਂ 6 ਅੰਕਾਂ ਵਾਲਾ ਕੋਡ ਦਰਜ ਕਰੋ। + ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਤੋਂ 6 ਅੰਕਾਂ ਵਾਲਾ ਕੋਡ ਦਰਜ ਕਰੋ। - ਕੋਡ + ਕੋਡ - ਮੁਕੰਮਲ - + ਮੁਕੰਮਲ + - ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ + ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ - ਇੱਕ-ਵਾਰੀ ਵਰਤਣ ਵਾਲੇ ਤਸਦੀਕ ਕੋਡ ਤਿਆਰ ਕਰਨ ਲਈ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਸੈੱਟ ਅੱਪ ਕਰੋ + ਇੱਕ-ਵਾਰੀ ਵਰਤਣ ਵਾਲੇ ਤਸਦੀਕ ਕੋਡ ਤਿਆਰ ਕਰਨ ਲਈ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਸੈੱਟ ਅੱਪ ਕਰੋ - ਹੋਰ ਜਾਣੋ + ਹੋਰ ਜਾਣੋ - ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਸ਼ਾਮਲ ਕਰੋ + ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਸ਼ਾਮਲ ਕਰੋ - ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ + ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ - ਕੋਈ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨਹੀਂ ਹੈ + ਕੋਈ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨਹੀਂ ਹੈ - %1$s ਨੂੰ ਸ਼ਾਮਲ ਕੀਤਾ + %1$s ਨੂੰ ਸ਼ਾਮਲ ਕੀਤਾ - ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਦੇ ਵਿਕਲਪ ਖੋਲ੍ਹੋ + ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਦੇ ਵਿਕਲਪ ਖੋਲ੍ਹੋ - ਨਾਮ ਬਦਲੋ + ਨਾਮ ਬਦਲੋ - ਹਟਾਓ + ਹਟਾਓ - ਕੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨੂੰ ਹਟਾਉਣਾ ਹੈ? + ਕੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਨੂੰ ਹਟਾਉਣਾ ਹੈ? - ਇਸ ਪ੍ਰਮਾਣੀਕਰਨ ਦੀ ਵਿਧੀ ਨੂੰ ਹਟਾਉਣ ਲਈ ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਤੋਂ 6 ਅੰਕਾਂ ਵਾਲਾ ਕੋਡ ਦਰਜ ਕਰਨਾ ਲੋੜੀਂਦਾ ਹੈ। + ਇਸ ਪ੍ਰਮਾਣੀਕਰਨ ਦੀ ਵਿਧੀ ਨੂੰ ਹਟਾਉਣ ਲਈ ਆਪਣੀ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਤੋਂ 6 ਅੰਕਾਂ ਵਾਲਾ ਕੋਡ ਦਰਜ ਕਰਨਾ ਲੋੜੀਂਦਾ ਹੈ। - ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਸ਼ਾਮਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ + ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਸ਼ਾਮਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ - ਤੁਸੀਂ %1$d ਤੋਂ ਵੱਧ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪਾਂ ਸ਼ਾਮਲ ਨਹੀਂ ਕਰ ਸਕਦੇ। ਪਹਿਲਾਂ ਇੱਕ ਐਪ ਨੂੰ ਹਟਾਉਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ। + ਤੁਸੀਂ %1$d ਤੋਂ ਵੱਧ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪਾਂ ਸ਼ਾਮਲ ਨਹੀਂ ਕਰ ਸਕਦੇ। ਪਹਿਲਾਂ ਇੱਕ ਐਪ ਨੂੰ ਹਟਾਉਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ। - ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਹਟਾਈ ਗਈ + ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਹਟਾਈ ਗਈ - + - ਇੱਕ ਨਾਮ ਚੁਣੋ + ਇੱਕ ਨਾਮ ਚੁਣੋ - ਇਸ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਲਈ ਇੱਕ ਵਿਲੱਖਣ ਨਾਮ ਚੁਣੋ। + ਇਸ ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਲਈ ਇੱਕ ਵਿਲੱਖਣ ਨਾਮ ਚੁਣੋ। - ਇਸ ਪ੍ਰਮਾਣੀਕਰਣ ਐਪ ਲਈ ਇੱਕ ਵਿਲੱਖਣ ਨਾਮ ਚੁਣੋ ਤਾਂ ਜੋ ਤੁਸੀਂ ਇਸਨੂੰ ਬਾਅਦ ਵਿੱਚ ਪਛਾਣ ਸਕੋ। + ਇਸ ਪ੍ਰਮਾਣੀਕਰਣ ਐਪ ਲਈ ਇੱਕ ਵਿਲੱਖਣ ਨਾਮ ਚੁਣੋ ਤਾਂ ਜੋ ਤੁਸੀਂ ਇਸਨੂੰ ਬਾਅਦ ਵਿੱਚ ਪਛਾਣ ਸਕੋ। - ਨਾਂ + ਨਾਂ - ਅੱਗੇ + ਅੱਗੇ - ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਸੈੱਟ ਅੱਪ + ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਸੈੱਟ ਅੱਪ - ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਦਾ ਨਾਮ ਬਦਲਿਆ ਗਿਆ + ਪ੍ਰਮਾਣੀਕਰਨ ਐਪ ਦਾ ਨਾਮ ਬਦਲਿਆ ਗਿਆ diff --git a/feature/app-settings/src/main/res/values-pl/strings.xml b/feature/app-settings/src/main/res/values-pl/strings.xml index a30dda6fd0..40e8a186a6 100644 --- a/feature/app-settings/src/main/res/values-pl/strings.xml +++ b/feature/app-settings/src/main/res/values-pl/strings.xml @@ -86,37 +86,37 @@ Dowiedz się więcej - + - Set up your authenticator app + Set up your authenticator app - Wykonaj poniższe kroki, aby skonfigurować aplikację uwierzytelniającą. + Wykonaj poniższe kroki, aby skonfigurować aplikację uwierzytelniającą. - Dowiedz się więcej + Dowiedz się więcej - Krok 1 + Krok 1 - Zainstaluj na swoim urządzeniu zaufaną aplikację uwierzytelniającą. + Zainstaluj na swoim urządzeniu zaufaną aplikację uwierzytelniającą. - Krok 2 + Krok 2 - Dotknij poniższego przycisku, aby otworzyć aplikację uwierzytelniającą i dodać swoje konto Signal. + Dotknij poniższego przycisku, aby otworzyć aplikację uwierzytelniającą i dodać swoje konto Signal. - Otwórz + Otwórz - Możesz też skopiować ten kod, aby skonfigurować aplikację ręcznie. + Możesz też skopiować ten kod, aby skonfigurować aplikację ręcznie. - Skopiuj + Skopiuj - Skopiowano do schowka + Skopiowano do schowka - Nie znaleziono aplikacji uwierzytelniającej + Nie znaleziono aplikacji uwierzytelniającej - Krok 3 + Krok 3 - Skopiuj wygenerowany kod i wróć do tego ekranu, aby kontynuować. + Skopiuj wygenerowany kod i wróć do tego ekranu, aby kontynuować. - Kontynuuj + Kontynuuj @@ -146,60 +146,60 @@ Usuń - + - Podaj kod + Podaj kod - Podaj 6-cyfrowy kod z aplikacji uwierzytelniającej. + Podaj 6-cyfrowy kod z aplikacji uwierzytelniającej. - Kod + Kod - Gotowe - + Gotowe + - Aplikacja uwierzytelniająca + Aplikacja uwierzytelniająca - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Dowiedz się więcej + Dowiedz się więcej - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - %1$s należy teraz do grupy + %1$s należy teraz do grupy - Open authenticator app options + Open authenticator app options - Zmień nazwę + Zmień nazwę - Usuń + Usuń - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Wybierz nazwę + Wybierz nazwę - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Nazwa + Nazwa - Dalej + Dalej - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-pt-rBR/strings.xml b/feature/app-settings/src/main/res/values-pt-rBR/strings.xml index 252e223775..cb2f2bce5c 100644 --- a/feature/app-settings/src/main/res/values-pt-rBR/strings.xml +++ b/feature/app-settings/src/main/res/values-pt-rBR/strings.xml @@ -80,37 +80,37 @@ Saiba mais - + - Set up your authenticator app + Set up your authenticator app - Siga estes passos para configurar seu aplicativo de autenticação. + Siga estes passos para configurar seu aplicativo de autenticação. - Saiba mais + Saiba mais - Passo 1 + Passo 1 - Instale um app de autenticação confiável no seu dispositivo. + Instale um app de autenticação confiável no seu dispositivo. - Passo 2 + Passo 2 - Abra seu app de autenticação tocando no botão abaixo para adicionar sua conta do Signal. + Abra seu app de autenticação tocando no botão abaixo para adicionar sua conta do Signal. - Abrir + Abrir - Você também pode copiar esta chave para fazer a configuração manualmente. + Você também pode copiar esta chave para fazer a configuração manualmente. - Copiar + Copiar - Copiado para a área de transferência + Copiado para a área de transferência - Nenhum app de autenticação encontrado + Nenhum app de autenticação encontrado - Passo 3 + Passo 3 - Copie o código gerado e volte aqui para continuar. + Copie o código gerado e volte aqui para continuar. - Continuar + Continuar @@ -140,60 +140,60 @@ Excluir - + - Insira seu código + Insira seu código - Insira o código de 6 dígitos do seu app de autenticação. + Insira o código de 6 dígitos do seu app de autenticação. - Código + Código - Pronto - + Pronto + - App de autenticação + App de autenticação - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Saiba mais + Saiba mais - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Adicionou \"%1$s\" + Adicionou \"%1$s\" - Open authenticator app options + Open authenticator app options - Renomear + Renomear - Excluir + Excluir - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Escolha um nome + Escolha um nome - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Nome + Nome - Próximo + Próximo - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-pt/strings.xml b/feature/app-settings/src/main/res/values-pt/strings.xml index 9b6ad28945..2efc0eebca 100644 --- a/feature/app-settings/src/main/res/values-pt/strings.xml +++ b/feature/app-settings/src/main/res/values-pt/strings.xml @@ -80,37 +80,37 @@ Saber mais - + - Configure a sua app autenticadora + Configure a sua app autenticadora - Siga estes passos para configurar a sua app autenticadora. + Siga estes passos para configurar a sua app autenticadora. - Saber mais + Saber mais - Passo 1 + Passo 1 - Instale uma app autenticadora de confiança no seu dispositivo. + Instale uma app autenticadora de confiança no seu dispositivo. - Passo 2 + Passo 2 - Abra a app autenticadora tocando no botão em baixo para adicionar a sua conta Signal. + Abra a app autenticadora tocando no botão em baixo para adicionar a sua conta Signal. - Abrir + Abrir - Ou pode copiar esta chave para configurar manualmente. + Ou pode copiar esta chave para configurar manualmente. - Copiar + Copiar - Copiado para a área de transferência + Copiado para a área de transferência - Não foi encontrada nenhuma app autenticadora + Não foi encontrada nenhuma app autenticadora - Passo 3 + Passo 3 - Copie o código que foi gerado e regresse aqui para continuar. + Copie o código que foi gerado e regresse aqui para continuar. - Continuar + Continuar @@ -140,60 +140,60 @@ Remover - + - Introduza o seu código + Introduza o seu código - Introduza o código de 6 dígitos da sua app autenticadora. + Introduza o código de 6 dígitos da sua app autenticadora. - Código + Código - Concluído - + Concluído + - App autenticadora + App autenticadora - Utilize uma app autenticadora para gerar códigos de verificação únicos + Utilize uma app autenticadora para gerar códigos de verificação únicos - Saber mais + Saber mais - Adicionar app autenticadora + Adicionar app autenticadora - Apps autenticadoras + Apps autenticadoras - Não há apps autenticadoras + Não há apps autenticadoras - Adicionado(s) \"%1$s\" + Adicionado(s) \"%1$s\" - Abrir opções da app autenticadora + Abrir opções da app autenticadora - Mudar o nome + Mudar o nome - Remover + Remover - Remover app autenticadora? + Remover app autenticadora? - Para remover este método de autenticação é necessário o código de 6 dígitos da sua app autenticadora. + Para remover este método de autenticação é necessário o código de 6 dígitos da sua app autenticadora. - Não é possível adicionar a app autenticadora + Não é possível adicionar a app autenticadora - Não pode adicionar mais de %1$d apps autenticadoras. Experimente remover uma primeiro. + Não pode adicionar mais de %1$d apps autenticadoras. Experimente remover uma primeiro. - App autenticadora removida + App autenticadora removida - + - Escolha um nome + Escolha um nome - Escolha um nome único para esta app autenticadora. + Escolha um nome único para esta app autenticadora. - Escolha um nome único para esta app autenticadora para ser mais fácil identificá-la. + Escolha um nome único para esta app autenticadora para ser mais fácil identificá-la. - Nome + Nome - Seguinte + Seguinte - Configuração da app autenticadora + Configuração da app autenticadora - App autenticadora renomeada + App autenticadora renomeada diff --git a/feature/app-settings/src/main/res/values-ro/strings.xml b/feature/app-settings/src/main/res/values-ro/strings.xml index 7055808942..f320be9608 100644 --- a/feature/app-settings/src/main/res/values-ro/strings.xml +++ b/feature/app-settings/src/main/res/values-ro/strings.xml @@ -83,37 +83,37 @@ Află mai multe - + - Set up your authenticator app + Set up your authenticator app - Urmează acești pași și setează-ți aplicația de autentificare. + Urmează acești pași și setează-ți aplicația de autentificare. - Află mai multe + Află mai multe - Pasul 1 + Pasul 1 - Instalează-ți pe dispozitiv o aplicație de autentificare în care ai încredere. + Instalează-ți pe dispozitiv o aplicație de autentificare în care ai încredere. - Pasul 2 + Pasul 2 - Deschide aplicația de autentificare atingând butonul mai jos ca să-ți adaugi contul de Signal. + Deschide aplicația de autentificare atingând butonul mai jos ca să-ți adaugi contul de Signal. - Deschide + Deschide - Sau poți să copiezi acest cod ca să o setezi manual. + Sau poți să copiezi acest cod ca să o setezi manual. - Copiază + Copiază - S-a copiat în clipboard + S-a copiat în clipboard - Nu s-a găsit nicio aplicație de autentificare + Nu s-a găsit nicio aplicație de autentificare - Pasul 3 + Pasul 3 - Copiază codul care este generat și revino aici pentru a continua. + Copiază codul care este generat și revino aici pentru a continua. - Continuare + Continuare @@ -143,60 +143,60 @@ Elimină - + - Introdu-ți codul + Introdu-ți codul - Introdu un cod de 6 cifre din aplicația de autentificare. + Introdu un cod de 6 cifre din aplicația de autentificare. - Cod + Cod - Gata - + Gata + - Aplicație de autentificare + Aplicație de autentificare - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Află mai multe + Află mai multe - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - \"%1$s\" a fost adăugat + \"%1$s\" a fost adăugat - Open authenticator app options + Open authenticator app options - Redenumește + Redenumește - Elimină + Elimină - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Alege un nume + Alege un nume - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Nume + Nume - Următorul + Următorul - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-ru/strings.xml b/feature/app-settings/src/main/res/values-ru/strings.xml index f16a86bbc7..6555dcbc24 100644 --- a/feature/app-settings/src/main/res/values-ru/strings.xml +++ b/feature/app-settings/src/main/res/values-ru/strings.xml @@ -86,37 +86,37 @@ Узнать больше - + - Set up your authenticator app + Set up your authenticator app - Следуйте этим инструкциям, чтобы настроить приложение для аутентификации. + Следуйте этим инструкциям, чтобы настроить приложение для аутентификации. - Узнать больше + Узнать больше - Шаг 1 + Шаг 1 - Установите на своё устройство надёжное приложение для аутентификации. + Установите на своё устройство надёжное приложение для аутентификации. - Шаг 2 + Шаг 2 - Откройте приложение для аутентификации, нажав на кнопку ниже, чтобы добавить свою учётную запись Signal. + Откройте приложение для аутентификации, нажав на кнопку ниже, чтобы добавить свою учётную запись Signal. - Открыть + Открыть - Или скопируйте этот ключ, чтобы настроить приложение вручную. + Или скопируйте этот ключ, чтобы настроить приложение вручную. - Копировать + Копировать - Скопировано в буфер обмена + Скопировано в буфер обмена - Приложение для аутентификации не найдено + Приложение для аутентификации не найдено - Шаг 3 + Шаг 3 - Скопируйте сгенерированный код и вернитесь сюда, чтобы продолжить. + Скопируйте сгенерированный код и вернитесь сюда, чтобы продолжить. - Продолжить + Продолжить @@ -146,60 +146,60 @@ Удалить - + - Введите свой код + Введите свой код - Введите 6-значный код из приложения для аутентификации. + Введите 6-значный код из приложения для аутентификации. - Код + Код - Готово - + Готово + - Приложение для аутентификации + Приложение для аутентификации - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Узнать больше + Узнать больше - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - «%1$s» был(-а) добавлен(-а) + «%1$s» был(-а) добавлен(-а) - Open authenticator app options + Open authenticator app options - Переименовать + Переименовать - Удалить + Удалить - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Имя + Имя - Далее + Далее - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-sk/strings.xml b/feature/app-settings/src/main/res/values-sk/strings.xml index b9266409dc..78981d58de 100644 --- a/feature/app-settings/src/main/res/values-sk/strings.xml +++ b/feature/app-settings/src/main/res/values-sk/strings.xml @@ -86,37 +86,37 @@ Zistiť viac - + - Nastavte si aplikáciu na autentifikáciu + Nastavte si aplikáciu na autentifikáciu - Postupujte podľa týchto pokynov pre nastavenie vašej aplikácie na autentifikáciu. + Postupujte podľa týchto pokynov pre nastavenie vašej aplikácie na autentifikáciu. - Zistiť viac + Zistiť viac - Krok 1 + Krok 1 - Nainštalujte si dôveryhodnú aplikáciu na autentifikáciu do svojho zariadenia. + Nainštalujte si dôveryhodnú aplikáciu na autentifikáciu do svojho zariadenia. - Krok 2 + Krok 2 - Otvorte svoju aplikáciu na autentifikáciu ťuknutím na tlačidlo nižšie a pridajte do nej svoj Signal účet. + Otvorte svoju aplikáciu na autentifikáciu ťuknutím na tlačidlo nižšie a pridajte do nej svoj Signal účet. - Otvoriť + Otvoriť - Alebo môžete skopírovať tento kód a nastaviť ju manuálne. + Alebo môžete skopírovať tento kód a nastaviť ju manuálne. - Kopírovať + Kopírovať - Skopírované do schránky + Skopírované do schránky - Nenašla sa žiadna aplikácia na autentifikáciu + Nenašla sa žiadna aplikácia na autentifikáciu - Krok 3 + Krok 3 - Skopírujte vygenerovaný kód a vráťte sa späť sem. + Skopírujte vygenerovaný kód a vráťte sa späť sem. - Pokračovať + Pokračovať @@ -146,60 +146,60 @@ Odstrániť - + - Zadajte svoj kód + Zadajte svoj kód - Zadajte 6-miestny kód z vašej aplikácie na autentifikáciu. + Zadajte 6-miestny kód z vašej aplikácie na autentifikáciu. - Kód + Kód - Hotovo - + Hotovo + - Aplikácia na autentifikáciu + Aplikácia na autentifikáciu - Nastavte aplikáciu na autentifikáciu na generovanie jednorazových overovacích kódov + Nastavte aplikáciu na autentifikáciu na generovanie jednorazových overovacích kódov - Zistiť viac + Zistiť viac - Pridať aplikáciu na autentifikáciu + Pridať aplikáciu na autentifikáciu - Aplikácie na autentifikáciu + Aplikácie na autentifikáciu - Žiadne aplikácie na autentifikáciu + Žiadne aplikácie na autentifikáciu - Pridaný „%1$s“ + Pridaný „%1$s“ - Otvoriť možnosti aplikácie na autentifikáciu + Otvoriť možnosti aplikácie na autentifikáciu - Premenovať + Premenovať - Odstrániť + Odstrániť - Odstrániť aplikáciu na autentifikáciu? + Odstrániť aplikáciu na autentifikáciu? - Na odstránenie tejto metódy overovania je potrebný 6-miestny kód z vašej aplikácie na autentifikáciu. + Na odstránenie tejto metódy overovania je potrebný 6-miestny kód z vašej aplikácie na autentifikáciu. - Nie je možné pridať aplikáciu na autentifikáciu + Nie je možné pridať aplikáciu na autentifikáciu - Nemôžete pridať viac ako %1$d aplikácie na autentifikáciu. Skúste najprv jednu odstrániť. + Nemôžete pridať viac ako %1$d aplikácie na autentifikáciu. Skúste najprv jednu odstrániť. - Aplikácia na autentifikáciu bola odstránená + Aplikácia na autentifikáciu bola odstránená - + - Zvoliť názov + Zvoliť názov - Vyberte si jedinečný názov pre túto aplikáciu na autentifikáciu. + Vyberte si jedinečný názov pre túto aplikáciu na autentifikáciu. - Vyberte si jedinečný názov pre túto aplikáciu na autentifikáciu, aby ste ju mohli ľahšie identifikovať. + Vyberte si jedinečný názov pre túto aplikáciu na autentifikáciu, aby ste ju mohli ľahšie identifikovať. - Názov + Názov - Ďalej + Ďalej - Aplikácia na autentifikáciu bola nastavená + Aplikácia na autentifikáciu bola nastavená - Aplikácia na autentifikáciu bola premenovaná + Aplikácia na autentifikáciu bola premenovaná diff --git a/feature/app-settings/src/main/res/values-sl/strings.xml b/feature/app-settings/src/main/res/values-sl/strings.xml index 9f69c70a3f..350e6b0ed3 100644 --- a/feature/app-settings/src/main/res/values-sl/strings.xml +++ b/feature/app-settings/src/main/res/values-sl/strings.xml @@ -86,37 +86,37 @@ Več o tem - + - Set up your authenticator app + Set up your authenticator app - Sledite tem korakom, da vzpostavite aplikacijo za preverjanje pristnosti. + Sledite tem korakom, da vzpostavite aplikacijo za preverjanje pristnosti. - Več o tem + Več o tem - 1. korak + 1. korak - Namestite zaupanja vredno aplikacijo za preverjanje pristnosti v svojo napravo. + Namestite zaupanja vredno aplikacijo za preverjanje pristnosti v svojo napravo. - 2. korak + 2. korak - Odprite svojo aplikacijo za preverjanje pristnosti tako, da tapnete spodnji gumb, in dodajte svoj račun Signal. + Odprite svojo aplikacijo za preverjanje pristnosti tako, da tapnete spodnji gumb, in dodajte svoj račun Signal. - Odpri + Odpri - Lahko pa ta ključ kopirate in jo ročno vzpostavite. + Lahko pa ta ključ kopirate in jo ročno vzpostavite. - Kopiraj + Kopiraj - Kopirano v odložišče + Kopirano v odložišče - Aplikacije za preverjanje pristnosti ni bilo mogoče najti + Aplikacije za preverjanje pristnosti ni bilo mogoče najti - 3. korak + 3. korak - Kopirajte ustvarjeno kodo in se vrnite sem, da nadaljujete. + Kopirajte ustvarjeno kodo in se vrnite sem, da nadaljujete. - Nadaljuj + Nadaljuj @@ -146,60 +146,60 @@ Odstrani - + - Vnesite svojo kodo + Vnesite svojo kodo - Vnesite 6-mestno kodo iz svoje aplikacije za preverjanje pristnosti. + Vnesite 6-mestno kodo iz svoje aplikacije za preverjanje pristnosti. - Koda + Koda - OK - + OK + - Aplikacija za preverjanje pristnosti + Aplikacija za preverjanje pristnosti - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Več o tem + Več o tem - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Dodan_a uporabnik_ca \"%1$s\" + Dodan_a uporabnik_ca \"%1$s\" - Open authenticator app options + Open authenticator app options - Rename + Rename - Odstrani + Odstrani - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Ime + Ime - Naprej + Naprej - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-sq/strings.xml b/feature/app-settings/src/main/res/values-sq/strings.xml index 6623f185d5..ff233ccbe8 100644 --- a/feature/app-settings/src/main/res/values-sq/strings.xml +++ b/feature/app-settings/src/main/res/values-sq/strings.xml @@ -80,37 +80,37 @@ Mëso më shumë - + - Konfiguro aplikacionin e autentifikimit + Konfiguro aplikacionin e autentifikimit - Ndiq këto hapa për të konfiguruar aplikacionin e autentifikimit. + Ndiq këto hapa për të konfiguruar aplikacionin e autentifikimit. - Mëso më shumë + Mëso më shumë - Hapi 1 + Hapi 1 - Instalo një aplikacion të besueshëm autentifikimi në pajisje. + Instalo një aplikacion të besueshëm autentifikimi në pajisje. - Hapi 2 + Hapi 2 - Hap aplikacionin e autentifikimit duke shtypur butonin më poshtë për të shtuar llogarinë e Signal. + Hap aplikacionin e autentifikimit duke shtypur butonin më poshtë për të shtuar llogarinë e Signal. - Hap + Hap - Ose mund ta kopjosh kodin për ta vendosur manualisht. + Ose mund ta kopjosh kodin për ta vendosur manualisht. - Kopjoje + Kopjoje - U kopjua në kujtesën e fragmenteve + U kopjua në kujtesën e fragmenteve - Nuk u gjet asnjë aplikacion autentifikimi + Nuk u gjet asnjë aplikacion autentifikimi - Hapi 3 + Hapi 3 - Kopjo kodin e gjeneruar dhe kthehu këtu për të vazhduar. + Kopjo kodin e gjeneruar dhe kthehu këtu për të vazhduar. - Vazhdo + Vazhdo @@ -140,60 +140,60 @@ Hiqe - + - Vendos kodin + Vendos kodin - Vendos kodin 6-shifror nga aplikacioni i autentifikimit. + Vendos kodin 6-shifror nga aplikacioni i autentifikimit. - Kodi + Kodi - U bë - + U bë + - Aplikacioni i autentifikimit + Aplikacioni i autentifikimit - Përdor aplikacionin e autentifikimit për të gjeneruar kode verifikimi njëpërdorimshme + Përdor aplikacionin e autentifikimit për të gjeneruar kode verifikimi njëpërdorimshme - Mëso më shumë + Mëso më shumë - Shto aplikacionin e autentifikimit + Shto aplikacionin e autentifikimit - Aplikacionet e autentifikimit + Aplikacionet e autentifikimit - Nuk ka aplikacione autentifikimi + Nuk ka aplikacione autentifikimi - U shtua \"%1$s\" + U shtua \"%1$s\" - Hap opsionet e aplikacionit të autentifikimit + Hap opsionet e aplikacionit të autentifikimit - Riemërto + Riemërto - Hiqe + Hiqe - Të hiqet aplikacioni i autentifikimit? + Të hiqet aplikacioni i autentifikimit? - Për të hequr këtë metodë autentifikimi, kërkohet kodi 6-shifror nga aplikacioni i autentifikimit. + Për të hequr këtë metodë autentifikimi, kërkohet kodi 6-shifror nga aplikacioni i autentifikimit. - Nuk mund të shtohet aplikacioni i autentifikimit + Nuk mund të shtohet aplikacioni i autentifikimit - Nuk mund të shtosh më shumë se %1$d aplikacione autentifikimi. Provo fillimisht të heqësh një. + Nuk mund të shtosh më shumë se %1$d aplikacione autentifikimi. Provo fillimisht të heqësh një. - Aplikacioni i autentifikimit u hoq + Aplikacioni i autentifikimit u hoq - + - Zgjidh një emër + Zgjidh një emër - Zgjidh një emër unik për këtë aplikacion autentifikimi. + Zgjidh një emër unik për këtë aplikacion autentifikimi. - Zgjidh një emër unik për këtë aplikacion autentifikimi, që ta identifikosh më lehtë më vonë. + Zgjidh një emër unik për këtë aplikacion autentifikimi, që ta identifikosh më lehtë më vonë. - Emër + Emër - Tjetër + Tjetër - Konfigurimi i aplikacionit të autentifikimit + Konfigurimi i aplikacionit të autentifikimit - Aplikacioni i autentifikimit u riemërtua + Aplikacioni i autentifikimit u riemërtua diff --git a/feature/app-settings/src/main/res/values-sr/strings.xml b/feature/app-settings/src/main/res/values-sr/strings.xml index bcd498614d..8a43b62b46 100644 --- a/feature/app-settings/src/main/res/values-sr/strings.xml +++ b/feature/app-settings/src/main/res/values-sr/strings.xml @@ -80,37 +80,37 @@ Сазнајте више - + - Подешавање апликације за аутентификацију + Подешавање апликације за аутентификацију - Пратите ове кораке ради успостављања апликације за аутентификацију. + Пратите ове кораке ради успостављања апликације за аутентификацију. - Сазнајте више + Сазнајте више - Корак 1 + Корак 1 - Инсталирајте поуздану апликацију за аутентификацију на телефону. + Инсталирајте поуздану апликацију за аутентификацију на телефону. - Корак 2 + Корак 2 - Отворите апликацију за аутентификацију додиром на дугме испод да додате налог на Signal-у. + Отворите апликацију за аутентификацију додиром на дугме испод да додате налог на Signal-у. - Отвори + Отвори - Или копирајте овај кључ да подесите ручно. + Или копирајте овај кључ да подесите ручно. - Копирај + Копирај - Копирано + Копирано - Није пронађена апликација за аутентификацију + Није пронађена апликација за аутентификацију - Корак 3 + Корак 3 - Копирајте генерисани кôд и вратите се овде да наставите. + Копирајте генерисани кôд и вратите се овде да наставите. - Настави + Настави @@ -140,60 +140,60 @@ Уклони - + - Унесите кôд + Унесите кôд - Унесите 6-цифрени кôд са апликације за аутентификацију. + Унесите 6-цифрени кôд са апликације за аутентификацију. - Кôд + Кôд - Готово - + Готово + - Апликација за аутентификацију + Апликација за аутентификацију - Помоћу апликације за аутентификацију можете генерисати једнократне верификационе кодове + Помоћу апликације за аутентификацију можете генерисати једнократне верификационе кодове - Сазнајте више + Сазнајте више - Додај апликацију за аутентификацију + Додај апликацију за аутентификацију - Апликације за аутентификацију + Апликације за аутентификацију - Нема апликација за аутентификацију + Нема апликација за аутентификацију - Додат је корисник %1$s + Додат је корисник %1$s - Отвори опције апликације за аутентификацију + Отвори опције апликације за аутентификацију - Промени назив + Промени назив - Уклони + Уклони - Желите ли да уклоните апликацију за аутентификацију? + Желите ли да уклоните апликацију за аутентификацију? - За уклањање ове методе аутентификације неопходан вам је 6-цифрени кôд са апликације за аутентификацију. + За уклањање ове методе аутентификације неопходан вам је 6-цифрени кôд са апликације за аутентификацију. - Није могуће додати апликацију за аутентификацију + Није могуће додати апликацију за аутентификацију - Није могуће додати више од %1$d апликације за аутентификацију. Прво уклоните неку. + Није могуће додати више од %1$d апликације за аутентификацију. Прво уклоните неку. - Апликација за аутентификацију је уклоњена + Апликација за аутентификацију је уклоњена - + - Изаберите назив + Изаберите назив - Изаберите неискоришћено име за ову апликацију за аутентификацију. + Изаберите неискоришћено име за ову апликацију за аутентификацију. - Изаберите неискоришћено име за ову апликацију за аутентификацију како бисте је лакше проналазили касније. + Изаберите неискоришћено име за ову апликацију за аутентификацију како бисте је лакше проналазили касније. - Име + Име - Следеће + Следеће - Апликација за аутентификацију је постављена + Апликација за аутентификацију је постављена - Апликацији за аутентификацију је промењено име + Апликацији за аутентификацију је промењено име diff --git a/feature/app-settings/src/main/res/values-sv/strings.xml b/feature/app-settings/src/main/res/values-sv/strings.xml index 071b5cb296..e6c3e55f66 100644 --- a/feature/app-settings/src/main/res/values-sv/strings.xml +++ b/feature/app-settings/src/main/res/values-sv/strings.xml @@ -80,37 +80,37 @@ Läs mer - + - Set up your authenticator app + Set up your authenticator app - Följ de här stegen för att konfigurera din autentiseringsapp. + Följ de här stegen för att konfigurera din autentiseringsapp. - Läs mer + Läs mer - Steg 1 + Steg 1 - Installera en betrodd autentiseringsapp på din enhet. + Installera en betrodd autentiseringsapp på din enhet. - Steg 2 + Steg 2 - Öppna din autentiseringsapp genom att trycka på knappen nedan för att lägga till ditt Signal-konto. + Öppna din autentiseringsapp genom att trycka på knappen nedan för att lägga till ditt Signal-konto. - Öppna + Öppna - Eller så kan du kopiera den här nyckeln för att ställa in den manuellt. + Eller så kan du kopiera den här nyckeln för att ställa in den manuellt. - Kopiera + Kopiera - Kopierad till urklipp + Kopierad till urklipp - Ingen autentiseringsapp hittades + Ingen autentiseringsapp hittades - Steg 3 + Steg 3 - Kopiera koden som genereras och kom tillbaka hit för att fortsätta. + Kopiera koden som genereras och kom tillbaka hit för att fortsätta. - Fortsätt + Fortsätt @@ -140,60 +140,60 @@ Ta bort - + - Ange din kod + Ange din kod - Ange den 6-siffriga koden från din autentiseringsapp. + Ange den 6-siffriga koden från din autentiseringsapp. - Kod + Kod - Klar - + Klar + - Autentiseringsapp + Autentiseringsapp - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Läs mer + Läs mer - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Lade till \"%1$s\" + Lade till \"%1$s\" - Open authenticator app options + Open authenticator app options - Ändra namn + Ändra namn - Ta bort + Ta bort - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Välj ett namn + Välj ett namn - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Namn + Namn - Nästa + Nästa - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-sw/strings.xml b/feature/app-settings/src/main/res/values-sw/strings.xml index 364252adeb..e259df44ef 100644 --- a/feature/app-settings/src/main/res/values-sw/strings.xml +++ b/feature/app-settings/src/main/res/values-sw/strings.xml @@ -80,37 +80,37 @@ Jifunze zaidi - + - Weka mipangilio yako ya programu ya uthibitishaji + Weka mipangilio yako ya programu ya uthibitishaji - Fuata hatua hizi ili uweke programu yako ya kithibitishaji. + Fuata hatua hizi ili uweke programu yako ya kithibitishaji. - Jifunze zaidi + Jifunze zaidi - Hatua ya 1 + Hatua ya 1 - Weka programu ya kithibitishaji kwenye kifaa chako. + Weka programu ya kithibitishaji kwenye kifaa chako. - Hatua ya 2 + Hatua ya 2 - Fungua programu yako ya kithibitishaji kwa kugusa kitufe hapo chini ili uweke akaunti yako ya Signal. + Fungua programu yako ya kithibitishaji kwa kugusa kitufe hapo chini ili uweke akaunti yako ya Signal. - Fungua + Fungua - Au unaweza kunakili ufunguo huu ili uweke mwenyewe mipangilio. + Au unaweza kunakili ufunguo huu ili uweke mwenyewe mipangilio. - Nakili + Nakili - Imenakiliwa kwenye bodi ya kunakili + Imenakiliwa kwenye bodi ya kunakili - Programu ya kithibitishaji haijapatikana + Programu ya kithibitishaji haijapatikana - Hatua ya 3 + Hatua ya 3 - Nakili code iliyotayarishwa kisha rudi hapa ili uendelee. + Nakili code iliyotayarishwa kisha rudi hapa ili uendelee. - Endelea + Endelea @@ -140,60 +140,60 @@ Ondoa - + - Weka code yako + Weka code yako - Weka code ya tarakimu 6 kutoka kwenye programu yako ya kithibitishaji. + Weka code ya tarakimu 6 kutoka kwenye programu yako ya kithibitishaji. - Msimbo + Msimbo - Imekamilika - + Imekamilika + - Programu ya kithibitishaji + Programu ya kithibitishaji - Weka mipangilio ya programu ya uthibitishaji kutayarisha code za uthibitishaji wa mara moja + Weka mipangilio ya programu ya uthibitishaji kutayarisha code za uthibitishaji wa mara moja - Jifunze zaidi + Jifunze zaidi - Weka programu ya uthibitishaji + Weka programu ya uthibitishaji - Programu ya uthibitishaji + Programu ya uthibitishaji - Hakuna programu za uthibitishaji + Hakuna programu za uthibitishaji - Umemwongeza \"%1$s\" + Umemwongeza \"%1$s\" - Fungua chaguo za programu ya uthibitishaji + Fungua chaguo za programu ya uthibitishaji - Badilisha jina + Badilisha jina - Ondoa + Ondoa - Ungependa kuondoa programu ya uthibitishaji? + Ungependa kuondoa programu ya uthibitishaji? - Ili uondoe mbinu hii ya uthibitishaji utahitaji code ya tarakimu 6 ya programu yako ya uthibitishaji. + Ili uondoe mbinu hii ya uthibitishaji utahitaji code ya tarakimu 6 ya programu yako ya uthibitishaji. - Haiwezi kuweka programu ya uthibitishaji + Haiwezi kuweka programu ya uthibitishaji - Huwezi kuweka zaidi ya programu za uthibitishaji %1$d. Jaribu kuondoa moja kwanza. + Huwezi kuweka zaidi ya programu za uthibitishaji %1$d. Jaribu kuondoa moja kwanza. - Programu ya uthibitishaji imeondolewa + Programu ya uthibitishaji imeondolewa - + - Chagua jina + Chagua jina - Chagua jina la kipekee kwa programu hii ya uthibitishaji. + Chagua jina la kipekee kwa programu hii ya uthibitishaji. - Chagua jina la kipekee kwa programu hii ya uthibitishaji ikusaidie kuitambua baadae. + Chagua jina la kipekee kwa programu hii ya uthibitishaji ikusaidie kuitambua baadae. - Jina + Jina - Ifuatayo + Ifuatayo - Umeweka mipangilio ya programu ya uthibitishaji + Umeweka mipangilio ya programu ya uthibitishaji - Umebadili jina la programu ya uthibitishaji + Umebadili jina la programu ya uthibitishaji diff --git a/feature/app-settings/src/main/res/values-ta/strings.xml b/feature/app-settings/src/main/res/values-ta/strings.xml index f2c3ab7e1f..4edccd0167 100644 --- a/feature/app-settings/src/main/res/values-ta/strings.xml +++ b/feature/app-settings/src/main/res/values-ta/strings.xml @@ -80,37 +80,37 @@ மேலும் அறிக - + - உங்கள் அங்கீகரிப்புச் செயலியை அமைக்கவும் + உங்கள் அங்கீகரிப்புச் செயலியை அமைக்கவும் - உங்களது அங்கீகரிப்புச் செயலியை அமைக்க, இந்தப் படிகளைப் பின்பற்றவும். + உங்களது அங்கீகரிப்புச் செயலியை அமைக்க, இந்தப் படிகளைப் பின்பற்றவும். - மேலும் அறிக + மேலும் அறிக - படி 1 + படி 1 - உங்கள் சாதனத்தில் நம்பகமான அங்கீகரிப்புச் செயலியை நிறுவவும். + உங்கள் சாதனத்தில் நம்பகமான அங்கீகரிப்புச் செயலியை நிறுவவும். - படி 2 + படி 2 - உங்கள் சிக்னல் கணக்கைச் சேர்க்க, கீழுள்ள பட்டனைத் தட்டி உங்களது அங்கீகரிப்புச் செயலியைத் திறக்கவும். + உங்கள் சிக்னல் கணக்கைச் சேர்க்க, கீழுள்ள பட்டனைத் தட்டி உங்களது அங்கீகரிப்புச் செயலியைத் திறக்கவும். - திற + திற - அல்லது, நீங்களே கைமுறையாக அமைப்பதற்கு இந்தக் குறியீட்டை நகலெடுக்கலாம். + அல்லது, நீங்களே கைமுறையாக அமைப்பதற்கு இந்தக் குறியீட்டை நகலெடுக்கலாம். - நகல் + நகல் - கிளிப்போர்டுக்கு நகலெடுக்கப்பட்டது + கிளிப்போர்டுக்கு நகலெடுக்கப்பட்டது - அங்கீகரிப்புச் செயலி எதுவும் கிடைக்கவில்லை + அங்கீகரிப்புச் செயலி எதுவும் கிடைக்கவில்லை - படி 3 + படி 3 - உருவாக்கப்படும் குறியீட்டை நகலெடுத்துவிட்டு, தொடர இங்கே திரும்பவும். + உருவாக்கப்படும் குறியீட்டை நகலெடுத்துவிட்டு, தொடர இங்கே திரும்பவும். - தொடர் + தொடர் @@ -140,60 +140,60 @@ அகற்று - + - உங்கள் குறியீட்டை உள்ளிடவும் + உங்கள் குறியீட்டை உள்ளிடவும் - உங்கள் அங்கீகரிப்புச் செயலியில் உள்ள 6 இலக்கக் குறியீட்டை உள்ளிடவும். + உங்கள் அங்கீகரிப்புச் செயலியில் உள்ள 6 இலக்கக் குறியீட்டை உள்ளிடவும். - குறியீடு + குறியீடு - முடிந்தது - + முடிந்தது + - அங்கீகரிப்புச் செயலி + அங்கீகரிப்புச் செயலி - ஒருமுறை மட்டுமே பயன்படுத்தக்கூடிய சரிபார்ப்புக் குறியீடுகளை உருவாக்க, அங்கீகரிப்புச் செயலியை அமைக்கவும் + ஒருமுறை மட்டுமே பயன்படுத்தக்கூடிய சரிபார்ப்புக் குறியீடுகளை உருவாக்க, அங்கீகரிப்புச் செயலியை அமைக்கவும் - மேலும் அறிக + மேலும் அறிக - அங்கீகரிப்புச் செயலியைச் சேர் + அங்கீகரிப்புச் செயலியைச் சேர் - அங்கீகரிப்புச் செயலிகள் + அங்கீகரிப்புச் செயலிகள் - அங்கீகரிப்புச் செயலிகள் எதுவும் இல்லை + அங்கீகரிப்புச் செயலிகள் எதுவும் இல்லை - சேர்க்கப்பட்டது \"%1$s\" + சேர்க்கப்பட்டது \"%1$s\" - அங்கீகரிப்புச் செயலி விருப்பங்களைத் திறக்கவும் + அங்கீகரிப்புச் செயலி விருப்பங்களைத் திறக்கவும் - மறுபெயரிடுக + மறுபெயரிடுக - அகற்று + அகற்று - அங்கீகரிப்புச் செயலியை அகற்றவா? + அங்கீகரிப்புச் செயலியை அகற்றவா? - இந்த அங்கீகார முறையை அகற்ற, உங்களது அங்கீகரிப்புச் செயலியிலிருந்து 6-இலக்கக் குறியீடு தேவை. + இந்த அங்கீகார முறையை அகற்ற, உங்களது அங்கீகரிப்புச் செயலியிலிருந்து 6-இலக்கக் குறியீடு தேவை. - அங்கீகரிப்புச் செயலியைச் சேர்க்க முடியவில்லை + அங்கீகரிப்புச் செயலியைச் சேர்க்க முடியவில்லை - %1$d அங்கீகரிப்புச் செயலிகளுக்கு மேல் உங்களால் சேர்க்க முடியாது. முதலில் ஒன்றை அகற்றிவிட்டு முயலவும். + %1$d அங்கீகரிப்புச் செயலிகளுக்கு மேல் உங்களால் சேர்க்க முடியாது. முதலில் ஒன்றை அகற்றிவிட்டு முயலவும். - அங்கீகரிப்புச் செயலி அகற்றப்பட்டது + அங்கீகரிப்புச் செயலி அகற்றப்பட்டது - + - பெயரைத் தேர்வுசெய்யவும் + பெயரைத் தேர்வுசெய்யவும் - இந்த அங்கீகரிப்புச் செயலிக்குத் தனித்துவமான பெயரைத் தேர்வுசெய்யவும். + இந்த அங்கீகரிப்புச் செயலிக்குத் தனித்துவமான பெயரைத் தேர்வுசெய்யவும். - இந்த அங்கீகரிப்புச் செயலியைப் பின்னர் எளிதாக அடையாளம் காண, இதற்குத் தனித்துவமான ஒரு பெயரைத் தேர்வுசெய்யவும். + இந்த அங்கீகரிப்புச் செயலியைப் பின்னர் எளிதாக அடையாளம் காண, இதற்குத் தனித்துவமான ஒரு பெயரைத் தேர்வுசெய்யவும். - பெயர் + பெயர் - அடுத்தது + அடுத்தது - அங்கீகரிப்புச் செயலி அமைவு + அங்கீகரிப்புச் செயலி அமைவு - அங்கீகரிப்புச் செயலிக்கு மறுபெயரிடப்பட்டது + அங்கீகரிப்புச் செயலிக்கு மறுபெயரிடப்பட்டது diff --git a/feature/app-settings/src/main/res/values-te/strings.xml b/feature/app-settings/src/main/res/values-te/strings.xml index 1b5f502df6..becdab042c 100644 --- a/feature/app-settings/src/main/res/values-te/strings.xml +++ b/feature/app-settings/src/main/res/values-te/strings.xml @@ -80,37 +80,37 @@ మరింత తెలుసుకోండి - + - మీ అథెంటికేటర్ యాప్‌ను సెటప్ చేయండి + మీ అథెంటికేటర్ యాప్‌ను సెటప్ చేయండి - మీ అథెంటికేటర్ యాప్‌ను సెటప్ చేయడానికి ఈ దశలను అనుసరించండి. + మీ అథెంటికేటర్ యాప్‌ను సెటప్ చేయడానికి ఈ దశలను అనుసరించండి. - మరింత తెలుసుకోండి + మరింత తెలుసుకోండి - దశ 1 + దశ 1 - మీ పరికరంలో విశ్వసనీయమైన అథెంటికేటర్ యాప్‌ను ఇన్‌స్టాల్ చేయండి. + మీ పరికరంలో విశ్వసనీయమైన అథెంటికేటర్ యాప్‌ను ఇన్‌స్టాల్ చేయండి. - దశ 2 + దశ 2 - మీ Signal ఖాతాను జోడించడానికి, కింద ఉన్న బటన్‌ను తట్టడం ద్వారా మీ అథెంటికేటర్ యాప్‌ను తెరవండి. + మీ Signal ఖాతాను జోడించడానికి, కింద ఉన్న బటన్‌ను తట్టడం ద్వారా మీ అథెంటికేటర్ యాప్‌ను తెరవండి. - తెరవండి + తెరవండి - లేదా, దీన్ని మాన్యువల్‌గా సెటప్ చేయడానికి మీరు ఈ కీని కాపీ చేయవచ్చు. + లేదా, దీన్ని మాన్యువల్‌గా సెటప్ చేయడానికి మీరు ఈ కీని కాపీ చేయవచ్చు. - నకలు + నకలు - క్లిప్‌బోర్డ్‌కు కాపీ చేయబడింది + క్లిప్‌బోర్డ్‌కు కాపీ చేయబడింది - ఎటువంటి అథెంటికేటర్ యాప్ కనుగొనబడలేదు + ఎటువంటి అథెంటికేటర్ యాప్ కనుగొనబడలేదు - దశ 3 + దశ 3 - ఉత్పత్తి అయిన కోడ్‌ను కాపీ చేసి, కొనసాగించడానికి ఇక్కడికి తిరిగి రండి. + ఉత్పత్తి అయిన కోడ్‌ను కాపీ చేసి, కొనసాగించడానికి ఇక్కడికి తిరిగి రండి. - కొనసాగించు + కొనసాగించు @@ -140,60 +140,60 @@ తొలగించండి - + - మీ కోడ్‌ను ఎంటర్ చేయండి + మీ కోడ్‌ను ఎంటర్ చేయండి - మీ అథెంటికేటర్ యాప్ నుండి 6-అంకెల కోడ్‌ను ఎంటర్ చేయండి. + మీ అథెంటికేటర్ యాప్ నుండి 6-అంకెల కోడ్‌ను ఎంటర్ చేయండి. - కోడ్ + కోడ్ - పూర్తయింది - + పూర్తయింది + - అథెంటికేటర్ యాప్ + అథెంటికేటర్ యాప్ - ఒకసారి ధృవీకరణ కోడ్‌లను ఉత్పత్తి చేయడానికి అథెంటికేటర్ యాప్‌ను సెటప్ చేయండి + ఒకసారి ధృవీకరణ కోడ్‌లను ఉత్పత్తి చేయడానికి అథెంటికేటర్ యాప్‌ను సెటప్ చేయండి - మరింత తెలుసుకోండి + మరింత తెలుసుకోండి - అథెంటికేటర్ యాప్‌ను జోడించండి + అథెంటికేటర్ యాప్‌ను జోడించండి - అథెంటికేటర్ యాప్‌లు + అథెంటికేటర్ యాప్‌లు - ఎటువంటి అథెంటికేటర్ యాప్‌లు లేవు + ఎటువంటి అథెంటికేటర్ యాప్‌లు లేవు - \"8%1$sజోడించబడింది + \"8%1$sజోడించబడింది - అథెంటికేటర్ యాప్ ఎంపికలను తెరవండి + అథెంటికేటర్ యాప్ ఎంపికలను తెరవండి - పేరు మార్చండి + పేరు మార్చండి - తొలగించండి + తొలగించండి - అథెంటికేటర్ యాప్‌ను తొలగించాలా? + అథెంటికేటర్ యాప్‌ను తొలగించాలా? - ఈ ప్రమాణీకరణ పద్ధతిని తొలగించడానికి, మీ అథెంటికేటర్ యాప్ నుండి 6-అంకెల కోడ్ అవసరం. + ఈ ప్రమాణీకరణ పద్ధతిని తొలగించడానికి, మీ అథెంటికేటర్ యాప్ నుండి 6-అంకెల కోడ్ అవసరం. - అథెంటికేటర్ యాప్‌ను జోడించలేకపోతున్నాను + అథెంటికేటర్ యాప్‌ను జోడించలేకపోతున్నాను - మీరు %1$d కంటే ఎక్కువ అథెంటికేటర్ యాప్‌లను జోడించలేరు. ముందుగా ఒకదాన్ని తీసివేయడానికి ప్రయత్నించండి. + మీరు %1$d కంటే ఎక్కువ అథెంటికేటర్ యాప్‌లను జోడించలేరు. ముందుగా ఒకదాన్ని తీసివేయడానికి ప్రయత్నించండి. - అథెంటికేటర్ యాప్ తొలగించబడింది + అథెంటికేటర్ యాప్ తొలగించబడింది - + - ఒక పేరును ఎంచుకోండి + ఒక పేరును ఎంచుకోండి - ఈ అథెంటికేటర్ యాప్ కోసం ఒక ప్రత్యేకమైన పేరును ఎంచుకోండి. + ఈ అథెంటికేటర్ యాప్ కోసం ఒక ప్రత్యేకమైన పేరును ఎంచుకోండి. - దానిని తర్వాత గుర్తించడంలో మీకు సహాయపడటానికి, ఈ అథెంటికేటర్ యాప్ కోసం ఒక ప్రత్యేకమైన పేరును ఎంచుకోండి. + దానిని తర్వాత గుర్తించడంలో మీకు సహాయపడటానికి, ఈ అథెంటికేటర్ యాప్ కోసం ఒక ప్రత్యేకమైన పేరును ఎంచుకోండి. - పేరు + పేరు - తరువాత + తరువాత - అథెంటికేటర్ యాప్ సెటప్ + అథెంటికేటర్ యాప్ సెటప్ - అథెంటికేటర్ యాప్ పేరు మార్చబడింది + అథెంటికేటర్ యాప్ పేరు మార్చబడింది diff --git a/feature/app-settings/src/main/res/values-th/strings.xml b/feature/app-settings/src/main/res/values-th/strings.xml index c601953985..284941f096 100644 --- a/feature/app-settings/src/main/res/values-th/strings.xml +++ b/feature/app-settings/src/main/res/values-th/strings.xml @@ -77,37 +77,37 @@ เรียนรู้เพิ่มเติม - + - ตั้งค่าแอปยืนยันตัวตนของคุณ + ตั้งค่าแอปยืนยันตัวตนของคุณ - โปรดทำตามขั้นตอนต่อไปนี้เพื่อตั้งค่าแอปยืนยันตัวตนของคุณ + โปรดทำตามขั้นตอนต่อไปนี้เพื่อตั้งค่าแอปยืนยันตัวตนของคุณ - เรียนรู้เพิ่มเติม + เรียนรู้เพิ่มเติม - ขั้นตอนที่ 1 + ขั้นตอนที่ 1 - ติดตั้งแอปยืนยันตัวตนที่คุณมั่นใจว่าปลอดภัยบนอุปกรณ์ + ติดตั้งแอปยืนยันตัวตนที่คุณมั่นใจว่าปลอดภัยบนอุปกรณ์ - ขั้นตอนที่ 2 + ขั้นตอนที่ 2 - เปิดแอปยืนยันตัวตนนั้นโดยแตะที่ปุ่มด้านล่างเพื่อเพิ่มบัญชี Signal ของคุณ + เปิดแอปยืนยันตัวตนนั้นโดยแตะที่ปุ่มด้านล่างเพื่อเพิ่มบัญชี Signal ของคุณ - เปิด + เปิด - หรือคัดลอกกุญแจนี้เพื่อตั้งค่าด้วยตัวเอง + หรือคัดลอกกุญแจนี้เพื่อตั้งค่าด้วยตัวเอง - คัดลอก + คัดลอก - คัดลอกไปยังคลิปบอร์ดแล้ว + คัดลอกไปยังคลิปบอร์ดแล้ว - ไม่พบแอปยืนยันตัวตน + ไม่พบแอปยืนยันตัวตน - ขั้นตอนที่ 3 + ขั้นตอนที่ 3 - คัดลอกรหัสที่แอปสร้าง จากนั้นกลับมาที่หน้านี้เพื่อดำเนินการต่อ + คัดลอกรหัสที่แอปสร้าง จากนั้นกลับมาที่หน้านี้เพื่อดำเนินการต่อ - ทำต่อ + ทำต่อ @@ -137,60 +137,60 @@ ลบ - + - ใส่รหัสของคุณ + ใส่รหัสของคุณ - ใส่รหัส 6 หลักจากแอปยืนยันตัวตนของคุณ + ใส่รหัส 6 หลักจากแอปยืนยันตัวตนของคุณ - โค้ด + โค้ด - เสร็จสิ้น - + เสร็จสิ้น + - แอปยืนยันตัวตน (Authenticator App) + แอปยืนยันตัวตน (Authenticator App) - ตั้งค่าแอปยืนยันตัวตนเพื่อสร้างรหัสยืนยันแบบใช้ครั้งเดียว + ตั้งค่าแอปยืนยันตัวตนเพื่อสร้างรหัสยืนยันแบบใช้ครั้งเดียว - เรียนรู้เพิ่มเติม + เรียนรู้เพิ่มเติม - เพิ่มแอปยืนยันตัวตน + เพิ่มแอปยืนยันตัวตน - แอปยืนยันตัวตน + แอปยืนยันตัวตน - ไม่มีแอปยืนยันตัวตน + ไม่มีแอปยืนยันตัวตน - เพิ่ม \"%1$s\" แล้ว + เพิ่ม \"%1$s\" แล้ว - เปิดตัวเลือกแอปยืนยันตัวตน + เปิดตัวเลือกแอปยืนยันตัวตน - เปลี่ยนชื่อ + เปลี่ยนชื่อ - ลบ + ลบ - ลบแอปยืนยันตัวตนใช่หรือไม่ + ลบแอปยืนยันตัวตนใช่หรือไม่ - การลบการยืนยันตัวตนวิธีนี้จำเป็นต้องใช้รหัส 6 หลักจากแอปยืนยันตัวตนของคุณ + การลบการยืนยันตัวตนวิธีนี้จำเป็นต้องใช้รหัส 6 หลักจากแอปยืนยันตัวตนของคุณ - ไม่สามารถเพิ่มแอปยืนยันตัวตน + ไม่สามารถเพิ่มแอปยืนยันตัวตน - ไม่สามารถเพิ่มแอปยืนยันตัวตนเกิน %1$d แอป โปรดลบแอปเดิมออกหนึ่งแอป + ไม่สามารถเพิ่มแอปยืนยันตัวตนเกิน %1$d แอป โปรดลบแอปเดิมออกหนึ่งแอป - ลบแอปยืนยันตัวตนแล้ว + ลบแอปยืนยันตัวตนแล้ว - + - เลือกชื่อ + เลือกชื่อ - ตั้งชื่อเฉพาะให้แอปยืนยันตัวตนแอปนี้ + ตั้งชื่อเฉพาะให้แอปยืนยันตัวตนแอปนี้ - ตั้งชื่อเฉพาะให้แอปยืนยันตัวตนแอปนี้ที่จะช่วยให้คุณจำได้ในภายหลัง + ตั้งชื่อเฉพาะให้แอปยืนยันตัวตนแอปนี้ที่จะช่วยให้คุณจำได้ในภายหลัง - ชื่อ + ชื่อ - ดำเนินการต่อ + ดำเนินการต่อ - ตั้งค่าแอปยืนยันตัวตนแล้ว + ตั้งค่าแอปยืนยันตัวตนแล้ว - เปลี่ยนชื่อแอปยืนยันตัวตนแล้ว + เปลี่ยนชื่อแอปยืนยันตัวตนแล้ว diff --git a/feature/app-settings/src/main/res/values-tl/strings.xml b/feature/app-settings/src/main/res/values-tl/strings.xml index 4f292c12fb..4bf50dbe0c 100644 --- a/feature/app-settings/src/main/res/values-tl/strings.xml +++ b/feature/app-settings/src/main/res/values-tl/strings.xml @@ -80,37 +80,37 @@ Matuto pa - + - I-set up ang authenticator app mo + I-set up ang authenticator app mo - Sundin ang steps na ito para i-set up ang authenticator app mo. + Sundin ang steps na ito para i-set up ang authenticator app mo. - Matuto pa + Matuto pa - Step 1 + Step 1 - Mag-install ng trusted authenticator app sa device mo. + Mag-install ng trusted authenticator app sa device mo. - Step 2 + Step 2 - Buksan ang authenticator app sa pamamagitan ng pag-tap sa button sa ibaba para i-add ang Signal account mo. + Buksan ang authenticator app sa pamamagitan ng pag-tap sa button sa ibaba para i-add ang Signal account mo. - Buksan + Buksan - O kopyahin ang key na ito para i-set up ito manually. + O kopyahin ang key na ito para i-set up ito manually. - Kopyahin + Kopyahin - Kinopya sa clipboard + Kinopya sa clipboard - Walang nahanap na authenticator app + Walang nahanap na authenticator app - Step 3 + Step 3 - Kopyahin ang generated code at bumalik dito para magpatuloy. + Kopyahin ang generated code at bumalik dito para magpatuloy. - Magpatuloy + Magpatuloy @@ -140,60 +140,60 @@ Tanggalin - + - Ilagay ang code mo + Ilagay ang code mo - Ilagay ang 6-digit code mula sa authenticator app mo. + Ilagay ang 6-digit code mula sa authenticator app mo. - Code + Code - Tapos na - + Tapos na + - Authenticator app + Authenticator app - Mag-set up ng authenticator app para makagawa ng one-time verification codes + Mag-set up ng authenticator app para makagawa ng one-time verification codes - Matuto pa + Matuto pa - Mag-add ng authenticator app + Mag-add ng authenticator app - Authenticator apps + Authenticator apps - Walang authenticator app + Walang authenticator app - In-add ang \"%1$s\" + In-add ang \"%1$s\" - Buksan ang authenticator app options + Buksan ang authenticator app options - I-rename + I-rename - Tanggalin + Tanggalin - Tanggalin ang authenticator app? + Tanggalin ang authenticator app? - Para tanggalin ang authentication method na ito, kailangan ang 6-digit code mula sa iyong authenticator app. + Para tanggalin ang authentication method na ito, kailangan ang 6-digit code mula sa iyong authenticator app. - Hindi makapag-add ang authenticator app + Hindi makapag-add ang authenticator app - Hindi ka maaaring mag-add ng higit sa %1$d authenticator apps. Subukan munang magtanggal ng isa. + Hindi ka maaaring mag-add ng higit sa %1$d authenticator apps. Subukan munang magtanggal ng isa. - Tinanggal ang authenticator app + Tinanggal ang authenticator app - + - Pumil ng pangalan + Pumil ng pangalan - Pumili ng unique na pangalan para sa authenticator app na ito. + Pumili ng unique na pangalan para sa authenticator app na ito. - Pumili ng unique na pangalan para sa authenticator app na ito para madali itong matukoy sa susunod. + Pumili ng unique na pangalan para sa authenticator app na ito para madali itong matukoy sa susunod. - Pangalan + Pangalan - Susunod + Susunod - Na-set up ang authenticator app + Na-set up ang authenticator app - Ni-rename ang authenticator app + Ni-rename ang authenticator app diff --git a/feature/app-settings/src/main/res/values-tr/strings.xml b/feature/app-settings/src/main/res/values-tr/strings.xml index 0e89d14183..ed0794859b 100644 --- a/feature/app-settings/src/main/res/values-tr/strings.xml +++ b/feature/app-settings/src/main/res/values-tr/strings.xml @@ -80,37 +80,37 @@ Daha fazlasını öğren - + - Kimlik doğrulama uygulamanı kur + Kimlik doğrulama uygulamanı kur - Kimlik doğrulama uygulamanı kurmak için bu adımları izle. + Kimlik doğrulama uygulamanı kurmak için bu adımları izle. - Daha fazlasını öğren + Daha fazlasını öğren - Adım 1 + Adım 1 - Cihazına güvenilir bir kimlik doğrulama uygulaması yükle. + Cihazına güvenilir bir kimlik doğrulama uygulaması yükle. - Adım 2 + Adım 2 - Signal hesabını eklemek için aşağıdaki butona dokunarak kimlik doğrulama uygulamanı aç. + Signal hesabını eklemek için aşağıdaki butona dokunarak kimlik doğrulama uygulamanı aç. - + - Ya da manuel olarak kurmak için bu anahtarı kopyalayabilirsin. + Ya da manuel olarak kurmak için bu anahtarı kopyalayabilirsin. - Kopyala + Kopyala - Panoya kopyalandı + Panoya kopyalandı - Kimlik doğrulama uygulaması bulunamadı + Kimlik doğrulama uygulaması bulunamadı - Adım 3 + Adım 3 - Üretilen kodu kopyala ve devam etmek için buraya yapıştır. + Üretilen kodu kopyala ve devam etmek için buraya yapıştır. - Devam et + Devam et @@ -140,60 +140,60 @@ Kaldır - + - Kodunu gir + Kodunu gir - Kimlik doğrulama uygulamandaki 6 basamaklı kodu gir. + Kimlik doğrulama uygulamandaki 6 basamaklı kodu gir. - Kod + Kod - Tamam - + Tamam + - Kimlik doğrulama uygulaması + Kimlik doğrulama uygulaması - Tek kullanımlık doğrulama kodları üretmek için bir kimlik doğrulama uygulaması kur + Tek kullanımlık doğrulama kodları üretmek için bir kimlik doğrulama uygulaması kur - Daha fazlasını öğren + Daha fazlasını öğren - Kimlik doğrulama uygulaması ekle + Kimlik doğrulama uygulaması ekle - Kimlik doğrulama uygulamaları + Kimlik doğrulama uygulamaları - Kimlik doğrulama uygulaması yok + Kimlik doğrulama uygulaması yok - \"%1$s\" eklendi + \"%1$s\" eklendi - Kimlik doğrulama uygulama seçeneklerini aç + Kimlik doğrulama uygulama seçeneklerini aç - Yeniden adlandır + Yeniden adlandır - Kaldır + Kaldır - Kimlik doğrulama uygulaması kaldırılsın mı? + Kimlik doğrulama uygulaması kaldırılsın mı? - Bu kimlik doğrulama yöntemini kaldırmak için kimlik doğrulama uygulamandaki 6 basamaklı kod gerekir. + Bu kimlik doğrulama yöntemini kaldırmak için kimlik doğrulama uygulamandaki 6 basamaklı kod gerekir. - Kimlik doğrulama uygulaması eklenemiyor + Kimlik doğrulama uygulaması eklenemiyor - %1$d kimlik doğrulama uygulamasından fazlasını ekleyemezsin. Önce bir tanesini kaldırmayı dene. + %1$d kimlik doğrulama uygulamasından fazlasını ekleyemezsin. Önce bir tanesini kaldırmayı dene. - Kimlik doğrulama uygulaması kaldırıldı + Kimlik doğrulama uygulaması kaldırıldı - + - İsim seçin + İsim seçin - Bu kimlik doğrulama uygulaması için benzersiz bir isim seç. + Bu kimlik doğrulama uygulaması için benzersiz bir isim seç. - Bu kimlik doğrulama uygulamasını daha sonra kolayca hatırlayabilmek için benzersiz bir isim seç. + Bu kimlik doğrulama uygulamasını daha sonra kolayca hatırlayabilmek için benzersiz bir isim seç. - Ad + Ad - İleri + İleri - Kimlik doğrulama uygulaması kurulumu + Kimlik doğrulama uygulaması kurulumu - Kimlik doğrulama uygulaması yeniden adlandırıldı + Kimlik doğrulama uygulaması yeniden adlandırıldı diff --git a/feature/app-settings/src/main/res/values-ug/strings.xml b/feature/app-settings/src/main/res/values-ug/strings.xml index b9bd51f96d..ab6509db7f 100644 --- a/feature/app-settings/src/main/res/values-ug/strings.xml +++ b/feature/app-settings/src/main/res/values-ug/strings.xml @@ -77,37 +77,37 @@ تەپسىلاتى - + - دەلىللىگۈچى ئەپىڭىزنى سەپلەڭ + دەلىللىگۈچى ئەپىڭىزنى سەپلەڭ - دەلىللىگۈچى ئەپ دېتالىڭىزنى تەڭشەش ئۈچۈن تۆۋەندىكى قەدەملەرنى بېسىڭ. + دەلىللىگۈچى ئەپ دېتالىڭىزنى تەڭشەش ئۈچۈن تۆۋەندىكى قەدەملەرنى بېسىڭ. - تەپسىلاتى + تەپسىلاتى - 1-قەدەم + 1-قەدەم - ئىشەنچىلىك بىر دەلىللىگۈچى ئەپنى ئۈسكۈنىڭىزگە چۈشۈرۈڭ. + ئىشەنچىلىك بىر دەلىللىگۈچى ئەپنى ئۈسكۈنىڭىزگە چۈشۈرۈڭ. - 2-قەدەم + 2-قەدەم - تۆۋەندىكى كۇنۇپكىنى چېكىپ دەلىللىگۈچى ئەپنى ئېچىپ، Signal ھېساباتىڭىزنى قوشۇڭ. + تۆۋەندىكى كۇنۇپكىنى چېكىپ دەلىللىگۈچى ئەپنى ئېچىپ، Signal ھېساباتىڭىزنى قوشۇڭ. - ئېچىڭ + ئېچىڭ - ياكى بۇ ئاچقۇچنى كۆچۈرۈپ، ئۇنى قولدا تەڭشىسىڭىزمۇ بولىدۇ. + ياكى بۇ ئاچقۇچنى كۆچۈرۈپ، ئۇنى قولدا تەڭشىسىڭىزمۇ بولىدۇ. - كۆچۈر + كۆچۈر - چاپلاش تاختىسىغا كۆچۈرۈلدى + چاپلاش تاختىسىغا كۆچۈرۈلدى - دەلىللىگۈچى ئەپ تېپىلمىدى + دەلىللىگۈچى ئەپ تېپىلمىدى - 3-قەدەم + 3-قەدەم - ھاسىل قىلىنغان كودنى كۆچۈرۈپ، بۇ يەرگە قايتىپ كېلىپ داۋاملاشتۇرۇڭ. + ھاسىل قىلىنغان كودنى كۆچۈرۈپ، بۇ يەرگە قايتىپ كېلىپ داۋاملاشتۇرۇڭ. - داۋاملاشتۇر + داۋاملاشتۇر @@ -137,60 +137,60 @@ چىقىرىۋەت - + - كودىڭىزنى كىرگۈزۈڭ + كودىڭىزنى كىرگۈزۈڭ - دەلىللىگۈچى ئەپتىكى 6 خانىلىق كودنى كىرگۈزۈڭ. + دەلىللىگۈچى ئەپتىكى 6 خانىلىق كودنى كىرگۈزۈڭ. - كود + كود - تامام - + تامام + - دەلىللىگۈچى ئەپ + دەلىللىگۈچى ئەپ - بىر قېتىملا ئىشلىتىلىدىغان دەلىللەش كودلىرىنى ھاسىل قىلىدىغان بىر دەلىللىگۈچى ئەپنى سەپلەڭ + بىر قېتىملا ئىشلىتىلىدىغان دەلىللەش كودلىرىنى ھاسىل قىلىدىغان بىر دەلىللىگۈچى ئەپنى سەپلەڭ - تەپسىلاتى + تەپسىلاتى - دەلىللىگۈچى ئەپ قوشۇش + دەلىللىگۈچى ئەپ قوشۇش - دەلىللىگۈچى ئەپلەر + دەلىللىگۈچى ئەپلەر - دەلىللىگۈچى ئەپلەر يوق + دەلىللىگۈچى ئەپلەر يوق - قوشۇلغىنى %1$s + قوشۇلغىنى %1$s - دەلىللىگۈچى ئەپ تاللانمىلىرىنى ئېچىڭ + دەلىللىگۈچى ئەپ تاللانمىلىرىنى ئېچىڭ - ئىسىم ئۆزگەرتىش + ئىسىم ئۆزگەرتىش - چىقىرىۋەت + چىقىرىۋەت - دەلىللىگۈچى ئەپنى چىقىرىۋېتەمسىز؟ + دەلىللىگۈچى ئەپنى چىقىرىۋېتەمسىز؟ - بۇ دەلىللەش مېتودىنى ئۆچۈرۈش ئۈچۈن دەلىللەش ئەپىڭىزدىن ئېلىنغان 6 خانىلىق كود كېرەك. + بۇ دەلىللەش مېتودىنى ئۆچۈرۈش ئۈچۈن دەلىللەش ئەپىڭىزدىن ئېلىنغان 6 خانىلىق كود كېرەك. - دەلىللىگۈچى ئەپ قوشالمىدى + دەلىللىگۈچى ئەپ قوشالمىدى - %1$d دىن كۆپ دەلىللىگۈچى ئەپلەر قوشالمايسىز. ئالدى بىلەن بىرنى چىقىرىۋېتىشنى سىناپ بېقىڭ. + %1$d دىن كۆپ دەلىللىگۈچى ئەپلەر قوشالمايسىز. ئالدى بىلەن بىرنى چىقىرىۋېتىشنى سىناپ بېقىڭ. - دەلىللىگۈچى ئەپ چىقىرىۋېتىلدى + دەلىللىگۈچى ئەپ چىقىرىۋېتىلدى - + - ئات قويۇڭ + ئات قويۇڭ - بۇ دەلىللىگۈچى ئەپكە ئالاھىدە بىر ئىسىم قۇيۇڭ. + بۇ دەلىللىگۈچى ئەپكە ئالاھىدە بىر ئىسىم قۇيۇڭ. - بۇنى تېزراق بايقاشقا ياردىمى بولۇشى ئۈچۈن، بۇ دەلىللىگۈچى ئەپكە ئالاھىدە بىر ئىسىم قۇيۇڭ. + بۇنى تېزراق بايقاشقا ياردىمى بولۇشى ئۈچۈن، بۇ دەلىللىگۈچى ئەپكە ئالاھىدە بىر ئىسىم قۇيۇڭ. - ئىسمىڭىز + ئىسمىڭىز - كېيىنكى + كېيىنكى - دەلىللىگۈچى ئەپ سەپلەش + دەلىللىگۈچى ئەپ سەپلەش - دەلىللىگۈچى ئەپنىڭ نامىنى ئۆزگەرتىش + دەلىللىگۈچى ئەپنىڭ نامىنى ئۆزگەرتىش diff --git a/feature/app-settings/src/main/res/values-uk/strings.xml b/feature/app-settings/src/main/res/values-uk/strings.xml index fb46be62ae..a1d62b174e 100644 --- a/feature/app-settings/src/main/res/values-uk/strings.xml +++ b/feature/app-settings/src/main/res/values-uk/strings.xml @@ -86,37 +86,37 @@ Докладніше - + - Set up your authenticator app + Set up your authenticator app - Виконайте ці дії, щоб налаштувати автентифікатор. + Виконайте ці дії, щоб налаштувати автентифікатор. - Докладніше + Докладніше - Крок 1 + Крок 1 - Установіть надійний автентифікатор на свій пристрій. + Установіть надійний автентифікатор на свій пристрій. - Крок 2 + Крок 2 - Відкрийте автентифікатор, натиснувши кнопку нижче, щоб додати свій акаунт Signal. + Відкрийте автентифікатор, натиснувши кнопку нижче, щоб додати свій акаунт Signal. - Відкрити + Відкрити - Або ж скопіюйте цей ключ і налаштуйте вручну. + Або ж скопіюйте цей ключ і налаштуйте вручну. - Копіювати + Копіювати - Скопійовано в буфер обміну + Скопійовано в буфер обміну - Не знайдено автентифікатора серед застосунків + Не знайдено автентифікатора серед застосунків - Крок 3 + Крок 3 - Скопіюйте згенерований код і поверніться сюди. + Скопіюйте згенерований код і поверніться сюди. - Продовжити + Продовжити @@ -146,60 +146,60 @@ Вилучити - + - Введіть код + Введіть код - Введіть 6-значний код з автентифікатора. + Введіть 6-значний код з автентифікатора. - Код + Код - Готово - + Готово + - Автентифікатор + Автентифікатор - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Докладніше + Докладніше - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Додано користувача %1$s + Додано користувача %1$s - Open authenticator app options + Open authenticator app options - Перейменувати + Перейменувати - Вилучити + Вилучити - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Дайте назву + Дайте назву - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Назва + Назва - Далі + Далі - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-ur/strings.xml b/feature/app-settings/src/main/res/values-ur/strings.xml index c49d2a06e3..a48f572210 100644 --- a/feature/app-settings/src/main/res/values-ur/strings.xml +++ b/feature/app-settings/src/main/res/values-ur/strings.xml @@ -80,37 +80,37 @@ مزید جانیں - + - اپنی توثیق کار ایپ ترتیب دیں + اپنی توثیق کار ایپ ترتیب دیں - اپنی توثیق کار ایپ ترتیب دینے کے لیے ان مراحل پر عمل کریں۔ + اپنی توثیق کار ایپ ترتیب دینے کے لیے ان مراحل پر عمل کریں۔ - مزید جانیں + مزید جانیں - مرحلہ 1 + مرحلہ 1 - اپنی ڈیوائس پر ایک قابلِ اعتماد توثیق کار ایپ انسٹال کریں۔ + اپنی ڈیوائس پر ایک قابلِ اعتماد توثیق کار ایپ انسٹال کریں۔ - مرحلہ 2 + مرحلہ 2 - اپنا Signal اکاؤنٹ شامل کرنے کے لیے نیچے دیے گئے بٹن پر ٹیپ کر کے اپنی توثیق کار ایپ کھولیں۔ + اپنا Signal اکاؤنٹ شامل کرنے کے لیے نیچے دیے گئے بٹن پر ٹیپ کر کے اپنی توثیق کار ایپ کھولیں۔ - کھولیں + کھولیں - یا آپ اس کیی کو کاپی کر کے اسے دستی طور پر سیٹ کر سکتے ہیں۔ + یا آپ اس کیی کو کاپی کر کے اسے دستی طور پر سیٹ کر سکتے ہیں۔ - کاپی کریں + کاپی کریں - کلپ بورڈ کو کاپی کریں + کلپ بورڈ کو کاپی کریں - کوئی توثیق کار ایپ نہیں ملی + کوئی توثیق کار ایپ نہیں ملی - مرحلہ 3 + مرحلہ 3 - تیار کردہ کوڈ کو کاپی کریں اور جاری رکھنے کے لیے یہاں واپس آئیں۔ + تیار کردہ کوڈ کو کاپی کریں اور جاری رکھنے کے لیے یہاں واپس آئیں۔ - جاری رکھیں + جاری رکھیں @@ -140,60 +140,60 @@ ہٹا دیں - + - اپنا کوڈ درج کریں + اپنا کوڈ درج کریں - اپنی توثیق کار ایپ سے 6 ہندسوں کا کوڈ درج کریں۔ + اپنی توثیق کار ایپ سے 6 ہندسوں کا کوڈ درج کریں۔ - کوڈ + کوڈ - ہو گیا - + ہو گیا + - توثیق کار ایپ + توثیق کار ایپ - ایک وقتی تصدیقی کوڈز تخلیق کرنے کے لیے ایک توثیق کار ایپ ترتیب دیں + ایک وقتی تصدیقی کوڈز تخلیق کرنے کے لیے ایک توثیق کار ایپ ترتیب دیں - مزید جانیں + مزید جانیں - توثیق کار ایپ شامل کریں + توثیق کار ایپ شامل کریں - توثیق کار ایپس + توثیق کار ایپس - کوئی توثیق کار ایپس نہیں ہیں + کوئی توثیق کار ایپس نہیں ہیں - شامل \"%1$s\" + شامل \"%1$s\" - توثیق کار ایپ کے اختیارات کھولیں + توثیق کار ایپ کے اختیارات کھولیں - نام تبدیل کریں + نام تبدیل کریں - ہٹا دیں + ہٹا دیں - توثیق کار ایپ ہٹائیں؟ + توثیق کار ایپ ہٹائیں؟ - اس توثیقی طریقے کو ہٹانے کے لیے آپ کی توثیق کار ایپ سے 6 ہندسوں پر مشتمل کوڈ درکار ہے۔ + اس توثیقی طریقے کو ہٹانے کے لیے آپ کی توثیق کار ایپ سے 6 ہندسوں پر مشتمل کوڈ درکار ہے۔ - توثیق کار ایپ شامل نہیں کر سکتے + توثیق کار ایپ شامل نہیں کر سکتے - آپ %1$d سے زیادہ توثیق کار ایپس شامل نہیں کر سکتے۔ پہلے ایک کو ہٹانے کی کوشش کریں۔ + آپ %1$d سے زیادہ توثیق کار ایپس شامل نہیں کر سکتے۔ پہلے ایک کو ہٹانے کی کوشش کریں۔ - توثیق کار ایپ ہٹا دی گئی ہے + توثیق کار ایپ ہٹا دی گئی ہے - + - نام منتخب کریں + نام منتخب کریں - اس توثیق کار ایپ کے لیے ایک منفرد نام منتخب کریں۔ + اس توثیق کار ایپ کے لیے ایک منفرد نام منتخب کریں۔ - اس توثیق کار ایپ کے لیے ایک منفرد نام منتخب کریں تاکہ آپ کو بعد میں اس کی شناخت میں مدد مل سکے۔ + اس توثیق کار ایپ کے لیے ایک منفرد نام منتخب کریں تاکہ آپ کو بعد میں اس کی شناخت میں مدد مل سکے۔ - نام + نام - اگلا + اگلا - توثیق کار ایپ ترتیب دینا + توثیق کار ایپ ترتیب دینا - توثیق کار ایپ کا نام تبدیل کرنا + توثیق کار ایپ کا نام تبدیل کرنا diff --git a/feature/app-settings/src/main/res/values-vi/strings.xml b/feature/app-settings/src/main/res/values-vi/strings.xml index 8060e40f89..c97445fb87 100644 --- a/feature/app-settings/src/main/res/values-vi/strings.xml +++ b/feature/app-settings/src/main/res/values-vi/strings.xml @@ -77,37 +77,37 @@ Tìm hiểu thêm - + - Set up your authenticator app + Set up your authenticator app - Làm theo các bước sau để thiết lập ứng dụng xác thực của bạn. + Làm theo các bước sau để thiết lập ứng dụng xác thực của bạn. - Tìm hiểu thêm + Tìm hiểu thêm - Bước 1 + Bước 1 - Cài đặt một ứng dụng xác thực đáng tin cậy trên thiết bị của bạn. + Cài đặt một ứng dụng xác thực đáng tin cậy trên thiết bị của bạn. - Bước 2 + Bước 2 - Mở ứng dụng xác thực của bạn bằng cách nhấn vào nút bên dưới để thêm tài khoản Signal của bạn. + Mở ứng dụng xác thực của bạn bằng cách nhấn vào nút bên dưới để thêm tài khoản Signal của bạn. - Mở + Mở - Hoặc bạn có thể sao chép mã khóa này để thiết lập thủ công. + Hoặc bạn có thể sao chép mã khóa này để thiết lập thủ công. - Sao chép + Sao chép - Đã sao chép vào clipboard + Đã sao chép vào clipboard - Không tìm thấy ứng dụng xác thực nào + Không tìm thấy ứng dụng xác thực nào - Bước 3 + Bước 3 - Sao chép mã được tạo và quay lại đây để tiếp tục. + Sao chép mã được tạo và quay lại đây để tiếp tục. - Tiếp tục + Tiếp tục @@ -137,60 +137,60 @@ Xóa - + - Nhập mã của bạn + Nhập mã của bạn - Nhập mã gồm 6 chữ số từ ứng dụng xác thực của bạn. + Nhập mã gồm 6 chữ số từ ứng dụng xác thực của bạn. - + - Xong - + Xong + - Ứng dụng xác thực + Ứng dụng xác thực - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Tìm hiểu thêm + Tìm hiểu thêm - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Đã thêm \"%1$s\" + Đã thêm \"%1$s\" - Open authenticator app options + Open authenticator app options - Đổi tên + Đổi tên - Xóa + Xóa - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Tên + Tên - Tiếp + Tiếp - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-yue/strings.xml b/feature/app-settings/src/main/res/values-yue/strings.xml index cfd49f3d1d..168eef75af 100644 --- a/feature/app-settings/src/main/res/values-yue/strings.xml +++ b/feature/app-settings/src/main/res/values-yue/strings.xml @@ -77,37 +77,37 @@ 了解詳情 - + - 設定你嘅驗證 APP + 設定你嘅驗證 APP - 跟住下面步驟設定你嘅驗證 APP。 + 跟住下面步驟設定你嘅驗證 APP。 - 了解詳情 + 了解詳情 - 步驟 1 + 步驟 1 - 喺你部裝置上面安裝一款可以信賴嘅驗證 APP。 + 喺你部裝置上面安裝一款可以信賴嘅驗證 APP。 - 步驟 2 + 步驟 2 - 㩒一下下面個掣打開驗證 APP,就可以加你嘅 Signal 帳戶。 + 㩒一下下面個掣打開驗證 APP,就可以加你嘅 Signal 帳戶。 - 打開 + 打開 - 或者你可以複製呢個金鑰嚟人手設定。 + 或者你可以複製呢個金鑰嚟人手設定。 - 複製 + 複製 - 複製咗去剪貼簿 + 複製咗去剪貼簿 - 搵唔到驗證 APP + 搵唔到驗證 APP - 步驟 3 + 步驟 3 - 複製產生出嚟嘅驗證碼,然後返嚟呢度繼續。 + 複製產生出嚟嘅驗證碼,然後返嚟呢度繼續。 - 繼續 + 繼續 @@ -137,60 +137,60 @@ 移除 - + - 輸入驗證碼 + 輸入驗證碼 - 輸入驗證 APP 入面嘅 6 位數驗證碼。 + 輸入驗證 APP 入面嘅 6 位數驗證碼。 - 二維碼 + 二維碼 - 完成 - + 完成 + - 驗證 APP + 驗證 APP - 設定驗證 APP 嚟產生一次性驗證碼 + 設定驗證 APP 嚟產生一次性驗證碼 - 了解詳情 + 了解詳情 - 加驗證 APP + 加驗證 APP - 驗證 APP + 驗證 APP - 冇驗證 APP + 冇驗證 APP - 已加咗「%1$s」 + 已加咗「%1$s」 - 打開驗證 APP 選項 + 打開驗證 APP 選項 - 重新命名 + 重新命名 - 移除 + 移除 - 係咪要移除驗證 APP? + 係咪要移除驗證 APP? - 要移除呢個驗證方式,你需要喺驗證 APP 入面攞個 6 位數驗證碼。 + 要移除呢個驗證方式,你需要喺驗證 APP 入面攞個 6 位數驗證碼。 - 加唔到驗證 APP + 加唔到驗證 APP - 你冇得加超過 %1$d 個驗證 APP。試下先移除一個。 + 你冇得加超過 %1$d 個驗證 APP。試下先移除一個。 - 驗證 APP 剷走咗 + 驗證 APP 剷走咗 - + - 揀個名 + 揀個名 - 為呢個驗證 APP 揀個特別嘅名。 + 為呢個驗證 APP 揀個特別嘅名。 - 為呢個驗證 APP 揀個特別嘅名,第時可以易啲認到佢。 + 為呢個驗證 APP 揀個特別嘅名,第時可以易啲認到佢。 - 姓名 + 姓名 - 下一步 + 下一步 - 驗證 APP 設定 + 驗證 APP 設定 - 驗證 APP 改咗名 + 驗證 APP 改咗名 diff --git a/feature/app-settings/src/main/res/values-zh-rCN/strings.xml b/feature/app-settings/src/main/res/values-zh-rCN/strings.xml index ddf2ed5697..25d26bfa41 100644 --- a/feature/app-settings/src/main/res/values-zh-rCN/strings.xml +++ b/feature/app-settings/src/main/res/values-zh-rCN/strings.xml @@ -77,37 +77,37 @@ 了解详情 - + - 设置您的身份验证器应用 + 设置您的身份验证器应用 - 请按以下步骤设置您的身份验证器应用。 + 请按以下步骤设置您的身份验证器应用。 - 了解详情 + 了解详情 - 第 1 步 + 第 1 步 - 在您的设备上安装一个您信赖的身份验证器应用。 + 在您的设备上安装一个您信赖的身份验证器应用。 - 第 2 步 + 第 2 步 - 点击以下按钮打开您的身份验证器应用,添加您的 Signal 账户。 + 点击以下按钮打开您的身份验证器应用,添加您的 Signal 账户。 - 打开 + 打开 - 或者您可以复制此密钥进行手动设置。 + 或者您可以复制此密钥进行手动设置。 - 复制 + 复制 - 已复制到剪贴板 + 已复制到剪贴板 - 未找到身份验证器应用 + 未找到身份验证器应用 - 第 3 步 + 第 3 步 - 复制生成的验证码,然后返回此处继续操作。 + 复制生成的验证码,然后返回此处继续操作。 - 继续 + 继续 @@ -137,60 +137,60 @@ 移除 - + - 输入您的验证码 + 输入您的验证码 - 请输入您的身份验证器应用生成的 6 位数验证码。 + 请输入您的身份验证器应用生成的 6 位数验证码。 - 二维码 + 二维码 - 完成 - + 完成 + - 身份验证器应用 + 身份验证器应用 - 设置身份验证器应用以生成一次性验证码 + 设置身份验证器应用以生成一次性验证码 - 了解详情 + 了解详情 - 添加身份验证器应用 + 添加身份验证器应用 - 身份验证器应用 + 身份验证器应用 - 无身份验证器应用 + 无身份验证器应用 - 已添加 “%1$s” + 已添加 “%1$s” - 打开身份验证器应用选项 + 打开身份验证器应用选项 - 重命名 + 重命名 - 移除 + 移除 - 要移除身份验证器应用吗? + 要移除身份验证器应用吗? - 要移除此验证方式,需要提供您身份验证器应用生成的 6 位数字代码。 + 要移除此验证方式,需要提供您身份验证器应用生成的 6 位数字代码。 - 无法添加身份验证器应用 + 无法添加身份验证器应用 - 您最多只能添加 %1$d 个身份验证器应用。请先移除其他应用。 + 您最多只能添加 %1$d 个身份验证器应用。请先移除其他应用。 - 身份验证器应用已移除 + 身份验证器应用已移除 - + - 选择一个名称 + 选择一个名称 - 请为此身份验证器应用选择一个唯一的名称。 + 请为此身份验证器应用选择一个唯一的名称。 - 请为此身份验证器应用选择一个唯一的名称,方便您日后识别。 + 请为此身份验证器应用选择一个唯一的名称,方便您日后识别。 - 名称 + 名称 - 下一步 + 下一步 - 身份验证器应用已设置 + 身份验证器应用已设置 - 身份验证器应用已重命名 + 身份验证器应用已重命名 diff --git a/feature/app-settings/src/main/res/values-zh-rHK/strings.xml b/feature/app-settings/src/main/res/values-zh-rHK/strings.xml index cc9f7557ce..42d1430e55 100644 --- a/feature/app-settings/src/main/res/values-zh-rHK/strings.xml +++ b/feature/app-settings/src/main/res/values-zh-rHK/strings.xml @@ -77,37 +77,37 @@ 了解更多 - + - Set up your authenticator app + Set up your authenticator app - 依照以下步驟設定你的驗證應用程式。 + 依照以下步驟設定你的驗證應用程式。 - 了解更多 + 了解更多 - 步驟 1 + 步驟 1 - 請在你的裝置上安裝一款可信賴的驗證應用程式。 + 請在你的裝置上安裝一款可信賴的驗證應用程式。 - 步驟 2 + 步驟 2 - 點按下方的按鈕開啟驗證應用程式,以新增你的 Signal 帳戶。 + 點按下方的按鈕開啟驗證應用程式,以新增你的 Signal 帳戶。 - 開啟 + 開啟 - 或你可以複製此金鑰來手動進行設定。 + 或你可以複製此金鑰來手動進行設定。 - 複製 + 複製 - 已複製至剪貼簿 + 已複製至剪貼簿 - 找不到驗證應用程式 + 找不到驗證應用程式 - 步驟 3 + 步驟 3 - 複製生成的驗證碼,然後返回此處繼續。 + 複製生成的驗證碼,然後返回此處繼續。 - 繼續 + 繼續 @@ -137,60 +137,60 @@ 移除 - + - 輸入你的驗證碼 + 輸入你的驗證碼 - 請輸入驗證應用程式中的 6 位數驗證碼。 + 請輸入驗證應用程式中的 6 位數驗證碼。 - 二維碼 + 二維碼 - 完成 - + 完成 + - 驗證應用程式 + 驗證應用程式 - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - 了解更多 + 了解更多 - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - 已新增「%1$s」 + 已新增「%1$s」 - Open authenticator app options + Open authenticator app options - 重新命名 + 重新命名 - 移除 + 移除 - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - 選擇一個名稱 + 選擇一個名稱 - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - 姓名 + 姓名 - 下一步 + 下一步 - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values-zh-rTW/strings.xml b/feature/app-settings/src/main/res/values-zh-rTW/strings.xml index ce27c120da..3693df6678 100644 --- a/feature/app-settings/src/main/res/values-zh-rTW/strings.xml +++ b/feature/app-settings/src/main/res/values-zh-rTW/strings.xml @@ -77,37 +77,37 @@ 了解更多 - + - Set up your authenticator app + Set up your authenticator app - 依照以下步驟設定你的驗證應用程式。 + 依照以下步驟設定你的驗證應用程式。 - 了解更多 + 了解更多 - 步驟 1 + 步驟 1 - 請在你的裝置上安裝一款可信賴的驗證應用程式。 + 請在你的裝置上安裝一款可信賴的驗證應用程式。 - 步驟 2 + 步驟 2 - 點按下方的按鈕開啟驗證應用程式,以新增你的 Signal 帳戶。 + 點按下方的按鈕開啟驗證應用程式,以新增你的 Signal 帳戶。 - 開啟 + 開啟 - 或你可以複製此金鑰來手動進行設定。 + 或你可以複製此金鑰來手動進行設定。 - 複製 + 複製 - 已複製到剪貼簿 + 已複製到剪貼簿 - 找不到驗證應用程式 + 找不到驗證應用程式 - 步驟 3 + 步驟 3 - 複製生成的驗證碼,然後返回此處繼續。 + 複製生成的驗證碼,然後返回此處繼續。 - 繼續 + 繼續 @@ -137,60 +137,60 @@ 移除 - + - 輸入你的驗證碼 + 輸入你的驗證碼 - 請輸入驗證應用程式中的 6 位數驗證碼。 + 請輸入驗證應用程式中的 6 位數驗證碼。 - 二維碼 + 二維碼 - 完成 - + 完成 + - 驗證應用程式 + 驗證應用程式 - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - 了解更多 + 了解更多 - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - 已新增「%1$s」 + 已新增「%1$s」 - Open authenticator app options + Open authenticator app options - 重新命名 + 重新命名 - 移除 + 移除 - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + To remove this authentication method the 6-digit code from your authenticator app is required. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. - Authenticator app removed + Authenticator app removed - + - 選擇一個名稱 + 選擇一個名稱 - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - 姓名 + 姓名 - 下一步 + 下一步 - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed diff --git a/feature/app-settings/src/main/res/values/strings.xml b/feature/app-settings/src/main/res/values/strings.xml index db8babe5f6..0c6c879393 100644 --- a/feature/app-settings/src/main/res/values/strings.xml +++ b/feature/app-settings/src/main/res/values/strings.xml @@ -75,37 +75,39 @@ Learn more - + - Set up your authenticator app + Set up your authenticator app - Follow these steps to set up your authenticator app. + Follow these steps to set up your authenticator app. - Learn more + Learn more - Step 1 + Step 1 - Install a trusted authenticator app on your device. + Install a trusted authenticator app on your device. - Step 2 + Step 2 - Open your authenticator app by tapping the button below to add your Signal account. + Open your authenticator app by tapping the button below to add your Signal account. - Open + Open - Or you can copy this key to manually set it up. + Or you can copy this key to manually set it up. - Copy + Copy - Copied to clipboard + Copied to clipboard - No authenticator app found + No authenticator app found + + Couldn\'t reach Signal. Check your connection and try again. - Step 3 + Step 3 - Copy the code that\'s generated and return here to continue. + Copy the code that\'s generated and return here to continue. - Continue + Continue @@ -135,60 +137,70 @@ Remove - + - Enter your code + Enter your code - Enter the 6-digit code from your authenticator app. + Enter the 6-digit code from your authenticator app. - Code + Code - Done - + Done + + Incorrect code. Enter the code showing in your authenticator app now. + + Couldn\'t reach Signal. Check your connection and try again. + - Authenticator app + Authenticator app - Set up an authenticator app to generate one-time verification codes + Set up an authenticator app to generate one-time verification codes - Learn more + Learn more - Add authenticator app + Add authenticator app - Authenticator apps + Authenticator apps - No authenticator apps + No authenticator apps - Added %1$s + Added %1$s - Open authenticator app options + Open authenticator app options - Rename + Rename - Remove + Remove - Remove authenticator app? + Remove authenticator app? - To remove this authentication method the 6-digit code from your authenticator app is required. + You won\'t be able to use this app for one-time verification codes when you sign in. - Can\'t add authenticator app + Can\'t add authenticator app - You can\'t add more than %1$d authenticator apps. Try removing one first. + You can\'t add more than %1$d authenticator apps. Try removing one first. + + Couldn\'t load your authenticator apps. Check your connection and try again. - Authenticator app removed + Authenticator app removed + + Couldn\'t remove authenticator app. Check your connection and try again. - + - Choose a name + Choose a name - Choose a unique name for this authenticator app. + Choose a unique name for this authenticator app. - Choose a unique name for this authenticator app to help you identify it later. + Choose a unique name for this authenticator app to help you identify it later. - Name + Name - Next + Next - Authenticator app set up + Authenticator app set up - Authenticator app renamed + Authenticator app renamed + + Couldn\'t save name. Check your connection and try again. diff --git a/feature/app-settings/src/test/java/org/signal/appsettings/account/AccountSettingsScreenTest.kt b/feature/app-settings/src/test/java/org/signal/appsettings/account/AccountSettingsScreenTest.kt index dc35c1f2d1..043d0a088a 100644 --- a/feature/app-settings/src/test/java/org/signal/appsettings/account/AccountSettingsScreenTest.kt +++ b/feature/app-settings/src/test/java/org/signal/appsettings/account/AccountSettingsScreenTest.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode import assertk.assertThat @@ -21,7 +22,9 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config +import org.signal.appsettings.R import org.signal.appsettings.account.AccountSettingsState.Dialog import org.signal.core.ui.compose.Dialogs @@ -29,6 +32,8 @@ import org.signal.core.ui.compose.Dialogs @Config(application = Application::class) class AccountSettingsScreenTest { + private val context: Application = RuntimeEnvironment.getApplication() + @get:Rule val composeTestRule = createComposeRule() @@ -294,12 +299,12 @@ class AccountSettingsScreenTest { setContent(createState()) composeTestRule.onNodeWithTag(AccountSettingsTestTags.CARD_SIGNAL_LOGIN).assertDoesNotExist() - composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_AUTHENTICATOR_APP).assertDoesNotExist() + composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_TOTP_APP).assertDoesNotExist() } @Test fun givenASignalLogin_whenIClickTheSignalLoginCard_thenIExpectAccountAndRecoveryEvent() { - setContent(createState(signalLogin = AccountSettingsState.SignalLogin(authenticatorAppCount = 0, passkeyCount = 0))) + setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = 0, passkeyCount = 0))) composeTestRule.onNodeWithTag(AccountSettingsTestTags.CARD_SIGNAL_LOGIN).performClick() @@ -307,19 +312,34 @@ class AccountSettingsScreenTest { } @Test - fun givenASignalLogin_whenIClickAuthenticatorApp_thenIExpectAuthenticatorAppEvent() { - setContent(createState(signalLogin = AccountSettingsState.SignalLogin(authenticatorAppCount = 0, passkeyCount = 0))) + fun givenASignalLogin_whenIClickTotpApp_thenIExpectTotpAppEvent() { + setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = 0, passkeyCount = 0))) composeTestRule.onNodeWithTag(AccountSettingsTestTags.CARD_SIGNAL_LOGIN).assertIsDisplayed() - composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_AUTHENTICATOR_APP).performClick() + composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_TOTP_APP).performClick() - assertThat(events).contains(AccountSettingsEvent.AuthenticatorAppClicked) + assertThat(events).contains(AccountSettingsEvent.TotpAppClicked) + } + + /** A count we couldn't fetch has to read the same as no count at all, rather than as "0 configured". */ + @Test + fun givenAnUnknownTotpAppCount_whenScreenDisplayed_thenTheRowDoesNotClaimACount() { + setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = null, passkeyCount = 0))) + + composeTestRule.onNodeWithText(context.getString(R.string.AccountSettingsFragment__one_time_verification_codes)).assertIsDisplayed() + } + + @Test + fun givenConfiguredTotpApps_whenScreenDisplayed_thenTheRowShowsTheCount() { + setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = 2, passkeyCount = 0))) + + composeTestRule.onNodeWithText(context.resources.getQuantityString(R.plurals.AccountSettingsFragment__d_configured, 2, 2)).assertIsDisplayed() } @Test fun givenASignalLogin_whenIClickPasskeys_thenIExpectPasskeysEvent() { - setContent(createState(signalLogin = AccountSettingsState.SignalLogin(authenticatorAppCount = 0, passkeyCount = 0))) + setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = 0, passkeyCount = 0))) scrollTo(AccountSettingsTestTags.ROW_PASSKEYS) diff --git a/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsScreenTest.kt b/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsScreenTest.kt deleted file mode 100644 index 85bfc76888..0000000000 --- a/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorapps/AuthenticatorAppsScreenTest.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.appsettings.authenticatorapps - -import android.app.Application -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onAllNodesWithTag -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToNode -import assertk.assertThat -import assertk.assertions.contains -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.signal.appsettings.authenticatorapps.AuthenticatorAppsState.Dialog -import org.signal.core.ui.compose.Dialogs - -@RunWith(RobolectricTestRunner::class) -@Config(application = Application::class) -class AuthenticatorAppsScreenTest { - - companion object { - private val APPS = listOf( - AuthenticatorApp(id = 1, name = "Bitwarden Authenticator", createdAt = System.currentTimeMillis()), - AuthenticatorApp(id = 2, name = "Twilio Authy", createdAt = System.currentTimeMillis()) - ) - } - - @get:Rule - val composeTestRule = createComposeRule() - - private val events = mutableListOf() - - @Test - fun givenNoApps_whenIDisplayScreen_thenIExpectTheEmptyMessage() { - setContent(AuthenticatorAppsState()) - - scrollTo(AuthenticatorAppsTestTags.EMPTY_MESSAGE) - composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.EMPTY_MESSAGE).assertIsDisplayed() - } - - @Test - fun givenApps_whenIDisplayScreen_thenIExpectARowPerApp() { - setContent(AuthenticatorAppsState(apps = APPS)) - - for (app in APPS) { - composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.SCROLLER).performScrollToNode(hasText(app.name)) - composeTestRule.onNodeWithText(app.name).assertIsDisplayed() - } - } - - @Test - fun whenIClickAddAuthenticatorApp_thenIExpectAddAuthenticatorAppClickedEvent() { - setContent(AuthenticatorAppsState()) - - composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.BUTTON_ADD) - .assertIsDisplayed() - .performClick() - - assertThat(events).contains(AuthenticatorAppsEvent.AddAuthenticatorAppClicked) - } - - @Test - fun givenApps_whenIClickRenameInTheMenu_thenIExpectRenameAppClickedEvent() { - setContent(AuthenticatorAppsState(apps = APPS)) - - scrollTo(AuthenticatorAppsTestTags.ROW_APP) - composeTestRule.onAllNodesWithTag(AuthenticatorAppsTestTags.BUTTON_APP_MENU)[0].performClick() - composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.MENU_ITEM_RENAME).performClick() - - assertThat(events).contains(AuthenticatorAppsEvent.RenameAppClicked(appId = APPS[0].id)) - } - - @Test - fun givenApps_whenIClickRemoveInTheMenu_thenIExpectRemoveAppClickedEvent() { - setContent(AuthenticatorAppsState(apps = APPS)) - - scrollTo(AuthenticatorAppsTestTags.ROW_APP) - composeTestRule.onAllNodesWithTag(AuthenticatorAppsTestTags.BUTTON_APP_MENU)[0].performClick() - composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.MENU_ITEM_REMOVE).performClick() - - assertThat(events).contains(AuthenticatorAppsEvent.RemoveAppClicked(appId = APPS[0].id)) - } - - @Test - fun givenTheConfirmRemoveDialog_whenIDisplayScreen_thenIExpectTheDialog() { - setContent(AuthenticatorAppsState(apps = APPS, dialog = Dialog.ConfirmRemove(APPS[0].id))) - - composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.DIALOG_CONFIRM_REMOVE).assertIsDisplayed() - } - - @Test - fun givenTheConfirmRemoveDialog_whenICancel_thenIExpectDialogDismissedEvent() { - setContent(AuthenticatorAppsState(apps = APPS, dialog = Dialog.ConfirmRemove(APPS[0].id))) - - composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_DISMISS_BUTTON).performClick() - - assertThat(events).contains(AuthenticatorAppsEvent.DialogDismissed) - } - - @Test - fun givenTheMaxAppsDialog_whenIDisplayScreen_thenIExpectTheDialog() { - setContent(AuthenticatorAppsState(apps = APPS, dialog = Dialog.MaxAppsReached)) - - composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.DIALOG_MAX_APPS_REACHED).assertIsDisplayed() - } - - @Test - fun givenTheMaxAppsDialog_whenIClickLearnMore_thenIExpectLearnMoreAndDismissEvents() { - setContent(AuthenticatorAppsState(apps = APPS, dialog = Dialog.MaxAppsReached)) - - composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_DISMISS_BUTTON).performClick() - - assertThat(events).contains(AuthenticatorAppsEvent.LearnMoreClicked) - assertThat(events).contains(AuthenticatorAppsEvent.DialogDismissed) - } - - private fun setContent(state: AuthenticatorAppsState) { - composeTestRule.setContent { - AuthenticatorAppsScreen( - state = state, - onEvent = { events += it } - ) - } - } - - private fun scrollTo(testTag: String) { - composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.SCROLLER) - .performScrollToNode(hasTestTag(testTag)) - } -} diff --git a/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupScreenTest.kt b/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupScreenTest.kt deleted file mode 100644 index a0e30d5741..0000000000 --- a/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorsetup/AuthenticatorSetupScreenTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.appsettings.authenticatorsetup - -import android.app.Application -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToNode -import assertk.assertThat -import assertk.assertions.contains -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -@Config(application = Application::class) -class AuthenticatorSetupScreenTest { - - companion object { - private const val SETUP_KEY = "KVZ7WL3FDDWJZMTOB7PLZPKVRFD4LYSX" - } - - @get:Rule - val composeTestRule = createComposeRule() - - private val events = mutableListOf() - - @Test - fun whenIClickOpen_thenIExpectOpenAuthenticatorAppEvent() { - setContent() - - scrollTo(AuthenticatorSetupTestTags.BUTTON_OPEN) - - composeTestRule.onNodeWithTag(AuthenticatorSetupTestTags.BUTTON_OPEN).performClick() - - assertThat(events).contains(AuthenticatorSetupEvent.OpenAuthenticatorAppClicked) - } - - @Test - fun whenIClickCopy_thenIExpectCopyKeyEvent() { - setContent() - - scrollTo(AuthenticatorSetupTestTags.BUTTON_COPY) - - composeTestRule.onNodeWithTag(AuthenticatorSetupTestTags.BUTTON_COPY).performClick() - - assertThat(events).contains(AuthenticatorSetupEvent.CopyKeyClicked) - } - - @Test - fun whenIClickContinue_thenIExpectContinueEvent() { - setContent() - - composeTestRule.onNodeWithTag(AuthenticatorSetupTestTags.BUTTON_CONTINUE) - .assertIsDisplayed() - .performClick() - - assertThat(events).contains(AuthenticatorSetupEvent.ContinueClicked) - } - - private fun setContent() { - composeTestRule.setContent { - AuthenticatorSetupScreen( - state = AuthenticatorSetupState(setupKey = SETUP_KEY), - onEvent = { events += it } - ) - } - } - - private fun scrollTo(testTag: String) { - composeTestRule.onNodeWithTag(AuthenticatorSetupTestTags.SCROLLER) - .performScrollToNode(hasTestTag(testTag)) - } -} diff --git a/feature/app-settings/src/test/java/org/signal/appsettings/totpapplist/TotpAppListScreenTest.kt b/feature/app-settings/src/test/java/org/signal/appsettings/totpapplist/TotpAppListScreenTest.kt new file mode 100644 index 0000000000..e760b08310 --- /dev/null +++ b/feature/app-settings/src/test/java/org/signal/appsettings/totpapplist/TotpAppListScreenTest.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.appsettings.totpapplist + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import assertk.assertThat +import assertk.assertions.contains +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.appsettings.totpapplist.TotpAppListState.Dialog +import org.signal.appsettings.totpapplist.TotpAppListState.LoadState +import org.signal.core.ui.compose.Dialogs + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class TotpAppListScreenTest { + + companion object { + private val APPS = listOf( + TotpApp(id = 1, name = "Bitwarden Authenticator", createdAt = System.currentTimeMillis()), + TotpApp(id = 2, name = "Twilio Authy", createdAt = System.currentTimeMillis()) + ) + } + + @get:Rule + val composeTestRule = createComposeRule() + + private val events = mutableListOf() + + @Test + fun givenNoApps_whenIDisplayScreen_thenIExpectTheEmptyMessage() { + setContent(TotpAppListState(loadState = LoadState.LOADED)) + + scrollTo(TotpAppListTestTags.EMPTY_MESSAGE) + composeTestRule.onNodeWithTag(TotpAppListTestTags.EMPTY_MESSAGE).assertIsDisplayed() + } + + @Test + fun givenApps_whenIDisplayScreen_thenIExpectARowPerApp() { + setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED)) + + for (app in APPS) { + composeTestRule.onNodeWithTag(TotpAppListTestTags.SCROLLER).performScrollToNode(hasText(app.name)) + composeTestRule.onNodeWithText(app.name).assertIsDisplayed() + } + } + + @Test + fun whenIClickAddTotpApp_thenIExpectAddTotpAppClickedEvent() { + setContent(TotpAppListState(loadState = LoadState.LOADED)) + + composeTestRule.onNodeWithTag(TotpAppListTestTags.BUTTON_ADD) + .assertIsDisplayed() + .performClick() + + assertThat(events).contains(TotpAppListEvent.AddTotpAppClicked) + } + + @Test + fun givenApps_whenIClickRenameInTheMenu_thenIExpectRenameAppClickedEvent() { + setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED)) + + scrollTo(TotpAppListTestTags.ROW_APP) + composeTestRule.onAllNodesWithTag(TotpAppListTestTags.BUTTON_APP_MENU)[0].performClick() + composeTestRule.onNodeWithTag(TotpAppListTestTags.MENU_ITEM_RENAME).performClick() + + assertThat(events).contains(TotpAppListEvent.RenameAppClicked(appId = APPS[0].id)) + } + + @Test + fun givenApps_whenIClickRemoveInTheMenu_thenIExpectRemoveAppClickedEvent() { + setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED)) + + scrollTo(TotpAppListTestTags.ROW_APP) + composeTestRule.onAllNodesWithTag(TotpAppListTestTags.BUTTON_APP_MENU)[0].performClick() + composeTestRule.onNodeWithTag(TotpAppListTestTags.MENU_ITEM_REMOVE).performClick() + + assertThat(events).contains(TotpAppListEvent.RemoveAppClicked(appId = APPS[0].id)) + } + + @Test + fun givenTheConfirmRemoveDialog_whenIDisplayScreen_thenIExpectTheDialog() { + setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.ConfirmRemove(APPS[0].id))) + + composeTestRule.onNodeWithTag(TotpAppListTestTags.DIALOG_CONFIRM_REMOVE).assertIsDisplayed() + } + + /** The event has to carry the id, since the dialog holding it is dismissed before the confirmation is reported. */ + @Test + fun givenTheConfirmRemoveDialog_whenIConfirm_thenIExpectRemoveAppConfirmedForThatApp() { + setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.ConfirmRemove(APPS[1].id))) + + composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_CONFIRM_BUTTON).performClick() + + assertThat(events).contains(TotpAppListEvent.RemoveAppConfirmed(APPS[1].id)) + } + + @Test + fun givenTheConfirmRemoveDialog_whenICancel_thenIExpectDialogDismissedEvent() { + setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.ConfirmRemove(APPS[0].id))) + + composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_DISMISS_BUTTON).performClick() + + assertThat(events).contains(TotpAppListEvent.DialogDismissed) + } + + @Test + fun givenTheMaxAppsDialog_whenIDisplayScreen_thenIExpectTheDialog() { + setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.MaxAppsReached)) + + composeTestRule.onNodeWithTag(TotpAppListTestTags.DIALOG_MAX_APPS_REACHED).assertIsDisplayed() + } + + @Test + fun givenTheMaxAppsDialog_whenIClickLearnMore_thenIExpectLearnMoreAndDismissEvents() { + setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.MaxAppsReached)) + + composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_DISMISS_BUTTON).performClick() + + assertThat(events).contains(TotpAppListEvent.LearnMoreClicked) + assertThat(events).contains(TotpAppListEvent.DialogDismissed) + } + + @Test + fun whenTheListHasntArrived_thenIExpectASpinnerRatherThanNoTotpApps() { + setContent(TotpAppListState(loadState = LoadState.LOADING)) + + scrollTo(TotpAppListTestTags.LOADING) + composeTestRule.onNodeWithTag(TotpAppListTestTags.LOADING).assertIsDisplayed() + composeTestRule.onNodeWithTag(TotpAppListTestTags.EMPTY_MESSAGE).assertDoesNotExist() + } + + /** An account we couldn't ask about is not an account with no authenticator apps. */ + @Test + fun givenTheListCouldntBeLoaded_whenIDisplayScreen_thenIExpectTheFailureMessageRatherThanTheEmptyOne() { + setContent(TotpAppListState(loadState = LoadState.NETWORK_FAILURE)) + + scrollTo(TotpAppListTestTags.LOAD_FAILED_MESSAGE) + composeTestRule.onNodeWithTag(TotpAppListTestTags.LOAD_FAILED_MESSAGE).assertIsDisplayed() + composeTestRule.onNodeWithTag(TotpAppListTestTags.EMPTY_MESSAGE).assertDoesNotExist() + composeTestRule.onNodeWithTag(TotpAppListTestTags.LOADING).assertDoesNotExist() + } + + private fun setContent(state: TotpAppListState) { + composeTestRule.setContent { + TotpAppListScreen( + state = state, + onEvent = { events += it } + ) + } + } + + private fun scrollTo(testTag: String) { + composeTestRule.onNodeWithTag(TotpAppListTestTags.SCROLLER) + .performScrollToNode(hasTestTag(testTag)) + } +} diff --git a/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryScreenTest.kt b/feature/app-settings/src/test/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryScreenTest.kt similarity index 57% rename from feature/app-settings/src/test/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryScreenTest.kt rename to feature/app-settings/src/test/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryScreenTest.kt index 63997d7d8e..0dc8441b15 100644 --- a/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorcodeentry/AuthenticatorCodeEntryScreenTest.kt +++ b/feature/app-settings/src/test/java/org/signal/appsettings/totpcodeentry/TotpCodeEntryScreenTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorcodeentry +package org.signal.appsettings.totpcodeentry import android.app.Application import androidx.compose.ui.test.assertIsEnabled @@ -22,43 +22,43 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(application = Application::class) -class AuthenticatorCodeEntryScreenTest { +class TotpCodeEntryScreenTest { @get:Rule val composeTestRule = createComposeRule() - private val events = mutableListOf() + private val events = mutableListOf() @Test fun givenAPartialCode_whenScreenDisplayed_thenDoneIsDisabled() { - setContent(AuthenticatorCodeEntryState(code = "123")) + setContent(TotpCodeEntryState(code = "123")) - composeTestRule.onNodeWithTag(AuthenticatorCodeEntryTestTags.BUTTON_DONE).assertIsNotEnabled() + composeTestRule.onNodeWithTag(TotpCodeEntryTestTags.BUTTON_DONE).assertIsNotEnabled() } @Test fun givenAFullCode_whenIClickDone_thenIExpectDoneEvent() { - setContent(AuthenticatorCodeEntryState(code = "123456")) + setContent(TotpCodeEntryState(code = "123456")) - composeTestRule.onNodeWithTag(AuthenticatorCodeEntryTestTags.BUTTON_DONE) + composeTestRule.onNodeWithTag(TotpCodeEntryTestTags.BUTTON_DONE) .assertIsEnabled() .performClick() - assertThat(events).contains(AuthenticatorCodeEntryEvent.DoneClicked) + assertThat(events).contains(TotpCodeEntryEvent.DoneClicked) } @Test fun whenITypeInTheCodeField_thenIExpectCodeChangedEvent() { - setContent(AuthenticatorCodeEntryState()) + setContent(TotpCodeEntryState()) - composeTestRule.onNodeWithTag(AuthenticatorCodeEntryTestTags.CODE_INPUT).performTextInput("123456") + composeTestRule.onNodeWithTag(TotpCodeEntryTestTags.CODE_INPUT).performTextInput("123456") - assertThat(events).contains(AuthenticatorCodeEntryEvent.CodeChanged("123456")) + assertThat(events).contains(TotpCodeEntryEvent.CodeChanged("123456")) } - private fun setContent(state: AuthenticatorCodeEntryState) { + private fun setContent(state: TotpCodeEntryState) { composeTestRule.setContent { - AuthenticatorCodeEntryScreen( + TotpCodeEntryScreen( state = state, onEvent = { events += it } ) diff --git a/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorname/AuthenticatorNameScreenTest.kt b/feature/app-settings/src/test/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreenTest.kt similarity index 59% rename from feature/app-settings/src/test/java/org/signal/appsettings/authenticatorname/AuthenticatorNameScreenTest.kt rename to feature/app-settings/src/test/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreenTest.kt index a7d73b53e8..4a985f3f23 100644 --- a/feature/app-settings/src/test/java/org/signal/appsettings/authenticatorname/AuthenticatorNameScreenTest.kt +++ b/feature/app-settings/src/test/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreenTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.appsettings.authenticatorname +package org.signal.appsettings.totpnameentry import android.app.Application import androidx.compose.ui.test.assertIsDisplayed @@ -23,52 +23,52 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(application = Application::class) -class AuthenticatorNameScreenTest { +class TotpNameEntryScreenTest { @get:Rule val composeTestRule = createComposeRule() - private val events = mutableListOf() + private val events = mutableListOf() @Test fun whenITypeAName_thenIExpectNameChangedEvent() { - setContent(AuthenticatorNameState()) + setContent(TotpNameEntryState()) - composeTestRule.onNodeWithTag(AuthenticatorNameTestTags.NAME_INPUT) + composeTestRule.onNodeWithTag(TotpNameEntryTestTags.NAME_INPUT) .assertIsDisplayed() .performTextReplacement("Bitwarden Authenticator") - assertThat(events).contains(AuthenticatorNameEvent.NameChanged("Bitwarden Authenticator")) + assertThat(events).contains(TotpNameEntryEvent.NameChanged("Bitwarden Authenticator")) } @Test fun givenABlankName_whenIDisplayScreen_thenIExpectNextDisabled() { - setContent(AuthenticatorNameState()) + setContent(TotpNameEntryState()) - composeTestRule.onNodeWithTag(AuthenticatorNameTestTags.BUTTON_NEXT).assertIsNotEnabled() + composeTestRule.onNodeWithTag(TotpNameEntryTestTags.BUTTON_NEXT).assertIsNotEnabled() } @Test fun givenASubmittingState_whenIDisplayScreen_thenIExpectNextDisabled() { - setContent(AuthenticatorNameState(name = "Twilio Authy", submitting = true)) + setContent(TotpNameEntryState(name = "Twilio Authy", submitting = true)) - composeTestRule.onNodeWithTag(AuthenticatorNameTestTags.BUTTON_NEXT).assertIsNotEnabled() + composeTestRule.onNodeWithTag(TotpNameEntryTestTags.BUTTON_NEXT).assertIsNotEnabled() } @Test fun givenAName_whenIClickNext_thenIExpectNextClickedEvent() { - setContent(AuthenticatorNameState(name = "Twilio Authy")) + setContent(TotpNameEntryState(name = "Twilio Authy")) - composeTestRule.onNodeWithTag(AuthenticatorNameTestTags.BUTTON_NEXT) + composeTestRule.onNodeWithTag(TotpNameEntryTestTags.BUTTON_NEXT) .assertIsEnabled() .performClick() - assertThat(events).contains(AuthenticatorNameEvent.NextClicked) + assertThat(events).contains(TotpNameEntryEvent.NextClicked) } - private fun setContent(state: AuthenticatorNameState) { + private fun setContent(state: TotpNameEntryState) { composeTestRule.setContent { - AuthenticatorNameScreen( + TotpNameEntryScreen( state = state, onEvent = { events += it } ) diff --git a/feature/app-settings/src/test/java/org/signal/appsettings/totpsetup/TotpSetupScreenTest.kt b/feature/app-settings/src/test/java/org/signal/appsettings/totpsetup/TotpSetupScreenTest.kt new file mode 100644 index 0000000000..2dec2d0aed --- /dev/null +++ b/feature/app-settings/src/test/java/org/signal/appsettings/totpsetup/TotpSetupScreenTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.appsettings.totpsetup + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isEmpty +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class TotpSetupScreenTest { + + companion object { + private const val SETUP_KEY = "KVZ7WL3FDDWJZMTOB7PLZPKVRFD4LYSX" + } + + @get:Rule + val composeTestRule = createComposeRule() + + private val events = mutableListOf() + + @Test + fun whenIClickOpen_thenIExpectOpenTotpAppEvent() { + setContent() + + scrollTo(TotpSetupTestTags.BUTTON_OPEN) + + composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_OPEN).performClick() + + assertThat(events).contains(TotpSetupEvent.OpenTotpAppClicked) + } + + @Test + fun whenIClickCopy_thenIExpectCopyKeyEvent() { + setContent() + + scrollTo(TotpSetupTestTags.BUTTON_COPY) + + composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_COPY).performClick() + + assertThat(events).contains(TotpSetupEvent.CopyKeyClicked) + } + + @Test + fun whenIClickContinue_thenIExpectContinueEvent() { + setContent() + + composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_CONTINUE) + .assertIsDisplayed() + .performClick() + + assertThat(events).contains(TotpSetupEvent.ContinueClicked) + } + + @Test + fun whenTheKeyHasntArrived_thenIExpectASpinnerAndNothingToClick() { + setContent(TotpSetupState(loading = true)) + + scrollTo(TotpSetupTestTags.SETUP_KEY_SPINNER) + + composeTestRule.onNodeWithTag(TotpSetupTestTags.SETUP_KEY_SPINNER).assertIsDisplayed() + composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_COPY).assertIsNotEnabled() + composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_CONTINUE).assertIsNotEnabled() + + composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_CONTINUE).performClick() + + assertThat(events).isEmpty() + } + + @Test + fun whenTheKeyHasArrived_thenIExpectItInPlaceOfTheSpinner() { + setContent() + + scrollTo(TotpSetupTestTags.SETUP_KEY) + + composeTestRule.onNodeWithTag(TotpSetupTestTags.SETUP_KEY).assertIsDisplayed() + } + + @Test + fun whenIDismissAFailureDialog_thenIExpectDialogDismissedEvent() { + setContent(TotpSetupState(loading = false, dialog = TotpSetupState.Dialog.NetworkFailure)) + + composeTestRule.onNodeWithText("OK").performClick() + + assertThat(events).contains(TotpSetupEvent.DialogDismissed) + } + + private fun setContent(state: TotpSetupState = TotpSetupState(setupKey = SETUP_KEY, loading = false)) { + composeTestRule.setContent { + TotpSetupScreen( + state = state, + onEvent = { events += it } + ) + } + } + + private fun scrollTo(testTag: String) { + composeTestRule.onNodeWithTag(TotpSetupTestTags.SCROLLER) + .performScrollToNode(hasTestTag(testTag)) + } +} diff --git a/feature/registration/src/main/java/org/signal/registration/NetworkController.kt b/feature/registration/src/main/java/org/signal/registration/NetworkController.kt index 8e526ea620..5e092975e1 100644 --- a/feature/registration/src/main/java/org/signal/registration/NetworkController.kt +++ b/feature/registration/src/main/java/org/signal/registration/NetworkController.kt @@ -113,6 +113,7 @@ interface NetworkController { * phone number, in which case the implementation generates a placeholder username the service ignores. * @param password The password for basic auth * @param aci The ACI of the existing numberless account to log back in to, used as the username for basic auth. + * @param totp A TOTP one-time password, required when recovering an account that has TOTP keys. */ suspend fun registerAccount( e164: String?, @@ -125,7 +126,8 @@ interface NetworkController { pniPreKeys: PreKeyCollection?, fcmToken: String?, skipDeviceTransfer: Boolean, - aci: ACI? + aci: ACI?, + totp: Int? ): RequestResult /** diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationFlowEvent.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationFlowEvent.kt index aaaeebc8e7..0c802b389a 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationFlowEvent.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationFlowEvent.kt @@ -24,7 +24,7 @@ sealed interface RegistrationFlowEvent { /** Navigate back one screen. */ data object NavigateBack : RegistrationFlowEvent - /** Pop the back stack back to an existing screen, removing everything above it. Replaces the current screen if the route isn't on the stack. */ + /** Pop the back stack back to an existing screen (matched by route type), removing everything above it. Replaces the current screen if the route isn't on the stack. */ data class NavigateBackToScreen(val route: RegistrationRoute) : RegistrationFlowEvent /** We've encountered some irrecoverable state where the best course of action is to completely reset registration. */ diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt index 525e92e74d..aa3775c93a 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt @@ -117,6 +117,7 @@ import org.signal.registration.screens.restoreselection.ArchiveRestoreSelectionV import org.signal.registration.screens.restoreselection.RegisteredState import org.signal.registration.screens.signallogincredentials.SignalLoginCredentialEntryScreen import org.signal.registration.screens.signallogincredentials.SignalLoginCredentialEntryScreenActions +import org.signal.registration.screens.signallogincredentials.SignalLoginCredentialEntryScreenEvents import org.signal.registration.screens.signallogincredentials.SignalLoginCredentialEntryViewModel import org.signal.registration.screens.signallogindetails.SignalLoginViewDetailsScreenActions import org.signal.registration.screens.signallogindetails.SignalLoginViewDetailsViewModel @@ -125,6 +126,12 @@ import org.signal.registration.screens.signallogininfo.SignalLoginInfoViewModel import org.signal.registration.screens.signalloginpayment.SignalLoginPaymentScreen import org.signal.registration.screens.signalloginpayment.SignalLoginPaymentScreenActions import org.signal.registration.screens.signalloginpayment.SignalLoginPaymentViewModel +import org.signal.registration.screens.totpentry.TotpEntryScreen +import org.signal.registration.screens.totpentry.TotpEntryViewModel +import org.signal.registration.screens.twofactorselection.TwoFactorMethod +import org.signal.registration.screens.twofactorselection.TwoFactorSelectionAction +import org.signal.registration.screens.twofactorselection.TwoFactorSelectionScreen +import org.signal.registration.screens.twofactorselection.TwoFactorSelectionViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo import org.signal.registration.screens.verificationcode.VerificationCodeScreen @@ -191,6 +198,14 @@ sealed interface RegistrationRoute : NavKey, Parcelable { override fun toString(): String = "SignalLoginCredentialEntry(prefilledAccountId=${prefilledAccountId?.censor()})" } + /** Choosing which second factor to authenticate with when logging in requires two-factor authentication. */ + @Serializable + data class TwoFactorSelection(val methods: List) : RegistrationRoute + + /** Entering the six-digit code from the user's authenticator app. */ + @Serializable + data object TotpEntry : RegistrationRoute + /** Optional username selection for a phone-numberless account. */ @Serializable data object AddUsername : RegistrationRoute @@ -370,6 +385,7 @@ private const val BACKUP_CREDENTIAL_RESULT = "backup_credential_result" private const val AEP_FOR_LOCAL_BACKUP_RESULT = "aep_for_local_backup_result" private const val LOCAL_BACKUP_RESTORE_RESULT = "local_backup_restore_result" private const val PHONE_NUMBER_DISCOVERABILITY_RESULT = "phone_number_discoverability_result" +private const val TWO_FACTOR_CODE_RESULT = "two_factor_code_result" private const val PIN_LEARN_MORE_URL = "https://support.signal.org/hc/articles/360007059792" // TODO [phonenumberless] Point at the real support article once it exists. @@ -777,12 +793,54 @@ private fun EntryProviderScope.navigationEntries( } } + ResultEffect(registrationViewModel.resultBus, TWO_FACTOR_CODE_RESULT) { code -> + viewModel.onEvent(SignalLoginCredentialEntryScreenEvents.TwoFactorCodeEntered(code)) + } + SignalLoginCredentialEntryScreen( state = state, onEvent = { viewModel.onEvent(it) } ) } + // -- Two-Factor Method Selection Screen + entry { key -> + val viewModel: TwoFactorSelectionViewModel = viewModel( + factory = TwoFactorSelectionViewModel.Factory( + methods = key.methods, + parentEventEmitter = parentEventEmitter + ) + ) + val state by viewModel.state.collectAsStateWithLifecycle() + CollectActions(viewModel.actions) { action -> + when (action) { + TwoFactorSelectionAction.AuthenticateWithPasskey -> error("Passkeys are not offered as a two-factor method yet.") + } + } + + TwoFactorSelectionScreen( + state = state, + onEvent = { viewModel.onEvent(it) } + ) + } + + // -- TOTP Code Entry Screen + entry { + val viewModel: TotpEntryViewModel = viewModel( + factory = TotpEntryViewModel.Factory( + parentEventEmitter = parentEventEmitter, + resultBus = registrationViewModel.resultBus, + resultKey = TWO_FACTOR_CODE_RESULT + ) + ) + val state by viewModel.state.collectAsStateWithLifecycle() + + TotpEntryScreen( + state = state, + onEvent = { viewModel.onEvent(it) } + ) + } + // -- Add Username Screen entry { val viewModel: AddUsernameViewModel = viewModel( diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt index 1dd8398621..46785a00bb 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt @@ -397,7 +397,8 @@ class RegistrationRepository( recoveryPassword: String, aep: AccountEntropyPool, registrationLock: String? = null, - skipDeviceTransfer: Boolean = true + skipDeviceTransfer: Boolean = true, + totp: Int? = null ): RequestResult = withContext(Dispatchers.IO) { val result = registerAccount( e164 = null, @@ -407,7 +408,8 @@ class RegistrationRepository( aci = aci, registrationLock = registrationLock, skipDeviceTransfer = skipDeviceTransfer, - existingAccountEntropyPool = aep + existingAccountEntropyPool = aep, + totp = totp ) if (result is RequestResult.Success && result.result.response.authCredentialSalt == null) { @@ -712,7 +714,8 @@ class RegistrationRepository( existingAccountEntropyPool: AccountEntropyPool? = null, existingAciIdentityKeyPair: IdentityKeyPair? = null, existingPniIdentityKeyPair: IdentityKeyPair? = null, - unrestrictedUnidentifiedAccess: Boolean = false + unrestrictedUnidentifiedAccess: Boolean = false, + totp: Int? = null ): RequestResult = withContext(Dispatchers.IO) { val phoneNumberless = receiptCredentialPresentation != null || aci != null @@ -724,7 +727,7 @@ class RegistrationRepository( check(e164 != null) { "Must provide an e164 when registering with a phone number" } } - Log.i(TAG, "[registerAccount] Starting registration for $e164. sessionId: ${sessionId != null}, recoveryPassword: ${recoveryPassword != null}, receiptCredentialPresentation: ${receiptCredentialPresentation != null}, aci: ${aci != null}, phoneNumberless: $phoneNumberless, registrationLock: ${registrationLock != null}, skipDeviceTransfer: $skipDeviceTransfer, existingAep: ${existingAccountEntropyPool != null}") + Log.i(TAG, "[registerAccount] Starting registration for $e164. sessionId: ${sessionId != null}, recoveryPassword: ${recoveryPassword != null}, receiptCredentialPresentation: ${receiptCredentialPresentation != null}, aci: ${aci != null}, phoneNumberless: $phoneNumberless, registrationLock: ${registrationLock != null}, skipDeviceTransfer: $skipDeviceTransfer, existingAep: ${existingAccountEntropyPool != null}, totp: ${totp != null}") val inProgressData = storageController.readInProgressRegistrationData() val resumedAciIdentityKeyPair = inProgressData.accountData?.aciIdentityKeyPair?.takeIf { it.size > 0 }?.let { IdentityKeyPair(it.toByteArray()) } @@ -814,7 +817,8 @@ class RegistrationRepository( pniPreKeys = pniPreKeys, fcmToken = fcmToken, skipDeviceTransfer = skipDeviceTransfer, - aci = aci + aci = aci, + totp = totp ) when (result) { diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt index 4b53841b50..999b887cc2 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt @@ -182,7 +182,7 @@ class RegistrationViewModel( } private fun applyNavigateBackToScreenEvent(inputState: RegistrationFlowState, event: RegistrationFlowEvent.NavigateBackToScreen): RegistrationFlowState { - val index = inputState.backStack.indexOfLast { it == event.route } + val index = inputState.backStack.indexOfLast { it::class == event.route::class } return if (index >= 0) { inputState.copy(backStack = inputState.backStack.take(index + 1)) } else { diff --git a/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryScreenEvents.kt b/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryScreenEvents.kt index ad3e5c188a..be80df8569 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryScreenEvents.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryScreenEvents.kt @@ -35,6 +35,11 @@ sealed class SignalLoginCredentialEntryScreenEvents { /** The user submitted the login, either with the next button or the keyboard's done action. */ data object NextClicked : SignalLoginCredentialEntryScreenEvents() + /** The user completed the two-factor flow the login was bounced to. Carries the entered code. */ + data class TwoFactorCodeEntered(val code: String) : SignalLoginCredentialEntryScreenEvents() { + override fun toString(): String = "TwoFactorCodeEntered(code=${code.censor()})" + } + /** The user dismissed the login error dialog. */ data object DismissError : SignalLoginCredentialEntryScreenEvents() } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryViewModel.kt index d7f1c65aed..fa91376c8b 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryViewModel.kt @@ -31,6 +31,7 @@ import org.signal.registration.RestoreDecision import org.signal.registration.screens.aepentry.AepInput import org.signal.registration.screens.shared.AccountIdError import org.signal.registration.screens.shared.AccountIdFormat +import org.signal.registration.screens.twofactorselection.TwoFactorMethod import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo @@ -102,7 +103,11 @@ class SignalLoginCredentialEntryViewModel( } is SignalLoginCredentialEntryScreenEvents.NextClicked -> { - applyNextClicked(state, parentEventEmitter, stateEmitter) + applyNextClicked(state, totp = null, parentEventEmitter, stateEmitter) + } + + is SignalLoginCredentialEntryScreenEvents.TwoFactorCodeEntered -> { + applyNextClicked(state, totp = event.code.toIntOrNull(), parentEventEmitter, stateEmitter) } } } @@ -129,7 +134,7 @@ class SignalLoginCredentialEntryViewModel( if (filledState.isNextEnabled) { Log.i(TAG, "[CredentialSelected] The password manager supplied a complete login. Submitting it.") - applyNextClicked(filledState, parentEventEmitter, stateEmitter) + applyNextClicked(filledState, totp = null, parentEventEmitter, stateEmitter) } else { Log.w(TAG, "[CredentialSelected] The password manager supplied a login we can't submit as-is. Leaving it in the fields for the user to fix.") } @@ -137,6 +142,7 @@ class SignalLoginCredentialEntryViewModel( private suspend fun applyNextClicked( state: SignalLoginCredentialEntryState, + totp: Int?, parentEventEmitter: (RegistrationFlowEvent) -> Unit, stateEmitter: (SignalLoginCredentialEntryState) -> Unit ) { @@ -154,15 +160,16 @@ class SignalLoginCredentialEntryViewModel( stateEmitter(state.copy(isLoggingIn = true)) parentEventEmitter(RegistrationFlowEvent.UserSuppliedAepSubmitted(aep)) - Log.i(TAG, "[Next] Attempting to log in to ${aci.logString()} with the RRP derived from the entered recovery key.") + Log.i(TAG, "[Next] Attempting to log in to ${aci.logString()} with the RRP derived from the entered recovery key. totp: ${totp != null}") - attemptToLogIn(state, aci, aep, provideRegistrationLock = false, parentEventEmitter, stateEmitter) + attemptToLogIn(state, aci, aep, totp, provideRegistrationLock = false, parentEventEmitter, stateEmitter) } private suspend fun attemptToLogIn( inputState: SignalLoginCredentialEntryState, aci: ACI, aep: AccountEntropyPool, + totp: Int?, provideRegistrationLock: Boolean, parentEventEmitter: (RegistrationFlowEvent) -> Unit, stateEmitter: (SignalLoginCredentialEntryState) -> Unit @@ -175,7 +182,8 @@ class SignalLoginCredentialEntryViewModel( aci = aci, recoveryPassword = recoveryPassword, aep = aep, - registrationLock = registrationLock + registrationLock = registrationLock, + totp = totp ) when (result) { @@ -217,7 +225,7 @@ class SignalLoginCredentialEntryViewModel( ) } else { Log.w(TAG, "[Next] Registration locked. Retrying with the reglock token derived from the recovery key.") - attemptToLogIn(inputState, aci, aep, provideRegistrationLock = true, parentEventEmitter, stateEmitter) + attemptToLogIn(inputState, aci, aep, totp, provideRegistrationLock = true, parentEventEmitter, stateEmitter) } } is RegisterAccountError.RateLimited -> { @@ -230,9 +238,15 @@ class SignalLoginCredentialEntryViewModel( is RegisterAccountError.DeviceTransferPossible -> { error("[Next] Device transfer possible. This should not happen with RRP-based registration.") } + RegisterAccountError.TotpMissingOrIncorrect -> { + // For now this error only means TOTP, but in the future it will indicate that some two-factor method is + // required, so we treat it generically and route through the method selection screen. + Log.w(TAG, "[Next] A two-factor code is required. Sending the user to two-factor method selection.") + stateEmitter(inputState.copy(isLoggingIn = false)) + parentEventEmitter.navigateTo(RegistrationRoute.TwoFactorSelection(methods = listOf(TwoFactorMethod.AuthenticatorApp))) + } is RegisterAccountError.InvalidRequest, is RegisterAccountError.InvalidReceiptCredentialPresentation, - RegisterAccountError.TotpMissingOrIncorrect, RegisterAccountError.PostQuantumRatchetRequired -> { Log.w(TAG, "[Next] Unexpected registration error: $error") stateEmitter(inputState.copy(isLoggingIn = false, loginError = SignalLoginError.UnknownError)) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/totpentry/TotpEntryAction.kt b/feature/registration/src/main/java/org/signal/registration/screens/totpentry/TotpEntryAction.kt deleted file mode 100644 index 6fd79422ce..0000000000 --- a/feature/registration/src/main/java/org/signal/registration/screens/totpentry/TotpEntryAction.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.registration.screens.totpentry - -import org.signal.core.util.censor - -/** - * One-shot side effects that have to be carried out by the host rather than the screen itself. - * - * Actions are logged, so be sure `toString()` contains nothing sensitive. - */ -sealed interface TotpEntryAction { - - /** The user cancelled code entry. Leave the screen. */ - data object NavigateBack : TotpEntryAction - - /** All six digits have been entered. The host should verify [code]. */ - data class CodeEntered(val code: String) : TotpEntryAction { - override fun toString(): String = "CodeEntered(code=${code.censor()})" - } -} diff --git a/feature/registration/src/main/java/org/signal/registration/screens/totpentry/TotpEntryViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/totpentry/TotpEntryViewModel.kt index 72e87cf603..a9848f6c5c 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/totpentry/TotpEntryViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/totpentry/TotpEntryViewModel.kt @@ -5,35 +5,40 @@ package org.signal.registration.screens.totpentry +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import org.signal.core.ui.compose.EventDrivenViewModel +import org.signal.core.ui.navigation.ResultEventBus import org.signal.core.util.logging.Log +import org.signal.registration.RegistrationFlowEvent +import org.signal.registration.RegistrationRoute import org.signal.registration.screens.totpentry.TotpEntryState.Companion.CODE_LENGTH +import org.signal.registration.screens.util.navigateBack /** - * Drives [TotpEntryScreen]. Interprets raw digit-field input into a six-digit code, surfacing the completed code - * (and cancellation) to the host as a [TotpEntryAction]. + * Drives [TotpEntryScreen]. Interprets raw digit-field input into a six-digit code. The completed code is emitted + * via [ResultEventBus], then the two-factor screens are popped so the login screen that bounced here can retry. */ -class TotpEntryViewModel : EventDrivenViewModel(TAG) { +class TotpEntryViewModel( + private val parentEventEmitter: (RegistrationFlowEvent) -> Unit, + private val resultBus: ResultEventBus, + private val resultKey: String +) : EventDrivenViewModel(TAG) { companion object { private val TAG = Log.tag(TotpEntryViewModel::class) } private val _state = MutableStateFlow(TotpEntryState()) - private val _actions = Channel(Channel.BUFFERED) val state: StateFlow = _state.asStateFlow() - val actions: Flow = _actions.receiveAsFlow() init { _state @@ -47,7 +52,7 @@ class TotpEntryViewModel : EventDrivenViewModel(TAG) { applyDigitChanged(event.index, event.value) } TotpEntryScreenEvents.CancelClicked -> { - _actions.send(TotpEntryAction.NavigateBack) + parentEventEmitter.navigateBack() } } } @@ -59,9 +64,9 @@ class TotpEntryViewModel : EventDrivenViewModel(TAG) { * - a single digit is recorded and focus advances * - multi-character input (e.g. a pasted code) populates every field at once * - * Once every field has a value, the completed code is emitted as [TotpEntryAction.CodeEntered]. + * Once every field has a value, the completed code is emitted. */ - private suspend fun applyDigitChanged(index: Int, value: String) { + private fun applyDigitChanged(index: Int, value: String) { check(index in _state.value.digits.indices) { "[DigitChanged] Out of bounds index $index." } if (value.isEmpty()) { @@ -94,7 +99,7 @@ class TotpEntryViewModel : EventDrivenViewModel(TAG) { * Populates every digit field from a full pasted [code] at once. Multi-character input that isn't a complete code * is ignored. */ - private suspend fun applyFullCode(code: String) { + private fun applyFullCode(code: String) { if (code.length != CODE_LENGTH) { Log.w(TAG, "[DigitChanged] Ignoring multi-character input containing ${code.length} digits.") return @@ -130,10 +135,21 @@ class TotpEntryViewModel : EventDrivenViewModel(TAG) { _state.update { it.copy(digits = newDigits, focusedDigitIndex = (index - 1).coerceAtLeast(0)) } } - private suspend fun emitCodeIfComplete() { + private fun emitCodeIfComplete() { val state = _state.value if (state.isComplete) { - _actions.send(TotpEntryAction.CodeEntered(state.code)) + resultBus.sendResult(resultKey, state.code) + parentEventEmitter(RegistrationFlowEvent.NavigateBackToScreen(RegistrationRoute.SignalLoginCredentialEntry())) + } + } + + class Factory( + private val parentEventEmitter: (RegistrationFlowEvent) -> Unit, + private val resultBus: ResultEventBus, + private val resultKey: String + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return TotpEntryViewModel(parentEventEmitter, resultBus, resultKey) as T } } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionAction.kt b/feature/registration/src/main/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionAction.kt index b73b07fed5..37e2149c3c 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionAction.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionAction.kt @@ -14,10 +14,4 @@ sealed interface TwoFactorSelectionAction { /** The user wants to authenticate with a passkey. The host should run the credential-manager flow. */ data object AuthenticateWithPasskey : TwoFactorSelectionAction - - /** The user wants to authenticate with an authenticator app. The host should move on to code entry. */ - data object NavigateToTotpEntry : TwoFactorSelectionAction - - /** The user cancelled two-factor authentication. Leave the screen. */ - data object NavigateBack : TwoFactorSelectionAction } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionViewModel.kt index 0ca7bf6be3..7ff203a678 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionViewModel.kt @@ -18,13 +18,17 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log +import org.signal.registration.RegistrationFlowEvent +import org.signal.registration.RegistrationRoute +import org.signal.registration.screens.util.navigateBack +import org.signal.registration.screens.util.navigateTo /** - * Drives [TwoFactorSelectionScreen]. Surfaces the method the user picked (and cancellation) to the host as a - * [TwoFactorSelectionAction]. + * Drives [TwoFactorSelectionScreen]. */ class TwoFactorSelectionViewModel( - methods: List + methods: List, + private val parentEventEmitter: (RegistrationFlowEvent) -> Unit ) : EventDrivenViewModel(TAG) { companion object { @@ -46,22 +50,23 @@ class TwoFactorSelectionViewModel( override suspend fun processEvent(event: TwoFactorSelectionScreenEvents) { when (event) { is TwoFactorSelectionScreenEvents.MethodSelected -> { - val action = when (event.method) { - TwoFactorMethod.Passkey -> TwoFactorSelectionAction.AuthenticateWithPasskey - TwoFactorMethod.AuthenticatorApp -> TwoFactorSelectionAction.NavigateToTotpEntry + when (event.method) { + TwoFactorMethod.Passkey -> _actions.send(TwoFactorSelectionAction.AuthenticateWithPasskey) + TwoFactorMethod.AuthenticatorApp -> parentEventEmitter.navigateTo(RegistrationRoute.TotpEntry) } - - _actions.send(action) } TwoFactorSelectionScreenEvents.CancelClicked -> { - _actions.send(TwoFactorSelectionAction.NavigateBack) + parentEventEmitter.navigateBack() } } } - class Factory(private val methods: List) : ViewModelProvider.Factory { + class Factory( + private val methods: List, + private val parentEventEmitter: (RegistrationFlowEvent) -> Unit + ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return TwoFactorSelectionViewModel(methods) as T + return TwoFactorSelectionViewModel(methods, parentEventEmitter) as T } } } diff --git a/feature/registration/src/test/java/org/signal/registration/fakes/FakeNetworkController.kt b/feature/registration/src/test/java/org/signal/registration/fakes/FakeNetworkController.kt index e93a0da52b..48998d2864 100644 --- a/feature/registration/src/test/java/org/signal/registration/fakes/FakeNetworkController.kt +++ b/feature/registration/src/test/java/org/signal/registration/fakes/FakeNetworkController.kt @@ -87,7 +87,7 @@ class FakeNetworkController( } data class UpdateSessionRequest(val sessionId: String?, val pushChallengeToken: String?, val captchaToken: String?) - data class RegisterAccountRequest(val e164: String?, val sessionId: String?, val recoveryPassword: String?, val registrationLock: String?, val aci: ACI? = null, val pniPreKeys: PreKeyCollection? = null, val pniRegistrationId: Int? = null) + data class RegisterAccountRequest(val e164: String?, val sessionId: String?, val recoveryPassword: String?, val registrationLock: String?, val aci: ACI? = null, val pniPreKeys: PreKeyCollection? = null, val pniRegistrationId: Int? = null, val totp: Int? = null) data class SetPinRequest(val pin: String, val masterKey: MasterKey) data class RestoreMasterKeyRequest(val svrCredentials: SvrCredentials, val pin: String) data class SetRestoreMethodRequest(val token: String, val method: RestoreMethod) @@ -329,9 +329,10 @@ class FakeNetworkController( pniPreKeys: PreKeyCollection?, fcmToken: String?, skipDeviceTransfer: Boolean, - aci: ACI? + aci: ACI?, + totp: Int? ): RequestResult { - val request = RegisterAccountRequest(e164, sessionId, recoveryPassword, attributes.registrationLock, aci, pniPreKeys, attributes.pniRegistrationId) + val request = RegisterAccountRequest(e164, sessionId, recoveryPassword, attributes.registrationLock, aci, pniPreKeys, attributes.pniRegistrationId, totp) lastRegisterAccountRequest = request return onRegisterAccount(request) } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryViewModelTest.kt index d05254fa0c..c22aac1380 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryViewModelTest.kt @@ -49,6 +49,7 @@ import org.signal.registration.screens.aepentry.AepInput import org.signal.registration.screens.restoreselection.ArchiveRestoreOption import org.signal.registration.screens.restoreselection.RegisteredState import org.signal.registration.screens.shared.AccountIdError +import org.signal.registration.screens.twofactorselection.TwoFactorMethod import java.io.IOException import java.util.UUID import kotlin.time.Duration @@ -416,6 +417,40 @@ class SignalLoginCredentialEntryViewModelTest { assertThat(emittedStates.last().loginError).isEqualTo(SignalLoginError.UnknownError) } + @Test + fun `NextClicked requiring a two-factor code navigates to two-factor selection offering only the authenticator app`() = runTest(testDispatcher) { + coEvery { mockRepository.reRegisterAccountWithoutPhoneNumber(any(), any(), any(), any(), any()) } returns + RequestResult.NonSuccess(RegisterAccountError.TotpMissingOrIncorrect) + + applyEvent(completeState(), SignalLoginCredentialEntryScreenEvents.NextClicked) + + assertThat(emittedStates.last().isLoggingIn).isEqualTo(false) + assertThat(emittedParentEvents.last()) + .isInstanceOf() + .prop(RegistrationFlowEvent.NavigateToScreen::route) + .isInstanceOf() + .prop(RegistrationRoute.TwoFactorSelection::methods) + .containsExactly(TwoFactorMethod.AuthenticatorApp) + } + + @Test + fun `TwoFactorCodeEntered retries the login with the entered code`() = runTest(testDispatcher) { + val aep = AccountEntropyPool(VALID_AEP) + stubSuccessfulLogin(aep) + + applyEvent(completeState(), SignalLoginCredentialEntryScreenEvents.TwoFactorCodeEntered("123456")) + + coVerify { + mockRepository.reRegisterAccountWithoutPhoneNumber( + aci = VALID_ACI, + recoveryPassword = aep.deriveMasterKey().deriveRegistrationRecoveryPassword(), + aep = match { it.value == VALID_AEP }, + registrationLock = null, + totp = 123456 + ) + } + } + @Test(expected = IllegalStateException::class) fun `NextClicked with SessionNotFoundOrNotVerified throws`() = runTest(testDispatcher) { coEvery { mockRepository.reRegisterAccountWithoutPhoneNumber(any(), any(), any(), any(), any()) } returns @@ -460,7 +495,7 @@ class SignalLoginCredentialEntryViewModelTest { } private fun stubSuccessfulLogin(aep: AccountEntropyPool, reregistration: Boolean = true) { - coEvery { mockRepository.reRegisterAccountWithoutPhoneNumber(any(), any(), any(), any(), any()) } returns successfulResult(aep, reregistration) + coEvery { mockRepository.reRegisterAccountWithoutPhoneNumber(any(), any(), any(), any(), any(), any()) } returns successfulResult(aep, reregistration) } private fun stubRemoteBackup(exists: Boolean) { diff --git a/feature/registration/src/test/java/org/signal/registration/screens/totpentry/TotpEntryViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/totpentry/TotpEntryViewModelTest.kt index f15591d3c4..1aed79c947 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/totpentry/TotpEntryViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/totpentry/TotpEntryViewModelTest.kt @@ -10,13 +10,10 @@ import assertk.assertions.containsExactly import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse +import assertk.assertions.isNull import assertk.assertions.isTrue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest @@ -24,12 +21,22 @@ import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Test +import org.signal.core.ui.navigation.ResultEventBus +import org.signal.registration.RegistrationFlowEvent +import org.signal.registration.RegistrationRoute @OptIn(ExperimentalCoroutinesApi::class) class TotpEntryViewModelTest { + companion object { + private const val RESULT_KEY = "totp_code_result" + } + private val testDispatcher = UnconfinedTestDispatcher() + private val emittedParentEvents = mutableListOf() + private val resultBus = ResultEventBus() + @Before fun setUp() { Dispatchers.setMain(testDispatcher) @@ -42,7 +49,7 @@ class TotpEntryViewModelTest { @Test fun `initial state is empty with focus on the first digit`() = runTest(testDispatcher) { - val viewModel = TotpEntryViewModel() + val viewModel = createViewModel() assertThat(viewModel.state.value.digits).isEqualTo(TotpEntryState.emptyDigits()) assertThat(viewModel.state.value.focusedDigitIndex).isEqualTo(0) @@ -51,7 +58,7 @@ class TotpEntryViewModelTest { @Test fun `entering a digit records it and advances focus`() = runTest(testDispatcher) { - val viewModel = TotpEntryViewModel() + val viewModel = createViewModel() viewModel.onEvent(TotpEntryScreenEvents.DigitChanged(0, "4")) @@ -60,47 +67,48 @@ class TotpEntryViewModelTest { } @Test - fun `entering the final digit emits CodeEntered`() = runTest(testDispatcher) { - val viewModel = TotpEntryViewModel() - val actions = collectActions(viewModel.actions) + fun `entering the final digit emits the code and pops back to the login screen`() = runTest(testDispatcher) { + val viewModel = createViewModel() "41837".forEachIndexed { index, digit -> viewModel.onEvent(TotpEntryScreenEvents.DigitChanged(index, digit.toString())) } - assertThat(actions).isEmpty() + assertThat(sentCode()).isNull() + assertThat(emittedParentEvents).isEmpty() viewModel.onEvent(TotpEntryScreenEvents.DigitChanged(5, "2")) assertThat(viewModel.state.value.isComplete).isTrue() - assertThat(actions).containsExactly(TotpEntryAction.CodeEntered("418372")) + assertThat(sentCode()).isEqualTo("418372") + assertThat(emittedParentEvents).containsExactly(RegistrationFlowEvent.NavigateBackToScreen(RegistrationRoute.SignalLoginCredentialEntry())) } @Test - fun `pasting a full code populates every field and emits CodeEntered`() = runTest(testDispatcher) { - val viewModel = TotpEntryViewModel() - val actions = collectActions(viewModel.actions) + fun `pasting a full code populates every field and emits the code`() = runTest(testDispatcher) { + val viewModel = createViewModel() viewModel.onEvent(TotpEntryScreenEvents.DigitChanged(0, "418372")) assertThat(viewModel.state.value.digits).isEqualTo(listOf("4", "1", "8", "3", "7", "2")) assertThat(viewModel.state.value.focusedDigitIndex).isEqualTo(5) - assertThat(actions).containsExactly(TotpEntryAction.CodeEntered("418372")) + assertThat(sentCode()).isEqualTo("418372") + assertThat(emittedParentEvents).containsExactly(RegistrationFlowEvent.NavigateBackToScreen(RegistrationRoute.SignalLoginCredentialEntry())) } @Test fun `pasting an incomplete code is ignored`() = runTest(testDispatcher) { - val viewModel = TotpEntryViewModel() - val actions = collectActions(viewModel.actions) + val viewModel = createViewModel() viewModel.onEvent(TotpEntryScreenEvents.DigitChanged(0, "4183")) assertThat(viewModel.state.value.digits).isEqualTo(TotpEntryState.emptyDigits()) - assertThat(actions).isEmpty() + assertThat(sentCode()).isNull() + assertThat(emittedParentEvents).isEmpty() } @Test fun `a backspace deletes the digit and shifts the following ones left`() = runTest(testDispatcher) { - val viewModel = TotpEntryViewModel() + val viewModel = createViewModel() viewModel.onEvent(TotpEntryScreenEvents.DigitChanged(0, "4")) viewModel.onEvent(TotpEntryScreenEvents.DigitChanged(1, "1")) @@ -114,7 +122,7 @@ class TotpEntryViewModelTest { @Test fun `a backspace on an empty field deletes the previous digit`() = runTest(testDispatcher) { - val viewModel = TotpEntryViewModel() + val viewModel = createViewModel() viewModel.onEvent(TotpEntryScreenEvents.DigitChanged(0, "4")) viewModel.onEvent(TotpEntryScreenEvents.DigitChanged(1, "")) @@ -124,18 +132,23 @@ class TotpEntryViewModelTest { } @Test - fun `CancelClicked emits NavigateBack`() = runTest(testDispatcher) { - val viewModel = TotpEntryViewModel() - val actions = collectActions(viewModel.actions) + fun `CancelClicked navigates back`() = runTest(testDispatcher) { + val viewModel = createViewModel() viewModel.onEvent(TotpEntryScreenEvents.CancelClicked) - assertThat(actions).containsExactly(TotpEntryAction.NavigateBack) + assertThat(emittedParentEvents).containsExactly(RegistrationFlowEvent.NavigateBack) } - private fun TestScope.collectActions(actions: Flow): List { - val collected = mutableListOf() - backgroundScope.launch { actions.toList(collected) } - return collected + private fun createViewModel(): TotpEntryViewModel { + return TotpEntryViewModel( + parentEventEmitter = { emittedParentEvents.add(it) }, + resultBus = resultBus, + resultKey = RESULT_KEY + ) + } + + private fun sentCode(): String? { + return resultBus.channelMap[RESULT_KEY]?.tryReceive()?.getOrNull() as String? } } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionViewModelTest.kt index 24979c20a0..71eade3a3d 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/twofactorselection/TwoFactorSelectionViewModelTest.kt @@ -7,6 +7,7 @@ package org.signal.registration.screens.twofactorselection import assertk.assertThat import assertk.assertions.containsExactly +import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -21,12 +22,16 @@ import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Test +import org.signal.registration.RegistrationFlowEvent +import org.signal.registration.RegistrationRoute @OptIn(ExperimentalCoroutinesApi::class) class TwoFactorSelectionViewModelTest { private val testDispatcher = UnconfinedTestDispatcher() + private val emittedParentEvents = mutableListOf() + @Before fun setUp() { Dispatchers.setMain(testDispatcher) @@ -52,26 +57,25 @@ class TwoFactorSelectionViewModelTest { viewModel.onEvent(TwoFactorSelectionScreenEvents.MethodSelected(TwoFactorMethod.Passkey)) assertThat(actions).containsExactly(TwoFactorSelectionAction.AuthenticateWithPasskey) + assertThat(emittedParentEvents).isEmpty() } @Test fun `selecting the authenticator app moves on to code entry`() = runTest(testDispatcher) { val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) viewModel.onEvent(TwoFactorSelectionScreenEvents.MethodSelected(TwoFactorMethod.AuthenticatorApp)) - assertThat(actions).containsExactly(TwoFactorSelectionAction.NavigateToTotpEntry) + assertThat(emittedParentEvents).containsExactly(RegistrationFlowEvent.NavigateToScreen(RegistrationRoute.TotpEntry)) } @Test fun `CancelClicked navigates back`() = runTest(testDispatcher) { val viewModel = createViewModel() - val actions = collectActions(viewModel.actions) viewModel.onEvent(TwoFactorSelectionScreenEvents.CancelClicked) - assertThat(actions).containsExactly(TwoFactorSelectionAction.NavigateBack) + assertThat(emittedParentEvents).containsExactly(RegistrationFlowEvent.NavigateBack) } @Test @@ -87,7 +91,7 @@ class TwoFactorSelectionViewModelTest { private fun createViewModel( methods: List = listOf(TwoFactorMethod.Passkey, TwoFactorMethod.AuthenticatorApp) ): TwoFactorSelectionViewModel { - return TwoFactorSelectionViewModel(methods) + return TwoFactorSelectionViewModel(methods, parentEventEmitter = { emittedParentEvents.add(it) }) } private fun TestScope.collectActions(actions: Flow): List {