mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 16:19:33 +01:00
Make regV5 resumable if the app closes.
This commit is contained in:
committed by
Michelle Tang
parent
c7ec3ab837
commit
f09bf5b14c
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.registration
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.models.MasterKey
|
||||
|
||||
/**
|
||||
* A serializable snapshot of [RegistrationFlowState] fields that need to survive app kills.
|
||||
*
|
||||
* Fields like [RegistrationFlowState.accountEntropyPool] and [RegistrationFlowState.temporaryMasterKey]
|
||||
* are reconstructed from dedicated proto fields, not from this JSON snapshot.
|
||||
* [RegistrationFlowState.preExistingRegistrationData] is loaded from permanent storage.
|
||||
*/
|
||||
@Serializable
|
||||
data class PersistedFlowState(
|
||||
val backStack: List<RegistrationRoute>,
|
||||
val sessionMetadata: NetworkController.SessionMetadata?,
|
||||
val sessionE164: String?,
|
||||
val doNotAttemptRecoveryPassword: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Extracts the persistable fields from a [RegistrationFlowState].
|
||||
*/
|
||||
fun RegistrationFlowState.toPersistedFlowState(): PersistedFlowState {
|
||||
return PersistedFlowState(
|
||||
backStack = backStack,
|
||||
sessionMetadata = sessionMetadata,
|
||||
sessionE164 = sessionE164,
|
||||
doNotAttemptRecoveryPassword = doNotAttemptRecoveryPassword
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a full [RegistrationFlowState] from persisted data and separately-stored fields.
|
||||
*
|
||||
* @param accountEntropyPool Restored from the proto's dedicated `accountEntropyPool` field.
|
||||
* @param temporaryMasterKey Restored from the proto's dedicated `temporaryMasterKey` field.
|
||||
* @param preExistingRegistrationData Loaded from permanent storage via [StorageController.getPreExistingRegistrationData].
|
||||
*/
|
||||
fun PersistedFlowState.toRegistrationFlowState(
|
||||
accountEntropyPool: AccountEntropyPool?,
|
||||
temporaryMasterKey: MasterKey?,
|
||||
preExistingRegistrationData: PreExistingRegistrationData?
|
||||
): RegistrationFlowState {
|
||||
return RegistrationFlowState(
|
||||
backStack = backStack,
|
||||
sessionMetadata = sessionMetadata,
|
||||
sessionE164 = sessionE164,
|
||||
accountEntropyPool = accountEntropyPool,
|
||||
temporaryMasterKey = temporaryMasterKey,
|
||||
preExistingRegistrationData = preExistingRegistrationData,
|
||||
doNotAttemptRecoveryPassword = doNotAttemptRecoveryPassword
|
||||
)
|
||||
}
|
||||
@@ -37,5 +37,8 @@ data class RegistrationFlowState(
|
||||
val preExistingRegistrationData: PreExistingRegistrationData? = null,
|
||||
|
||||
/** If true, do not attempt any flows where we generate RRP's. Create a session instead. */
|
||||
val doNotAttemptRecoveryPassword: Boolean = false
|
||||
val doNotAttemptRecoveryPassword: Boolean = false,
|
||||
|
||||
/** If true, the ViewModel is still deciding whether to restore a previous flow or start fresh. */
|
||||
val isRestoringNavigationState: Boolean = true
|
||||
) : Parcelable, DebugLoggable
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
package org.signal.registration
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -62,6 +66,7 @@ import org.signal.registration.screens.welcome.WelcomeScreenEvents
|
||||
* Navigation routes for the registration flow.
|
||||
* Using @Serializable and NavKey for type-safe navigation with Navigation 3.
|
||||
*/
|
||||
@Serializable
|
||||
@Parcelize
|
||||
sealed interface RegistrationRoute : NavKey, Parcelable {
|
||||
@Serializable
|
||||
@@ -77,7 +82,7 @@ sealed interface RegistrationRoute : NavKey, Parcelable {
|
||||
data object CountryCodePicker : RegistrationRoute
|
||||
|
||||
@Serializable
|
||||
data class VerificationCodeEntry(val session: NetworkController.SessionMetadata, val e164: String) : RegistrationRoute
|
||||
data object VerificationCodeEntry : RegistrationRoute
|
||||
|
||||
@Serializable
|
||||
data class Captcha(val session: NetworkController.SessionMetadata) : RegistrationRoute
|
||||
@@ -150,6 +155,13 @@ fun RegistrationNavHost(
|
||||
val registrationState by viewModel.state.collectAsStateWithLifecycle()
|
||||
val permissions: MultiplePermissionsState = permissionsState ?: rememberMultiplePermissionsState(viewModel.getRequiredPermissions())
|
||||
|
||||
if (registrationState.isRestoringNavigationState) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val entryProvider = entryProvider {
|
||||
navigationEntries(
|
||||
registrationRepository = registrationRepository,
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.models.MasterKey
|
||||
@@ -410,6 +411,81 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
return storageController.getPreExistingRegistrationData()
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the current flow state as JSON in the in-progress registration data proto.
|
||||
*/
|
||||
suspend fun saveFlowState(state: RegistrationFlowState) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val json = flowStateJson.encodeToString(PersistedFlowState.serializer(), state.toPersistedFlowState())
|
||||
storageController.updateInProgressRegistrationData {
|
||||
flowStateJson = json
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to save flow state", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the flow state from disk. Returns null if no state is saved or deserialization fails.
|
||||
* Reconstructs [RegistrationFlowState.accountEntropyPool] and [RegistrationFlowState.temporaryMasterKey]
|
||||
* from their dedicated proto fields, and loads [RegistrationFlowState.preExistingRegistrationData]
|
||||
* from permanent storage.
|
||||
*/
|
||||
suspend fun restoreFlowState(): RegistrationFlowState? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val data = storageController.readInProgressRegistrationData()
|
||||
if (data.flowStateJson.isEmpty()) return@withContext null
|
||||
|
||||
val persisted = flowStateJson.decodeFromString(PersistedFlowState.serializer(), data.flowStateJson)
|
||||
|
||||
val aep = data.accountEntropyPool.takeIf { it.isNotEmpty() }?.let { AccountEntropyPool(it) }
|
||||
val masterKey = data.temporaryMasterKey.takeIf { it.size > 0 }?.let { MasterKey(it.toByteArray()) }
|
||||
val preExisting = storageController.getPreExistingRegistrationData()
|
||||
|
||||
persisted.toRegistrationFlowState(
|
||||
accountEntropyPool = aep,
|
||||
temporaryMasterKey = masterKey,
|
||||
preExistingRegistrationData = preExisting
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to restore flow state", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any persisted flow state JSON from the in-progress registration data.
|
||||
*/
|
||||
suspend fun clearFlowState() = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
storageController.updateInProgressRegistrationData {
|
||||
flowStateJson = ""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to clear flow state", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a registration session by fetching its current status from the server.
|
||||
* Returns fresh [SessionMetadata] on success, or null if the session is expired/invalid.
|
||||
*/
|
||||
suspend fun validateSession(sessionId: String): SessionMetadata? = withContext(Dispatchers.IO) {
|
||||
when (val result = networkController.getSession(sessionId)) {
|
||||
is RegistrationNetworkResult.Success -> result.data
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the in-progress registration data indicates a completed registration
|
||||
* (i.e. both ACI and PNI have been saved).
|
||||
*/
|
||||
suspend fun isRegistered(): Boolean = withContext(Dispatchers.IO) {
|
||||
val data = storageController.readInProgressRegistrationData()
|
||||
data.aci.isNotEmpty() && data.pni.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun generateKeyMaterial(
|
||||
existingAccountEntropyPool: AccountEntropyPool? = null,
|
||||
existingAciIdentityKeyPair: IdentityKeyPair? = null,
|
||||
@@ -488,5 +564,6 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationRepository::class)
|
||||
private val flowStateJson = Json { ignoreUnknownKeys = true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.createSavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -37,9 +38,17 @@ class RegistrationViewModel(private val repository: RegistrationRepository, save
|
||||
val resultBus = ResultEventBus()
|
||||
|
||||
init {
|
||||
_state.value = _state.value.copy(isRestoringNavigationState = true)
|
||||
viewModelScope.launch {
|
||||
repository.getPreExistingRegistrationData()?.let {
|
||||
_state.value = _state.value.copy(preExistingRegistrationData = it)
|
||||
val restored = repository.restoreFlowState()
|
||||
if (restored != null) {
|
||||
Log.i(TAG, "[init] Restored flow state from disk. Backstack size: ${restored.backStack.size}, hasSession: ${restored.sessionMetadata != null}")
|
||||
_state.value = validateRestoredState(restored).copy(isRestoringNavigationState = false)
|
||||
} else {
|
||||
_state.value = _state.value.copy(
|
||||
preExistingRegistrationData = repository.getPreExistingRegistrationData(),
|
||||
isRestoringNavigationState = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,11 +56,15 @@ class RegistrationViewModel(private val repository: RegistrationRepository, save
|
||||
fun onEvent(event: RegistrationFlowEvent) {
|
||||
Log.d(TAG, "[Event] $event")
|
||||
_state.value = applyEvent(_state.value, event)
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
persistFlowState(event)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyEvent(state: RegistrationFlowState, event: RegistrationFlowEvent): RegistrationFlowState {
|
||||
return when (event) {
|
||||
is RegistrationFlowEvent.ResetState -> RegistrationFlowState()
|
||||
is RegistrationFlowEvent.ResetState -> RegistrationFlowState(isRestoringNavigationState = false)
|
||||
is RegistrationFlowEvent.SessionUpdated -> state.copy(sessionMetadata = event.session)
|
||||
is RegistrationFlowEvent.E164Chosen -> state.copy(sessionE164 = event.e164)
|
||||
is RegistrationFlowEvent.Registered -> state.copy(accountEntropyPool = event.accountEntropyPool)
|
||||
@@ -63,14 +76,43 @@ class RegistrationViewModel(private val repository: RegistrationRepository, save
|
||||
}
|
||||
|
||||
private fun applyNavigationToScreenEvent(inputState: RegistrationFlowState, event: RegistrationFlowEvent.NavigateToScreen): RegistrationFlowState {
|
||||
val state = inputState.copy(backStack = inputState.backStack + event.route)
|
||||
return inputState.copy(backStack = inputState.backStack + event.route)
|
||||
}
|
||||
|
||||
return when (event.route) {
|
||||
is RegistrationRoute.VerificationCodeEntry -> {
|
||||
state.copy(sessionMetadata = event.route.session, sessionE164 = event.route.e164)
|
||||
}
|
||||
else -> state
|
||||
/**
|
||||
* Validates a restored flow state by checking if the session is still valid.
|
||||
*
|
||||
* - If the session is still valid, updates session metadata with fresh data.
|
||||
* - If the session is expired and the user is already registered, nulls out the session
|
||||
* (post-registration screens like PinCreate don't need a session).
|
||||
* - If the session is expired and the user is NOT registered, resets the backstack to
|
||||
* PhoneNumberEntry with the phone number pre-filled so the user can re-submit.
|
||||
*/
|
||||
private suspend fun validateRestoredState(state: RegistrationFlowState): RegistrationFlowState {
|
||||
val sessionMetadata = state.sessionMetadata ?: return state
|
||||
|
||||
val freshSession = repository.validateSession(sessionMetadata.id)
|
||||
if (freshSession != null) {
|
||||
Log.i(TAG, "[validateRestoredState] Session still valid.")
|
||||
return state.copy(sessionMetadata = freshSession)
|
||||
}
|
||||
|
||||
Log.i(TAG, "[validateRestoredState] Session expired/invalid.")
|
||||
|
||||
if (repository.isRegistered()) {
|
||||
Log.i(TAG, "[validateRestoredState] User is registered, proceeding without session.")
|
||||
return state.copy(sessionMetadata = null)
|
||||
}
|
||||
|
||||
Log.i(TAG, "[validateRestoredState] User is NOT registered, resetting to PhoneNumberEntry.")
|
||||
return state.copy(
|
||||
backStack = listOf(
|
||||
RegistrationRoute.Welcome,
|
||||
RegistrationRoute.Permissions(nextRoute = RegistrationRoute.PhoneNumberEntry),
|
||||
RegistrationRoute.PhoneNumberEntry
|
||||
),
|
||||
sessionMetadata = null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,6 +143,27 @@ class RegistrationViewModel(private val repository: RegistrationRepository, save
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun persistFlowState(event: RegistrationFlowEvent) {
|
||||
when (event) {
|
||||
is RegistrationFlowEvent.ResetState -> repository.clearFlowState()
|
||||
is RegistrationFlowEvent.NavigateToScreen -> {
|
||||
if (event.route is RegistrationRoute.FullyComplete) {
|
||||
repository.clearFlowState()
|
||||
} else {
|
||||
repository.saveFlowState(_state.value)
|
||||
}
|
||||
}
|
||||
is RegistrationFlowEvent.NavigateBack,
|
||||
is RegistrationFlowEvent.SessionUpdated,
|
||||
is RegistrationFlowEvent.E164Chosen,
|
||||
is RegistrationFlowEvent.RecoveryPasswordInvalid -> repository.saveFlowState(_state.value)
|
||||
|
||||
// No need to persist anything new, fields accounted for in proto already
|
||||
is RegistrationFlowEvent.Registered,
|
||||
is RegistrationFlowEvent.MasterKeyRestoredFromSvr -> { }
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(private val repository: RegistrationRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: KClass<T>, extras: CreationExtras): T {
|
||||
return RegistrationViewModel(repository, extras.createSavedStateHandle()) as T
|
||||
|
||||
@@ -276,14 +276,17 @@ class PhoneNumberEntryViewModel(
|
||||
is NetworkController.RegistrationNetworkResult.Failure<NetworkController.CreateSessionError> -> {
|
||||
return when (response.error) {
|
||||
is NetworkController.CreateSessionError.InvalidRequest -> {
|
||||
Log.w(TAG, "[CreateSession] Invalid request when creating session. Message: ${response.error.message}")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
}
|
||||
is NetworkController.CreateSessionError.RateLimited -> {
|
||||
Log.w(TAG, "[CreateSession] Rate limited (retryAfter: ${response.error.retryAfter}).")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.RateLimited(response.error.retryAfter))
|
||||
}
|
||||
}
|
||||
}
|
||||
is NetworkController.RegistrationNetworkResult.NetworkError -> {
|
||||
Log.w(TAG, "[CreateSession] Network error.", response.exception)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
}
|
||||
is NetworkController.RegistrationNetworkResult.ApplicationError -> {
|
||||
@@ -304,23 +307,26 @@ class PhoneNumberEntryViewModel(
|
||||
Log.d(TAG, "Received push challenge token, submitting...")
|
||||
val updateResult = repository.submitPushChallengeToken(sessionMetadata.id, pushChallengeToken)
|
||||
sessionMetadata = when (updateResult) {
|
||||
is NetworkController.RegistrationNetworkResult.Success -> updateResult.data
|
||||
is NetworkController.RegistrationNetworkResult.Success -> {
|
||||
Log.d(TAG, "[SubmitPushChallengeToken] Successfully submitted push challenge token.")
|
||||
updateResult.data
|
||||
}
|
||||
is NetworkController.RegistrationNetworkResult.Failure -> {
|
||||
Log.w(TAG, "Failed to submit push challenge token: ${updateResult.error}")
|
||||
Log.w(TAG, "[SubmitPushChallengeToken] Failed to submit push challenge token: ${updateResult.error}")
|
||||
sessionMetadata
|
||||
}
|
||||
is NetworkController.RegistrationNetworkResult.NetworkError -> {
|
||||
Log.w(TAG, "Network error submitting push challenge token", updateResult.exception)
|
||||
Log.w(TAG, "[SubmitPushChallengeToken] Network error submitting push challenge token", updateResult.exception)
|
||||
sessionMetadata
|
||||
}
|
||||
is NetworkController.RegistrationNetworkResult.ApplicationError -> {
|
||||
Log.w(TAG, "Application error submitting push challenge token", updateResult.exception)
|
||||
Log.w(TAG, "[SubmitPushChallengeToken] Application error submitting push challenge token", updateResult.exception)
|
||||
sessionMetadata
|
||||
}
|
||||
}
|
||||
state = state.copy(sessionMetadata = sessionMetadata)
|
||||
} else {
|
||||
Log.d(TAG, "Push challenge token not received within timeout")
|
||||
Log.d(TAG, "[SubmitPushChallengeToken] Push challenge token not received within timeout")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,41 +343,49 @@ class PhoneNumberEntryViewModel(
|
||||
|
||||
sessionMetadata = when (verificationCodeResponse) {
|
||||
is NetworkController.RegistrationNetworkResult.Success<NetworkController.SessionMetadata> -> {
|
||||
Log.d(TAG, "[RequestVerificationCode] Successfully requested verification code.")
|
||||
verificationCodeResponse.data
|
||||
}
|
||||
is NetworkController.RegistrationNetworkResult.Failure<NetworkController.RequestVerificationCodeError> -> {
|
||||
return when (verificationCodeResponse.error) {
|
||||
is NetworkController.RequestVerificationCodeError.InvalidRequest -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Invalid request when requesting verification code. Message: ${verificationCodeResponse.error.message}")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.RateLimited -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Rate limited (retryAfter: ${verificationCodeResponse.error.retryAfter}).")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.RateLimited(verificationCodeResponse.error.retryAfter))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.CouldNotFulfillWithRequestedTransport -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Could not fulfill with requested transport.")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.CouldNotRequestCodeWithSelectedTransport)
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.InvalidSessionId -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Invalid session ID when requesting verification code.")
|
||||
parentEventEmitter(RegistrationFlowEvent.ResetState)
|
||||
state
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.MissingRequestInformationOrAlreadyVerified -> {
|
||||
Log.w(TAG, "When requesting verification code, missing request information or already verified.")
|
||||
Log.w(TAG, "[RequestVerificationCode] Missing request information or already verified.")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.SessionNotFound -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Session not found when requesting verification code.")
|
||||
parentEventEmitter(RegistrationFlowEvent.ResetState)
|
||||
state
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.ThirdPartyServiceError -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Third party service error.")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.ThirdPartyError)
|
||||
}
|
||||
}
|
||||
}
|
||||
is NetworkController.RegistrationNetworkResult.NetworkError -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Network error.", verificationCodeResponse.exception)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
}
|
||||
is NetworkController.RegistrationNetworkResult.ApplicationError -> {
|
||||
Log.w(TAG, "Unknown error when creating session.", verificationCodeResponse.exception)
|
||||
Log.w(TAG, "[RequestVerificationCode] Unknown error when creating session.", verificationCodeResponse.exception)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
}
|
||||
}
|
||||
@@ -383,7 +397,9 @@ class PhoneNumberEntryViewModel(
|
||||
return state
|
||||
}
|
||||
|
||||
parentEventEmitter.navigateTo(RegistrationRoute.VerificationCodeEntry(sessionMetadata, e164))
|
||||
parentEventEmitter(RegistrationFlowEvent.SessionUpdated(sessionMetadata))
|
||||
parentEventEmitter(RegistrationFlowEvent.E164Chosen(e164))
|
||||
parentEventEmitter.navigateTo(RegistrationRoute.VerificationCodeEntry)
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -470,9 +486,9 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val e164 = "+${inputState.countryCode}${inputState.nationalNumber}"
|
||||
|
||||
parentEventEmitter.navigateTo(RegistrationRoute.VerificationCodeEntry(sessionMetadata, e164))
|
||||
parentEventEmitter(RegistrationFlowEvent.SessionUpdated(sessionMetadata))
|
||||
parentEventEmitter(RegistrationFlowEvent.E164Chosen("+${inputState.countryCode}${inputState.nationalNumber}"))
|
||||
parentEventEmitter.navigateTo(RegistrationRoute.VerificationCodeEntry)
|
||||
return state
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ message RegistrationData {
|
||||
|
||||
// Provisioning data (from saveProvisioningData)
|
||||
ProvisioningData provisioningData = 20;
|
||||
|
||||
// JSON-serialized flow state snapshot (from saveFlowState/restoreFlowState)
|
||||
string flowStateJson = 21;
|
||||
}
|
||||
|
||||
message SvrCredential {
|
||||
|
||||
Reference in New Issue
Block a user