diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index f77453f334..e3431d7adb 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -4618,6 +4618,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -4749,6 +4757,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -4791,6 +4807,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -5114,6 +5138,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -5141,6 +5173,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -5374,6 +5414,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -5749,6 +5797,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -5770,6 +5826,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
@@ -6067,6 +6126,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
@@ -6410,6 +6472,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -6851,6 +6921,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -6879,6 +6957,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
diff --git a/registration/app/build.gradle.kts b/registration/app/build.gradle.kts
index 796167f55d..a7fd765eee 100644
--- a/registration/app/build.gradle.kts
+++ b/registration/app/build.gradle.kts
@@ -82,6 +82,9 @@ dependencies {
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
+ // Permissions
+ implementation(libs.accompanist.permissions)
+
// Compose BOM
platform(libs.androidx.compose.bom).let { composeBom ->
implementation(composeBom)
diff --git a/registration/app/src/main/java/org/signal/registration/sample/AppViewModel.kt b/registration/app/src/main/java/org/signal/registration/sample/AppViewModel.kt
deleted file mode 100644
index ada2bfe25c..0000000000
--- a/registration/app/src/main/java/org/signal/registration/sample/AppViewModel.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright 2025 Signal Messenger, LLC
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-package org.signal.registration.sample
-
-import androidx.lifecycle.ViewModel
-import org.signal.core.ui.navigation.ResultEventBus
-
-class AppViewModel : ViewModel() {
- val resultEventBus = ResultEventBus()
-}
diff --git a/registration/app/src/main/java/org/signal/registration/sample/MainActivity.kt b/registration/app/src/main/java/org/signal/registration/sample/MainActivity.kt
index de8b4f5e1c..d830350bd3 100644
--- a/registration/app/src/main/java/org/signal/registration/sample/MainActivity.kt
+++ b/registration/app/src/main/java/org/signal/registration/sample/MainActivity.kt
@@ -3,13 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+@file:OptIn(ExperimentalPermissionsApi::class)
+
package org.signal.registration.sample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.viewModels
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
@@ -39,15 +39,12 @@ import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
import kotlinx.serialization.Serializable
import org.signal.core.ui.compose.theme.SignalTheme
-import org.signal.core.ui.navigation.ResultEffect
-import org.signal.core.ui.navigation.ResultEventBus
-import org.signal.registration.NetworkController
-import org.signal.registration.RegistrationActivity
import org.signal.registration.RegistrationDependencies
-import org.signal.registration.StorageController
-import org.signal.registration.sample.MainActivity.Companion.REGISTRATION_RESULT
+import org.signal.registration.RegistrationNavHost
+import org.signal.registration.RegistrationRepository
import org.signal.registration.sample.screens.RegistrationCompleteScreen
import org.signal.registration.sample.screens.main.MainScreen
import org.signal.registration.sample.screens.main.MainScreenViewModel
@@ -89,6 +86,9 @@ sealed interface SampleRoute : NavKey {
@Serializable
data object Main : SampleRoute
+ @Serializable
+ data object Registration : SampleRoute
+
@Serializable
data object RegistrationComplete : SampleRoute
@@ -104,15 +104,14 @@ class MainActivity : ComponentActivity() {
const val REGISTRATION_RESULT = "registration_result"
}
- private val viewModel: AppViewModel by viewModels()
-
- private val registrationLauncher: ActivityResultLauncher = registerForActivityResult(RegistrationActivity.RegistrationContract()) { success ->
- viewModel.resultEventBus.sendResult(REGISTRATION_RESULT, success)
- }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ val registrationRepository = RegistrationRepository(
+ networkController = RegistrationDependencies.get().networkController,
+ storageController = RegistrationDependencies.get().storageController
+ )
+
setContent {
SignalTheme {
Surface(
@@ -122,11 +121,9 @@ class MainActivity : ComponentActivity() {
val backStack = rememberNavBackStack(SampleRoute.Main)
SampleNavHost(
- onLaunchRegistration = { registrationLauncher.launch(Unit) },
backStack = backStack,
- resultEventBus = viewModel.resultEventBus,
- storageController = RegistrationDependencies.get().storageController,
- networkController = RegistrationDependencies.get().networkController,
+ registrationRepository = registrationRepository,
+ registrationDependencies = RegistrationDependencies.get(),
onStartOver = {
backStack.clear()
backStack.add(SampleRoute.Main)
@@ -140,20 +137,18 @@ class MainActivity : ComponentActivity() {
@Composable
private fun SampleNavHost(
- onLaunchRegistration: () -> Unit,
onStartOver: () -> Unit,
+ registrationRepository: RegistrationRepository,
+ registrationDependencies: RegistrationDependencies,
backStack: NavBackStack,
- resultEventBus: ResultEventBus,
- storageController: StorageController,
- networkController: NetworkController,
modifier: Modifier = Modifier
) {
val entryProvider: (NavKey) -> NavEntry = entryProvider {
entry {
val viewModel: MainScreenViewModel = viewModel(
factory = MainScreenViewModel.Factory(
- storageController = storageController,
- onLaunchRegistration = onLaunchRegistration,
+ storageController = registrationDependencies.storageController,
+ onLaunchRegistration = { backStack.add(SampleRoute.Registration) },
onOpenPinSettings = { backStack.add(SampleRoute.PinSettings) }
)
)
@@ -164,19 +159,22 @@ private fun SampleNavHost(
onPauseOrDispose { }
}
- ResultEffect(resultEventBus, REGISTRATION_RESULT) { success ->
- if (success) {
- viewModel.refreshData()
- backStack.add(SampleRoute.RegistrationComplete)
- }
- }
-
MainScreen(
state = state,
onEvent = { viewModel.onEvent(it) }
)
}
+ entry {
+ RegistrationNavHost(
+ registrationRepository,
+ modifier = Modifier.fillMaxSize(),
+ onRegistrationComplete = {
+ backStack.add(SampleRoute.RegistrationComplete)
+ }
+ )
+ }
+
entry {
RegistrationCompleteScreen(onStartOver = onStartOver)
}
@@ -186,7 +184,7 @@ private fun SampleNavHost(
) {
val viewModel: PinSettingsViewModel = viewModel(
factory = PinSettingsViewModel.Factory(
- networkController = networkController,
+ networkController = registrationDependencies.networkController,
onBack = { backStack.removeLastOrNull() }
)
)
diff --git a/registration/lib/src/main/java/org/signal/registration/RegistrationActivity.kt b/registration/lib/src/main/java/org/signal/registration/RegistrationActivity.kt
index 89d674c3de..f31fa8ae97 100644
--- a/registration/lib/src/main/java/org/signal/registration/RegistrationActivity.kt
+++ b/registration/lib/src/main/java/org/signal/registration/RegistrationActivity.kt
@@ -7,11 +7,11 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.viewModels
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.rememberMultiplePermissionsState
import org.signal.core.ui.compose.theme.SignalTheme
-import org.signal.registration.screens.RegistrationHostScreen
/**
* Activity entry point for the registration flow.
@@ -39,16 +39,11 @@ class RegistrationActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
- val permissionsState = rememberMultiplePermissionsState(
- permissions = viewModel.getRequiredPermissions()
- )
-
SignalTheme(incognitoKeyboardEnabled = false) {
Surface {
- RegistrationHostScreen(
+ RegistrationNavHost(
registrationRepository = repository,
- viewModel = viewModel,
- permissionsState = permissionsState,
+ modifier = Modifier.fillMaxSize(),
onRegistrationComplete = {
setResult(RESULT_OK)
finish()
diff --git a/registration/lib/src/main/java/org/signal/registration/RegistrationNavigation.kt b/registration/lib/src/main/java/org/signal/registration/RegistrationNavigation.kt
index 91ce71dcf1..fd07d11aa9 100644
--- a/registration/lib/src/main/java/org/signal/registration/RegistrationNavigation.kt
+++ b/registration/lib/src/main/java/org/signal/registration/RegistrationNavigation.kt
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+@file:OptIn(ExperimentalPermissionsApi::class)
+
package org.signal.registration
import android.os.Parcelable
@@ -15,7 +17,6 @@ import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -27,6 +28,7 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
+import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import org.signal.core.ui.navigation.ResultEffect
@@ -48,6 +50,8 @@ import org.signal.registration.screens.pinentry.PinEntryScreen
import org.signal.registration.screens.restore.RestoreViaQrScreen
import org.signal.registration.screens.restore.RestoreViaQrScreenEvents
import org.signal.registration.screens.restore.RestoreViaQrState
+import org.signal.registration.screens.util.navigateBack
+import org.signal.registration.screens.util.navigateTo
import org.signal.registration.screens.verificationcode.VerificationCodeScreen
import org.signal.registration.screens.verificationcode.VerificationCodeViewModel
import org.signal.registration.screens.welcome.WelcomeScreen
@@ -113,8 +117,9 @@ private const val CAPTCHA_RESULT = "captcha_token"
/**
* Sets up the navigation graph for the registration flow using Navigation 3.
*
- * @param registrationViewModel The shared ViewModel for the registration flow.
- * @param permissionsState The permissions state managed at the activity level.
+ * @param registrationRepository The repository for registration data.
+ * @param registrationViewModel Optional ViewModel for testing. If null, creates one internally.
+ * @param permissionsState Optional permissions state for testing. If null, creates one internally.
* @param modifier Modifier to be applied to the NavDisplay.
* @param onRegistrationComplete Callback invoked when registration is successfully completed.
*/
@@ -122,20 +127,23 @@ private const val CAPTCHA_RESULT = "captcha_token"
@Composable
fun RegistrationNavHost(
registrationRepository: RegistrationRepository,
- registrationViewModel: RegistrationViewModel,
- permissionsState: MultiplePermissionsState,
+ registrationViewModel: RegistrationViewModel? = null,
+ permissionsState: MultiplePermissionsState? = null,
modifier: Modifier = Modifier,
onRegistrationComplete: () -> Unit = {}
) {
- val registrationState by registrationViewModel.state.collectAsStateWithLifecycle()
- val navigator = remember { RegistrationNavigator(eventEmitter = registrationViewModel::onEvent) }
+ val viewModel: RegistrationViewModel = registrationViewModel ?: viewModel(
+ factory = RegistrationViewModel.Factory(registrationRepository)
+ )
+
+ val registrationState by viewModel.state.collectAsStateWithLifecycle()
+ val permissions: MultiplePermissionsState = permissionsState ?: rememberMultiplePermissionsState(viewModel.getRequiredPermissions())
val entryProvider = entryProvider {
- registrationEntries(
+ navigationEntries(
registrationRepository = registrationRepository,
- registrationViewModel = registrationViewModel,
- permissionsState = permissionsState,
- navigator = navigator,
+ registrationViewModel = viewModel,
+ permissionsState = permissions,
onRegistrationComplete = onRegistrationComplete
)
}
@@ -152,7 +160,7 @@ fun RegistrationNavHost(
NavDisplay(
entries = entries,
- onBack = { registrationViewModel.onEvent(RegistrationFlowEvent.NavigateBack) },
+ onBack = { viewModel.onEvent(RegistrationFlowEvent.NavigateBack) },
modifier = modifier,
transitionSpec = {
// Slide in from right and fade in when navigating forward
@@ -204,25 +212,22 @@ fun RegistrationNavHost(
)
}
-/**
- * Defines all navigation entries for the registration flow.
- */
-@OptIn(ExperimentalPermissionsApi::class)
-private fun EntryProviderScope.registrationEntries(
+private fun EntryProviderScope.navigationEntries(
registrationRepository: RegistrationRepository,
registrationViewModel: RegistrationViewModel,
permissionsState: MultiplePermissionsState,
- navigator: RegistrationNavigator,
onRegistrationComplete: () -> Unit
) {
+ val parentEventEmitter: (RegistrationFlowEvent) -> Unit = registrationViewModel::onEvent
+
// --- Welcome Screen
entry {
WelcomeScreen(
onEvent = { event ->
when (event) {
- WelcomeScreenEvents.Continue -> navigator.navigate(RegistrationRoute.Permissions(forRestore = false))
- WelcomeScreenEvents.DoesNotHaveOldPhone -> navigator.navigate(RegistrationRoute.Restore)
- WelcomeScreenEvents.HasOldPhone -> navigator.navigate(RegistrationRoute.Permissions(forRestore = true))
+ WelcomeScreenEvents.Continue -> parentEventEmitter.navigateTo(RegistrationRoute.Permissions(forRestore = false))
+ WelcomeScreenEvents.DoesNotHaveOldPhone -> parentEventEmitter.navigateTo(RegistrationRoute.Restore)
+ WelcomeScreenEvents.HasOldPhone -> parentEventEmitter.navigateTo(RegistrationRoute.Permissions(forRestore = true))
}
}
)
@@ -234,9 +239,9 @@ private fun EntryProviderScope.registrationEntries(
permissionsState = permissionsState,
onProceed = {
if (key.forRestore) {
- navigator.navigate(RegistrationRoute.RestoreViaQr)
+ parentEventEmitter.navigateTo(RegistrationRoute.RestoreViaQr)
} else {
- navigator.navigate(RegistrationRoute.PhoneNumberEntry)
+ parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberEntry)
}
}
)
@@ -281,10 +286,10 @@ private fun EntryProviderScope.registrationEntries(
when (event) {
is CaptchaScreenEvents.CaptchaCompleted -> {
registrationViewModel.resultBus.sendResult(CAPTCHA_RESULT, event.token)
- navigator.goBack()
+ parentEventEmitter.navigateBack()
}
CaptchaScreenEvents.Cancel -> {
- navigator.goBack()
+ parentEventEmitter.navigateBack()
}
}
}
@@ -370,7 +375,7 @@ private fun EntryProviderScope.registrationEntries(
when (event) {
AccountLockedScreenEvents.Next -> {
// TODO: Navigate to appropriate next screen (likely back to welcome or phone entry)
- navigator.navigate(RegistrationRoute.Welcome)
+ parentEventEmitter.navigateTo(RegistrationRoute.Welcome)
}
AccountLockedScreenEvents.LearnMore -> {
// TODO: Open learn more URL
@@ -393,7 +398,7 @@ private fun EntryProviderScope.registrationEntries(
// TODO: Retry QR code generation
}
RestoreViaQrScreenEvents.Cancel -> {
- navigator.goBack()
+ parentEventEmitter.navigateBack()
}
RestoreViaQrScreenEvents.UseProxy -> {
// TODO: Navigate to proxy settings
@@ -420,20 +425,3 @@ private fun EntryProviderScope.registrationEntries(
}
}
}
-
-/**
- * Navigator for the registration flow.
- * Handles navigation events by updating the back stack.
- */
-private class RegistrationNavigator(
- private val eventEmitter: (RegistrationFlowEvent) -> Unit
-) {
-
- fun navigate(route: RegistrationRoute) {
- eventEmitter(RegistrationFlowEvent.NavigateToScreen(route))
- }
-
- fun goBack() {
- eventEmitter(RegistrationFlowEvent.NavigateBack)
- }
-}
diff --git a/registration/lib/src/main/java/org/signal/registration/screens/RegistrationHostScreen.kt b/registration/lib/src/main/java/org/signal/registration/screens/RegistrationHostScreen.kt
deleted file mode 100644
index e30748cbd7..0000000000
--- a/registration/lib/src/main/java/org/signal/registration/screens/RegistrationHostScreen.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2025 Signal Messenger, LLC
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-package org.signal.registration.screens
-
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.MultiplePermissionsState
-import org.signal.registration.RegistrationNavHost
-import org.signal.registration.RegistrationRepository
-import org.signal.registration.RegistrationViewModel
-
-/**
- * Entry point for the registration flow.
- *
- * This composable sets up the entire registration navigation flow and can be
- * embedded into the main app's navigation or launched as a standalone flow.
- *
- * @param viewModel The shared ViewModel for the registration flow.
- * @param permissionsState The permissions state managed at the activity level.
- * @param modifier Modifier to be applied to the root container.
- * @param onRegistrationComplete Callback invoked when the registration process is successfully completed.
- */
-@OptIn(ExperimentalPermissionsApi::class)
-@Composable
-fun RegistrationHostScreen(
- registrationRepository: RegistrationRepository,
- viewModel: RegistrationViewModel,
- permissionsState: MultiplePermissionsState,
- modifier: Modifier = Modifier,
- onRegistrationComplete: () -> Unit = {}
-) {
- RegistrationNavHost(
- registrationRepository = registrationRepository,
- registrationViewModel = viewModel,
- permissionsState = permissionsState,
- modifier = modifier.fillMaxSize(),
- onRegistrationComplete = onRegistrationComplete
- )
-}