From 1d11e29e946335a8f1e22949935e418998fa26fd Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 15 Jul 2026 11:12:01 -0400 Subject: [PATCH] Add linked device account screen. --- .../settings/app/AppSettingsActivity.kt | 6 +- .../settings/app/AppSettingsFragment.kt | 27 ++- ...LinkedDeviceAccountLearnMoreBottomSheet.kt | 142 +++++++++++ .../LinkedDeviceAccountSettingsEvent.kt | 29 +++ .../LinkedDeviceAccountSettingsFragment.kt | 220 ++++++++++++++++++ .../LinkedDeviceAccountSettingsState.kt | 20 ++ .../LinkedDeviceAccountSettingsViewModel.kt | 58 +++++ .../linkdevice/LinkDeviceRepository.kt | 2 +- .../app_settings_with_change_number.xml | 23 ++ app/src/main/res/values/strings.xml | 26 +++ ...inkedDeviceAccountSettingsViewModelTest.kt | 126 ++++++++++ .../core/ui/compose}/EventDrivenViewModel.kt | 2 +- .../core/ui/compose/theme/SignalTheme.kt | 2 +- .../registration/RegistrationViewModel.kt | 2 +- .../EnterAepForLocalBackupViewModel.kt | 2 +- ...orRemoteBackupPostRegistrationViewModel.kt | 2 +- ...ForRemoteBackupPreRegistrationViewModel.kt | 2 +- .../countrycode/CountryCodePickerViewModel.kt | 2 +- .../createprofile/CreateProfileViewModel.kt | 2 +- .../DeviceTransferCompleteViewModel.kt | 2 +- .../DeviceTransferInstructionsViewModel.kt | 2 +- .../DeviceTransferProgressViewModel.kt | 2 +- .../setup/DeviceTransferSetupViewModel.kt | 2 +- .../PhoneNumberDiscoverabilityViewModel.kt | 2 +- .../linkaccount/LinkAccountViewModel.kt | 2 +- .../LocalBackupRestoreViewModel.kt | 2 +- .../messagesync/MessageSyncViewModel.kt | 2 +- .../phonenumber/PhoneNumberEntryViewModel.kt | 2 +- .../pincreation/PinCreationViewModel.kt | 2 +- .../PinEntryForRegistrationLockViewModel.kt | 2 +- .../pinentry/PinEntryForSmsBypassViewModel.kt | 2 +- .../PinEntryForSvrRestoreViewModel.kt | 2 +- .../quickrestore/QuickRestoreQrViewModel.kt | 2 +- .../RemoteBackupRestoreViewModel.kt | 2 +- .../ArchiveRestoreSelectionViewModel.kt | 2 +- .../VerificationCodeViewModel.kt | 2 +- .../org/signal/network/api/LinkDeviceApi.kt | 7 +- 37 files changed, 695 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountLearnMoreBottomSheet.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsEvent.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsState.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsViewModel.kt create mode 100644 app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsViewModelTest.kt rename {feature/registration/src/main/java/org/signal/registration/screens => core/ui/src/main/java/org/signal/core/ui/compose}/EventDrivenViewModel.kt (96%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt index 30a15b3bd0..2d9d8fb207 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt @@ -90,7 +90,11 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent { is AppSettingsRoute.BackupsRoute.Backups -> AppSettingsFragmentDirections.actionDirectToBackupsSettingsFragment().setLaunchCheckoutFlow(appSettingsRoute.launchCheckoutFlow) AppSettingsRoute.Invite -> AppSettingsFragmentDirections.actionDirectToInviteFragment() AppSettingsRoute.DataAndStorageRoute.DataAndStorage -> AppSettingsFragmentDirections.actionDirectToStoragePreferenceFragment() - AppSettingsRoute.AccountRoute.Account -> AppSettingsFragmentDirections.actionDirectToAccountSettingsFragment() + AppSettingsRoute.AccountRoute.Account -> if (SignalStore.account.isPrimaryDevice) { + AppSettingsFragmentDirections.actionDirectToAccountSettingsFragment() + } else { + AppSettingsFragmentDirections.actionDirectToLinkedDeviceAccountSettingsFragment() + } else -> error("Unsupported start location: ${appSettingsRoute?.javaClass?.name}") } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index bd933f3e1e..bcd839b1dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -76,6 +76,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaym import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.CommunicationActions @@ -96,7 +97,11 @@ class AppSettingsFragment : ComposeFragment(), Callbacks { appSettingsRouter.currentRoute.collect { route -> when (route) { is AppSettingsRoute.BackupsRoute.Remote -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_remoteBackupsSettingsFragment) - is AppSettingsRoute.AccountRoute.Account -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_accountSettingsFragment) + is AppSettingsRoute.AccountRoute.Account -> if (SignalStore.account.isPrimaryDevice) { + findNavController().safeNavigate(R.id.action_appSettingsFragment_to_accountSettingsFragment) + } else { + findNavController().safeNavigate(R.id.action_appSettingsFragment_to_linkedDeviceAccountSettingsFragment) + } is AppSettingsRoute.LinkDeviceRoute.LinkDevice -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_linkDeviceFragment) is AppSettingsRoute.DonationsRoute.Donations -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageDonationsFragment) is AppSettingsRoute.AppearanceRoute.Appearance -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment) @@ -292,17 +297,17 @@ private fun AppSettingsContent( BackupFailureState.NONE -> Unit } - if (state.isPrimaryDevice) { - item { - Rows.TextRow( - text = stringResource(R.string.AccountSettingsFragment__account), - icon = painterResource(CoreUiR.drawable.symbol_person_circle_24), - onClick = { - callbacks.navigate(AppSettingsRoute.AccountRoute.Account) - } - ) - } + item { + Rows.TextRow( + text = stringResource(R.string.AccountSettingsFragment__account), + icon = painterResource(CoreUiR.drawable.symbol_person_circle_24), + onClick = { + callbacks.navigate(AppSettingsRoute.AccountRoute.Account) + } + ) + } + if (state.isPrimaryDevice) { item { Rows.TextRow( text = stringResource(R.string.preferences__linked_devices), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountLearnMoreBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountLearnMoreBottomSheet.kt new file mode 100644 index 0000000000..494111c029 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountLearnMoreBottomSheet.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentManager +import org.signal.core.ui.BottomSheetUtil +import org.signal.core.ui.compose.BottomSheets +import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.horizontalGutters +import org.thoughtcrime.securesms.R + +/** + * Bottom sheet shown from the linked-device Account settings screen explaining what linked devices + * are and that account management lives on the primary device. + */ +class LinkedDeviceAccountLearnMoreBottomSheet : ComposeBottomSheetDialogFragment() { + + override val peekHeightPercentage: Float = 0.75f + + companion object { + @JvmStatic + fun show(fragmentManager: FragmentManager) { + LinkedDeviceAccountLearnMoreBottomSheet() + .show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } + } + + @Composable + override fun SheetContent() { + LinkedDeviceAccountLearnMoreSheet() + } +} + +@Composable +private fun LinkedDeviceAccountLearnMoreSheet() { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .horizontalGutters() + .padding(bottom = 48.dp) + ) { + BottomSheets.Handle() + + Icon( + imageVector = SignalIcons.Devices.imageVector, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(top = 16.dp, bottom = 24.dp) + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)) + .padding(horizontal = 20.dp, vertical = 6.dp) + .size(40.dp) + ) + + val linkedDevices = stringResource(R.string.LinkedDeviceAccountLearnMoreBottomSheet__linked_devices) + val text = stringResource(R.string.LinkedDeviceAccountLearnMoreBottomSheet__linked_devices_let_you_access) + val annotatedText = remember(text, linkedDevices) { + buildAnnotatedString { + val start = text.indexOf(linkedDevices) + if (start >= 0) { + append(text.substring(0, start)) + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(linkedDevices) + } + append(text.substring(start + linkedDevices.length)) + } else { + append(text) + } + } + } + + Text( + text = annotatedText, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 20.dp) + ) + + BulletRow(stringResource(R.string.LinkedDeviceAccountLearnMoreBottomSheet__you_can_link_a_desktop)) + BulletRow(stringResource(R.string.LinkedDeviceAccountLearnMoreBottomSheet__your_primary_device_manages)) + BulletRow(stringResource(R.string.LinkedDeviceAccountLearnMoreBottomSheet__some_account_settings)) + } +} + +@Composable +private fun BulletRow(text: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + verticalAlignment = Alignment.Top + ) { + Text( + text = "\u2022", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(end = 8.dp) + ) + + Text( + text = text, + style = MaterialTheme.typography.bodyLarge + ) + } +} + +@DayNightPreviews +@Composable +private fun LinkedDeviceAccountLearnMoreSheetPreview() { + Previews.BottomSheetContentPreview { + LinkedDeviceAccountLearnMoreSheet() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsEvent.kt new file mode 100644 index 0000000000..e4344e85f3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsEvent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account + +sealed interface LinkedDeviceAccountSettingsEvent { + /** The user tapped "Learn more" on the linked-device callout. */ + data object LearnMoreClicked : LinkedDeviceAccountSettingsEvent + + /** The user tapped the navigation (back) icon. */ + data object NavigateBackClicked : LinkedDeviceAccountSettingsEvent + + /** The user tapped the "Delete app data" row. */ + data object DeleteAppDataClicked : LinkedDeviceAccountSettingsEvent + + /** The user confirmed the delete in the confirmation dialog. */ + data object DeleteConfirmed : LinkedDeviceAccountSettingsEvent + + /** The user dismissed the delete confirmation dialog. */ + data object DeleteDismissed : LinkedDeviceAccountSettingsEvent + + /** The fragment reported that clearing application data failed. */ + data object DataWipeFailed : LinkedDeviceAccountSettingsEvent + + /** The fragment has handled the current [LinkedDeviceAccountSettingsState.oneTimeEvent]. */ + data object ConsumeOneTimeEvent : LinkedDeviceAccountSettingsEvent +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsFragment.kt new file mode 100644 index 0000000000..1e5a5a8c8f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsFragment.kt @@ -0,0 +1,220 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.signal.core.ui.compose.Buttons +import org.signal.core.ui.compose.ComposeFragment +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Dialogs +import org.signal.core.ui.compose.Dividers +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.Rows +import org.signal.core.ui.compose.Scaffolds +import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.core.util.ServiceUtil +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.app.account.LinkedDeviceAccountSettingsState.OneTimeEvent +import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier +import org.thoughtcrime.securesms.dependencies.AppDependencies + +/** + * Account settings shown when the current device is a linked (non-primary) device. Account + * management lives on the primary device, so this screen only surfaces an informational callout + * and the ability to delete the Signal data stored on this device. + */ +class LinkedDeviceAccountSettingsFragment : ComposeFragment() { + + private val viewModel: LinkedDeviceAccountSettingsViewModel by viewModels() + + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(state.oneTimeEvent) { + viewModel.onEvent(LinkedDeviceAccountSettingsEvent.ConsumeOneTimeEvent) + when (state.oneTimeEvent) { + OneTimeEvent.OpenLearnMore -> LinkedDeviceAccountLearnMoreBottomSheet.show(childFragmentManager) + OneTimeEvent.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() + OneTimeEvent.WipeData -> { + if (!ServiceUtil.getActivityManager(AppDependencies.application).clearApplicationUserData()) { + viewModel.onEvent(LinkedDeviceAccountSettingsEvent.DataWipeFailed) + } + } + OneTimeEvent.DeleteFailed -> Toast.makeText(requireContext(), R.string.preferences_account_delete_all_data_failed, Toast.LENGTH_LONG).show() + null -> Unit + } + } + + LinkedDeviceAccountSettingsScreen( + state = state, + onEvent = viewModel::onEvent + ) + } +} + +@Composable +private fun LinkedDeviceAccountSettingsScreen( + state: LinkedDeviceAccountSettingsState, + onEvent: (LinkedDeviceAccountSettingsEvent) -> Unit +) { + Scaffolds.Settings( + title = stringResource(R.string.AccountSettingsFragment__account), + onNavigationClick = { onEvent(LinkedDeviceAccountSettingsEvent.NavigateBackClicked) }, + navigationIcon = SignalIcons.ArrowStart.imageVector + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .then(rememberStatusBarColorNestedScrollModifier()) + ) { + item { + LinkedDeviceCallout(onLearnMoreClick = { onEvent(LinkedDeviceAccountSettingsEvent.LearnMoreClicked) }) + } + + item { + Dividers.Default() + } + + item { + Rows.TextRow( + text = stringResource(R.string.LinkedDeviceAccountSettingsFragment__delete_app_data), + label = stringResource(R.string.LinkedDeviceAccountSettingsFragment__this_will_remove_all_data), + onClick = { onEvent(LinkedDeviceAccountSettingsEvent.DeleteAppDataClicked) } + ) + } + } + } + + if (state.showDeleteConfirmationDialog) { + DeleteAppDataConfirmationDialog( + onConfirm = { onEvent(LinkedDeviceAccountSettingsEvent.DeleteConfirmed) }, + onDismiss = { onEvent(LinkedDeviceAccountSettingsEvent.DeleteDismissed) } + ) + } + + if (state.deleting) { + Dialogs.IndeterminateProgressDialog() + } +} + +@Composable +private fun LinkedDeviceCallout( + onLearnMoreClick: () -> Unit +) { + Row( + modifier = Modifier + .padding(horizontal = 24.dp, vertical = 12.dp) + .fillMaxWidth() + .background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(12.dp)) + .padding(20.dp) + ) { + Icon( + imageVector = SignalIcons.Devices.imageVector, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.width(24.dp)) + + Column { + Text( + text = stringResource(R.string.LinkedDeviceAccountSettingsFragment__this_is_a_linked_device), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = stringResource(R.string.LinkedDeviceAccountSettingsFragment__to_manage_your_account_settings), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Buttons.Small( + onClick = onLearnMoreClick, + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer) + ) { + Text(text = stringResource(R.string.LearnMoreTextView_learn_more)) + } + } + } +} + +@Composable +private fun DeleteAppDataConfirmationDialog( + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.LinkedDeviceAccountSettingsFragment__delete_app_data_question), + body = stringResource(R.string.LinkedDeviceAccountSettingsFragment__this_will_delete_all_data_and_messages), + confirm = stringResource(R.string.delete), + confirmColor = MaterialTheme.colorScheme.error, + onConfirm = onConfirm, + dismiss = stringResource(android.R.string.cancel), + onDismiss = onDismiss + ) +} + +@DayNightPreviews +@Composable +private fun LinkedDeviceAccountSettingsScreenPreview() { + Previews.Preview { + LinkedDeviceAccountSettingsScreen( + state = LinkedDeviceAccountSettingsState(), + onEvent = {} + ) + } +} + +@DayNightPreviews +@Composable +private fun LinkedDeviceAccountSettingsScreenDeleteConfirmationPreview() { + Previews.Preview { + LinkedDeviceAccountSettingsScreen( + state = LinkedDeviceAccountSettingsState(showDeleteConfirmationDialog = true), + onEvent = {} + ) + } +} + +@DayNightPreviews +@Composable +private fun LinkedDeviceAccountSettingsScreenDeletingPreview() { + Previews.Preview { + LinkedDeviceAccountSettingsScreen( + state = LinkedDeviceAccountSettingsState(deleting = true), + onEvent = {} + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsState.kt new file mode 100644 index 0000000000..c46f044f62 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsState.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account + +data class LinkedDeviceAccountSettingsState( + val showDeleteConfirmationDialog: Boolean = false, + val deleting: Boolean = false, + val oneTimeEvent: OneTimeEvent? = null +) { + /** One-time side effects the fragment must carry out (they need Android/fragment context). Consumed via [LinkedDeviceAccountSettingsEvent.ConsumeOneTimeEvent]. */ + sealed interface OneTimeEvent { + data object OpenLearnMore : OneTimeEvent + data object NavigateBack : OneTimeEvent + data object WipeData : OneTimeEvent + data object DeleteFailed : OneTimeEvent + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsViewModel.kt new file mode 100644 index 0000000000..aac29cbd53 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsViewModel.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account + +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.withContext +import org.signal.core.ui.compose.EventDrivenViewModel +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.components.settings.app.account.LinkedDeviceAccountSettingsState.OneTimeEvent +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.linkdevice.LinkDeviceRepository + +/** + * Drives the account settings screen shown on a linked (non-primary) device. Owns the screen state + * and, on delete, makes a best-effort attempt to unlink this device from the server before the + * fragment wipes local data. + */ +class LinkedDeviceAccountSettingsViewModel( + private val selfDeviceId: () -> Int = { SignalStore.account.deviceId }, + private val removeDevice: suspend (Int) -> Boolean = LinkDeviceRepository::removeDevice +) : EventDrivenViewModel(TAG) { + + companion object { + private val TAG = Log.tag(LinkedDeviceAccountSettingsViewModel::class) + } + + private val _state = MutableStateFlow(LinkedDeviceAccountSettingsState()) + val state: StateFlow = _state.asStateFlow() + + override suspend fun processEvent(event: LinkedDeviceAccountSettingsEvent) { + applyEvent(_state.value, event) { _state.value = it } + } + + @VisibleForTesting + suspend fun applyEvent(state: LinkedDeviceAccountSettingsState, event: LinkedDeviceAccountSettingsEvent, stateEmitter: (LinkedDeviceAccountSettingsState) -> Unit) { + when (event) { + LinkedDeviceAccountSettingsEvent.LearnMoreClicked -> stateEmitter(state.copy(oneTimeEvent = OneTimeEvent.OpenLearnMore)) + LinkedDeviceAccountSettingsEvent.NavigateBackClicked -> stateEmitter(state.copy(oneTimeEvent = OneTimeEvent.NavigateBack)) + LinkedDeviceAccountSettingsEvent.DeleteAppDataClicked -> stateEmitter(state.copy(showDeleteConfirmationDialog = true)) + LinkedDeviceAccountSettingsEvent.DeleteDismissed -> stateEmitter(state.copy(showDeleteConfirmationDialog = false)) + LinkedDeviceAccountSettingsEvent.DeleteConfirmed -> { + val deletingState = state.copy(showDeleteConfirmationDialog = false, deleting = true) + stateEmitter(deletingState) + withContext(Dispatchers.IO) { removeDevice(selfDeviceId()) } + stateEmitter(deletingState.copy(oneTimeEvent = OneTimeEvent.WipeData)) + } + LinkedDeviceAccountSettingsEvent.DataWipeFailed -> stateEmitter(state.copy(deleting = false, oneTimeEvent = OneTimeEvent.DeleteFailed)) + LinkedDeviceAccountSettingsEvent.ConsumeOneTimeEvent -> stateEmitter(state.copy(oneTimeEvent = null)) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt index 0c8c931e64..539296986b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt @@ -51,7 +51,7 @@ object LinkDeviceRepository { private val TAG = Log.tag(LinkDeviceRepository::class) private const val DECRYPTION_INFO = "deviceCreatedAt" - fun removeDevice(deviceId: Int): Boolean { + suspend fun removeDevice(deviceId: Int): Boolean { return when (val result = AppDependencies.linkDeviceApi.removeDevice(deviceId)) { is NetworkResult.Success -> { LinkedDeviceInactiveCheckJob.enqueue() 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 5dc9e66523..80f3f6a378 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 @@ -43,6 +43,13 @@ app:exitAnim="@anim/fragment_open_exit" app:popEnterAnim="@anim/fragment_close_enter" app:popExitAnim="@anim/fragment_close_exit" /> + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6dedd63fa1..e1479b729d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5112,6 +5112,32 @@ Failed to delete data + + + This is a linked device + + To manage your account settings, open Signal on your primary device. + + Delete app data + + This will remove all data from the Signal app on this device. + + Delete app data? + + This will delete all data and messages from the Signal app on this device. Your Signal account and data on your primary device or other linked devices will not be deleted. The app will close after this process is complete. + + + + Linked Devices + + Linked Devices let you access your Signal account and messages securely on any of your additional devices. + + You can link a desktop, tablet or additional phone + + Your primary device manages what\'s linked, you can link up to 5 devices. + + Some account settings are managed on your primary device only, including account settings and backups. + Transfer or restore account If you have previously registered a Signal account, you can transfer or restore your account and messages diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsViewModelTest.kt new file mode 100644 index 0000000000..a29b514260 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/LinkedDeviceAccountSettingsViewModelTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.account + +import assertk.assertThat +import assertk.assertions.hasSize +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.test.StandardTestDispatcher +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.Test +import org.thoughtcrime.securesms.components.settings.app.account.LinkedDeviceAccountSettingsState.OneTimeEvent + +@OptIn(ExperimentalCoroutinesApi::class) +class LinkedDeviceAccountSettingsViewModelTest { + + companion object { + private const val SELF_DEVICE_ID = 3 + } + + private val testDispatcher = StandardTestDispatcher() + + private lateinit var emittedStates: MutableList + private lateinit var stateEmitter: (LinkedDeviceAccountSettingsState) -> Unit + + private var removedDeviceId: Int? = null + + private lateinit var viewModel: LinkedDeviceAccountSettingsViewModel + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + emittedStates = mutableListOf() + stateEmitter = { state -> emittedStates.add(state) } + removedDeviceId = null + viewModel = LinkedDeviceAccountSettingsViewModel( + selfDeviceId = { SELF_DEVICE_ID }, + removeDevice = { deviceId -> + removedDeviceId = deviceId + true + } + ) + testDispatcher.scheduler.advanceUntilIdle() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `DeleteAppDataClicked shows the confirmation dialog`() = runTest { + viewModel.applyEvent(LinkedDeviceAccountSettingsState(), LinkedDeviceAccountSettingsEvent.DeleteAppDataClicked, stateEmitter) + + assertThat(emittedStates).hasSize(1) + assertThat(emittedStates.last().showDeleteConfirmationDialog).isTrue() + } + + @Test + fun `DeleteDismissed hides the confirmation dialog`() = runTest { + viewModel.applyEvent(LinkedDeviceAccountSettingsState(showDeleteConfirmationDialog = true), LinkedDeviceAccountSettingsEvent.DeleteDismissed, stateEmitter) + + assertThat(emittedStates).hasSize(1) + assertThat(emittedStates.last().showDeleteConfirmationDialog).isFalse() + } + + @Test + fun `LearnMoreClicked requests the learn more event`() = runTest { + viewModel.applyEvent(LinkedDeviceAccountSettingsState(), LinkedDeviceAccountSettingsEvent.LearnMoreClicked, stateEmitter) + + assertThat(emittedStates.last().oneTimeEvent).isEqualTo(OneTimeEvent.OpenLearnMore) + } + + @Test + fun `NavigateBackClicked requests the navigate back event`() = runTest { + viewModel.applyEvent(LinkedDeviceAccountSettingsState(), LinkedDeviceAccountSettingsEvent.NavigateBackClicked, stateEmitter) + + assertThat(emittedStates.last().oneTimeEvent).isEqualTo(OneTimeEvent.NavigateBack) + } + + @Test + fun `DeleteConfirmed unlinks this device then requests the wipe`() = runTest { + viewModel.applyEvent(LinkedDeviceAccountSettingsState(showDeleteConfirmationDialog = true), LinkedDeviceAccountSettingsEvent.DeleteConfirmed, stateEmitter) + + assertThat(emittedStates).hasSize(2) + + val deletingState = emittedStates.first() + assertThat(deletingState.showDeleteConfirmationDialog).isFalse() + assertThat(deletingState.deleting).isTrue() + assertThat(deletingState.oneTimeEvent).isNull() + + val wipeState = emittedStates.last() + assertThat(wipeState.deleting).isTrue() + assertThat(wipeState.oneTimeEvent).isEqualTo(OneTimeEvent.WipeData) + + assertThat(removedDeviceId).isEqualTo(SELF_DEVICE_ID) + } + + @Test + fun `DataWipeFailed clears the spinner and reports the failure`() = runTest { + viewModel.applyEvent(LinkedDeviceAccountSettingsState(deleting = true), LinkedDeviceAccountSettingsEvent.DataWipeFailed, stateEmitter) + + assertThat(emittedStates).hasSize(1) + assertThat(emittedStates.last().deleting).isFalse() + assertThat(emittedStates.last().oneTimeEvent).isEqualTo(OneTimeEvent.DeleteFailed) + } + + @Test + fun `ConsumeOneTimeEvent clears the pending event`() = runTest { + viewModel.applyEvent(LinkedDeviceAccountSettingsState(oneTimeEvent = OneTimeEvent.WipeData), LinkedDeviceAccountSettingsEvent.ConsumeOneTimeEvent, stateEmitter) + + assertThat(emittedStates).hasSize(1) + assertThat(emittedStates.last().oneTimeEvent).isNull() + } +} diff --git a/feature/registration/src/main/java/org/signal/registration/screens/EventDrivenViewModel.kt b/core/ui/src/main/java/org/signal/core/ui/compose/EventDrivenViewModel.kt similarity index 96% rename from feature/registration/src/main/java/org/signal/registration/screens/EventDrivenViewModel.kt rename to core/ui/src/main/java/org/signal/core/ui/compose/EventDrivenViewModel.kt index f02fa01f5e..c21d0941f0 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/EventDrivenViewModel.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/EventDrivenViewModel.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.registration.screens +package org.signal.core.ui.compose import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/theme/SignalTheme.kt b/core/ui/src/main/java/org/signal/core/ui/compose/theme/SignalTheme.kt index b030fc50d8..81de34d4f5 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/theme/SignalTheme.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/theme/SignalTheme.kt @@ -132,7 +132,7 @@ private val lightExtendedColors = ExtendedColors( private val darkExtendedColors = ExtendedColors( neutralSurface = Color(0x14FFFFFF), colorOnCustom = Color(0xFFFFFFFF), - colorOnCustomVariant = Color(0xB3FFFFFF), + colorOnCustomVariant = Color(0x18FFFFFF), colorSurface1 = Color(0xFF23242A), colorSurface2 = Color(0xFF272A31), colorSurface3 = Color(0xFF2C2F37), 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 4d5080144c..fb2b2f3c29 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt @@ -21,9 +21,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +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.screens.EventDrivenViewModel import org.signal.registration.screens.restoreselection.RegisteredState import kotlin.reflect.KClass diff --git a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForLocalBackupViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForLocalBackupViewModel.kt index 5320154f43..1c3b6214e4 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForLocalBackupViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForLocalBackupViewModel.kt @@ -14,10 +14,10 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach 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.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack class EnterAepForLocalBackupViewModel( diff --git a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForRemoteBackupPostRegistrationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForRemoteBackupPostRegistrationViewModel.kt index 7f0ef6fbd4..df641edd3b 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForRemoteBackupPostRegistrationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForRemoteBackupPostRegistrationViewModel.kt @@ -12,13 +12,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.signal.core.models.AccountEntropyPool +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.NetworkController import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateTo class EnterAepForRemoteBackupPostRegistrationViewModel( diff --git a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForRemoteBackupPreRegistrationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForRemoteBackupPreRegistrationViewModel.kt index a947ab085b..def6b46bcb 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForRemoteBackupPreRegistrationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepForRemoteBackupPreRegistrationViewModel.kt @@ -15,13 +15,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.signal.core.models.AccountEntropyPool +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.NetworkController import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateTo class EnterAepForRemoteBackupPreRegistrationViewModel( diff --git a/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryCodePickerViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryCodePickerViewModel.kt index e2ba2a9b38..da589803b3 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryCodePickerViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryCodePickerViewModel.kt @@ -15,10 +15,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +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.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack /** diff --git a/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileViewModel.kt index cd87fbf81f..8187fdb218 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileViewModel.kt @@ -12,12 +12,12 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateTo /** diff --git a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModel.kt index 318c41fc8f..4850ac7259 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModel.kt @@ -13,11 +13,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationRepository import org.signal.registration.RestoreDecision -import org.signal.registration.screens.EventDrivenViewModel class DeviceTransferCompleteViewModel( private val repository: RegistrationRepository, diff --git a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsViewModel.kt index 59eeaf4b99..bdb7ab4e2c 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsViewModel.kt @@ -13,10 +13,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +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.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo diff --git a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressViewModel.kt index 63f021f1a8..6343cb1a1d 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressViewModel.kt @@ -23,13 +23,13 @@ import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.devicetransfer.DeviceToDeviceTransferService import org.signal.devicetransfer.NewDeviceRestoreStatus import org.signal.devicetransfer.TransferStatus import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo diff --git a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupViewModel.kt index 31666ccf12..990a0bb205 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupViewModel.kt @@ -31,6 +31,7 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.signal.core.models.AccountEntropyPool +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.devicetransfer.DeviceToDeviceTransferService import org.signal.devicetransfer.TransferStatus @@ -39,7 +40,6 @@ import org.signal.registration.NetworkController import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo import kotlin.time.Duration.Companion.seconds diff --git a/feature/registration/src/main/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityViewModel.kt index 2517a85308..a8633baf3e 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityViewModel.kt @@ -13,10 +13,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +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.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack class PhoneNumberDiscoverabilityViewModel( diff --git a/feature/registration/src/main/java/org/signal/registration/screens/linkaccount/LinkAccountViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/linkaccount/LinkAccountViewModel.kt index 8f722bad2a..c1e1e8a32a 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/linkaccount/LinkAccountViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/linkaccount/LinkAccountViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.ui.compose.QrCodeData import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult @@ -26,7 +27,6 @@ import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.quickrestore.QrState import org.signal.registration.screens.util.navigateTo diff --git a/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModel.kt index 8927868aa8..1c55084569 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModel.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.signal.archive.LocalBackupRestoreProgress import org.signal.core.models.AccountEntropyPool +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 @@ -26,7 +27,6 @@ import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute import org.signal.registration.RestoreDecision -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo diff --git a/feature/registration/src/main/java/org/signal/registration/screens/messagesync/MessageSyncViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/messagesync/MessageSyncViewModel.kt index fbb749c041..61024d2047 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/messagesync/MessageSyncViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/messagesync/MessageSyncViewModel.kt @@ -18,13 +18,13 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.bytes import org.signal.core.util.logging.Log import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateTo /** diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt index 9259686a08..6a82d98399 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.withTimeoutOrNull import org.signal.core.models.AccountEntropyPool +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.E164Util import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult @@ -29,7 +30,6 @@ import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.countrycode.Country import org.signal.registration.screens.countrycode.CountryUtils import org.signal.registration.screens.localbackuprestore.LocalBackupRestoreResult diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt index 2366c06a5f..e9fed94ada 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.NetworkController @@ -21,7 +22,6 @@ import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RestoreDecision -import org.signal.registration.screens.EventDrivenViewModel import kotlin.time.toKotlinDuration /** diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt index cffa6c14f7..8cafbdb7bb 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.signal.core.models.MasterKey +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.NetworkController @@ -22,7 +23,6 @@ import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo import kotlin.time.Duration.Companion.days diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt index dc545b35e1..c618faf2e9 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.signal.core.models.MasterKey +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.Hex import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult @@ -22,7 +23,6 @@ import org.signal.registration.NetworkController import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.util.SensitiveLog diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt index a6e64fe7e6..8bacfeda22 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.NetworkController @@ -22,7 +23,6 @@ import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute import org.signal.registration.RestoreDecision -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateTo /** diff --git a/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrViewModel.kt index a65c357703..ae2bffeb12 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.signal.core.models.AccountEntropyPool +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.ui.compose.QrCodeData import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult @@ -24,7 +25,6 @@ import org.signal.registration.NetworkController import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo diff --git a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModel.kt index f0f9ea5b3e..9316ee5f12 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.signal.core.models.AccountEntropyPool +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.NetworkController @@ -26,7 +27,6 @@ import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute import org.signal.registration.RestoreDecision -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo import kotlin.coroutines.CoroutineContext diff --git a/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModel.kt index 64941106f4..de2c8c1c89 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.signal.core.models.AccountEntropyPool +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.NetworkController @@ -25,7 +26,6 @@ import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute import org.signal.registration.RestoreDecision -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateTo /** diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt index f5e6a4894e..409729d9ac 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.NetworkController @@ -34,7 +35,6 @@ import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute -import org.signal.registration.screens.EventDrivenViewModel import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo import org.signal.registration.screens.verificationcode.VerificationCodeState.OneTimeEvent diff --git a/lib/network/src/main/java/org/signal/network/api/LinkDeviceApi.kt b/lib/network/src/main/java/org/signal/network/api/LinkDeviceApi.kt index d4426bacc6..69958c5949 100644 --- a/lib/network/src/main/java/org/signal/network/api/LinkDeviceApi.kt +++ b/lib/network/src/main/java/org/signal/network/api/LinkDeviceApi.kt @@ -66,10 +66,11 @@ class LinkDeviceApi( * * - 200: Success */ - fun removeDevice(deviceId: Int): NetworkResult { + suspend fun removeDevice(deviceId: Int): NetworkResult { val request = WebSocketRequestMessage.delete("/v1/devices/$deviceId") - return NetworkResult - .fromWebSocketRequest(authWebSocket, request) + return NetworkResult.fromWebSocketSuspend(NetworkResult.DefaultWebSocketConverter(Unit::class)) { + authWebSocket.requestSuspend(request) + } } /**