mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-04 20:34:14 +01:00
Remove OneTimeEvent pattern from regV5.
This commit is contained in:
+7
-5
@@ -30,7 +30,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -48,6 +47,7 @@ import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.WindowBreakpoint
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.rememberWindowBreakpoint
|
||||
@@ -84,10 +84,12 @@ fun CreateProfileScreen(
|
||||
pickAvatarLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
||||
}
|
||||
|
||||
LaunchedEffect(state.oneTimeEvent) {
|
||||
if (state.oneTimeEvent != null) {
|
||||
onEvent(CreateProfileScreenEvents.ConsumeOneTimeEvent)
|
||||
}
|
||||
if (state.showUploadFailedDialog) {
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = stringResource(R.string.VerificationCodeScreen__an_unexpected_error_occurred),
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = { onEvent(CreateProfileScreenEvents.UploadFailedDialogDismissed) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.isLoading) {
|
||||
|
||||
+1
-1
@@ -25,5 +25,5 @@ sealed class CreateProfileScreenEvents {
|
||||
data object WhoCanFindMeClicked : CreateProfileScreenEvents()
|
||||
data class DiscoverabilityChanged(val discoverable: Boolean) : CreateProfileScreenEvents()
|
||||
data object NextClicked : CreateProfileScreenEvents()
|
||||
data object ConsumeOneTimeEvent : CreateProfileScreenEvents()
|
||||
data object UploadFailedDialogDismissed : CreateProfileScreenEvents()
|
||||
}
|
||||
|
||||
+4
-8
@@ -12,14 +12,14 @@ data class CreateProfileState(
|
||||
val discoverableByPhoneNumber: Boolean = true,
|
||||
val isLoading: Boolean = true,
|
||||
val isSubmitting: Boolean = false,
|
||||
val oneTimeEvent: OneTimeEvent? = null
|
||||
val showUploadFailedDialog: Boolean = false
|
||||
) {
|
||||
|
||||
val isFormValid: Boolean
|
||||
get() = givenName.trim().isNotEmpty()
|
||||
|
||||
override fun toString(): String {
|
||||
return "CreateProfileState(givenName=${givenName.length} chars, familyName=${familyName.length} chars, avatar=${avatar?.size ?: 0} bytes, discoverableByPhoneNumber=$discoverableByPhoneNumber, isLoading=$isLoading, isSubmitting=$isSubmitting, oneTimeEvent=$oneTimeEvent)"
|
||||
return "CreateProfileState(givenName=${givenName.length} chars, familyName=${familyName.length} chars, avatar=${avatar?.size ?: 0} bytes, discoverableByPhoneNumber=$discoverableByPhoneNumber, isLoading=$isLoading, isSubmitting=$isSubmitting, showUploadFailedDialog=$showUploadFailedDialog)"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -36,7 +36,7 @@ data class CreateProfileState(
|
||||
if (discoverableByPhoneNumber != other.discoverableByPhoneNumber) return false
|
||||
if (isLoading != other.isLoading) return false
|
||||
if (isSubmitting != other.isSubmitting) return false
|
||||
if (oneTimeEvent != other.oneTimeEvent) return false
|
||||
if (showUploadFailedDialog != other.showUploadFailedDialog) return false
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -47,11 +47,7 @@ data class CreateProfileState(
|
||||
result = 31 * result + discoverableByPhoneNumber.hashCode()
|
||||
result = 31 * result + isLoading.hashCode()
|
||||
result = 31 * result + isSubmitting.hashCode()
|
||||
result = 31 * result + (oneTimeEvent?.hashCode() ?: 0)
|
||||
result = 31 * result + showUploadFailedDialog.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
sealed interface OneTimeEvent {
|
||||
data object UploadFailed : OneTimeEvent
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -97,8 +97,8 @@ class CreateProfileViewModel(
|
||||
stateEmitter(state.copy(isSubmitting = true))
|
||||
submitProfile(state, parentEventEmitter, repository, stateEmitter)
|
||||
}
|
||||
CreateProfileScreenEvents.ConsumeOneTimeEvent -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = null))
|
||||
CreateProfileScreenEvents.UploadFailedDialogDismissed -> {
|
||||
stateEmitter(state.copy(showUploadFailedDialog = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,15 +126,15 @@ class CreateProfileViewModel(
|
||||
}
|
||||
is RequestResult.NonSuccess -> {
|
||||
Log.w(TAG, "[submitProfile] Profile save failed: ${result.error}")
|
||||
stateEmitter(state.copy(isSubmitting = false, oneTimeEvent = CreateProfileState.OneTimeEvent.UploadFailed))
|
||||
stateEmitter(state.copy(isSubmitting = false, showUploadFailedDialog = true))
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[submitProfile] Network error saving profile.", result.networkError)
|
||||
stateEmitter(state.copy(isSubmitting = false, oneTimeEvent = CreateProfileState.OneTimeEvent.UploadFailed))
|
||||
stateEmitter(state.copy(isSubmitting = false, showUploadFailedDialog = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[submitProfile] Application error saving profile.", result.cause)
|
||||
stateEmitter(state.copy(isSubmitting = false, oneTimeEvent = CreateProfileState.OneTimeEvent.UploadFailed))
|
||||
stateEmitter(state.copy(isSubmitting = false, showUploadFailedDialog = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -7,5 +7,4 @@ package org.signal.registration.screens.devicetransfer.complete
|
||||
|
||||
sealed class DeviceTransferCompleteScreenEvents {
|
||||
data object ContinueClicked : DeviceTransferCompleteScreenEvents()
|
||||
data object ConsumeOneTimeEvent : DeviceTransferCompleteScreenEvents()
|
||||
}
|
||||
|
||||
+1
-5
@@ -5,8 +5,4 @@
|
||||
|
||||
package org.signal.registration.screens.devicetransfer.complete
|
||||
|
||||
data class DeviceTransferCompleteState(
|
||||
val oneTimeEvent: OneTimeEvent? = null
|
||||
) {
|
||||
sealed interface OneTimeEvent
|
||||
}
|
||||
class DeviceTransferCompleteState
|
||||
|
||||
-3
@@ -55,9 +55,6 @@ class DeviceTransferCompleteViewModel(
|
||||
repository.restoreAccountRecord()
|
||||
parentEventEmitter(RegistrationFlowEvent.RegistrationComplete)
|
||||
}
|
||||
DeviceTransferCompleteScreenEvents.ConsumeOneTimeEvent -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -8,5 +8,4 @@ package org.signal.registration.screens.devicetransfer.instructions
|
||||
sealed class DeviceTransferInstructionsScreenEvents {
|
||||
data object ContinueClicked : DeviceTransferInstructionsScreenEvents()
|
||||
data object BackClicked : DeviceTransferInstructionsScreenEvents()
|
||||
data object ConsumeOneTimeEvent : DeviceTransferInstructionsScreenEvents()
|
||||
}
|
||||
|
||||
+1
-5
@@ -5,8 +5,4 @@
|
||||
|
||||
package org.signal.registration.screens.devicetransfer.instructions
|
||||
|
||||
data class DeviceTransferInstructionsState(
|
||||
val oneTimeEvent: OneTimeEvent? = null
|
||||
) {
|
||||
sealed interface OneTimeEvent
|
||||
}
|
||||
class DeviceTransferInstructionsState
|
||||
|
||||
-3
@@ -55,9 +55,6 @@ class DeviceTransferInstructionsViewModel(
|
||||
DeviceTransferInstructionsScreenEvents.BackClicked -> {
|
||||
parentEventEmitter.navigateBack()
|
||||
}
|
||||
DeviceTransferInstructionsScreenEvents.ConsumeOneTimeEvent -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -10,5 +10,4 @@ sealed class DeviceTransferProgressScreenEvents {
|
||||
data object CancelConfirmed : DeviceTransferProgressScreenEvents()
|
||||
data object CancelDismissed : DeviceTransferProgressScreenEvents()
|
||||
data object TryAgainClicked : DeviceTransferProgressScreenEvents()
|
||||
data object ConsumeOneTimeEvent : DeviceTransferProgressScreenEvents()
|
||||
}
|
||||
|
||||
+1
-6
@@ -8,8 +8,7 @@ package org.signal.registration.screens.devicetransfer.progress
|
||||
data class DeviceTransferProgressState(
|
||||
val messageCount: Long = 0,
|
||||
val status: Status = Status.RECEIVING,
|
||||
val errorReason: ErrorReason? = null,
|
||||
val oneTimeEvent: OneTimeEvent? = null
|
||||
val errorReason: ErrorReason? = null
|
||||
) {
|
||||
|
||||
enum class Status {
|
||||
@@ -24,8 +23,4 @@ data class DeviceTransferProgressState(
|
||||
FOREIGN_KEY,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
sealed interface OneTimeEvent {
|
||||
data object TransferCanceled : OneTimeEvent
|
||||
}
|
||||
}
|
||||
|
||||
-3
@@ -98,9 +98,6 @@ class DeviceTransferProgressViewModel(
|
||||
stopService()
|
||||
parentEventEmitter.navigateTo(RegistrationRoute.DeviceTransferInstructions)
|
||||
}
|
||||
DeviceTransferProgressScreenEvents.ConsumeOneTimeEvent -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-12
@@ -82,25 +82,26 @@ internal fun DeviceTransferSetupScreen(
|
||||
onEvent(DeviceTransferSetupScreenEvents.BackClicked)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.oneTimeEvent) {
|
||||
val event = state.oneTimeEvent ?: return@LaunchedEffect
|
||||
onEvent(DeviceTransferSetupScreenEvents.ConsumeOneTimeEvent)
|
||||
when (event) {
|
||||
DeviceTransferSetupState.OneTimeEvent.RequestLocationPermission -> {
|
||||
LaunchedEffect(state.pendingActions) {
|
||||
when {
|
||||
state.pendingActions.requestLocationPermission -> {
|
||||
when (permissionState.status) {
|
||||
is PermissionStatus.Granted -> onEvent(DeviceTransferSetupScreenEvents.PermissionsGranted)
|
||||
is PermissionStatus.Denied -> permissionState.launchPermissionRequest()
|
||||
}
|
||||
onEvent(DeviceTransferSetupScreenEvents.RequestLocationPermissionHandled)
|
||||
}
|
||||
DeviceTransferSetupState.OneTimeEvent.OpenLocationSettings -> {
|
||||
state.pendingActions.openLocationSettings -> {
|
||||
runCatching { context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) }
|
||||
.onFailure { runCatching { context.startActivity(Intent(Settings.ACTION_SETTINGS)) } }
|
||||
onEvent(DeviceTransferSetupScreenEvents.OpenLocationSettingsHandled)
|
||||
}
|
||||
DeviceTransferSetupState.OneTimeEvent.OpenWifiSettings -> {
|
||||
state.pendingActions.openWifiSettings -> {
|
||||
runCatching { context.startActivity(Intent(Settings.ACTION_WIFI_SETTINGS)) }
|
||||
.onFailure { runCatching { context.startActivity(Intent(Settings.ACTION_SETTINGS)) } }
|
||||
onEvent(DeviceTransferSetupScreenEvents.OpenWifiSettingsHandled)
|
||||
}
|
||||
DeviceTransferSetupState.OneTimeEvent.OpenAppSettings -> {
|
||||
state.pendingActions.openAppSettings -> {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", context.packageName, null)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@@ -110,10 +111,7 @@ internal fun DeviceTransferSetupScreen(
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
// nothing we can do
|
||||
}
|
||||
}
|
||||
DeviceTransferSetupState.OneTimeEvent.NavigateToProgress,
|
||||
DeviceTransferSetupState.OneTimeEvent.NavigateAway -> {
|
||||
// Navigation is handled by the ViewModel via parentEventEmitter.
|
||||
onEvent(DeviceTransferSetupScreenEvents.OpenAppSettingsHandled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -38,5 +38,15 @@ sealed class DeviceTransferSetupScreenEvents {
|
||||
/** Back / close. Stops the service and pops the nav stack. */
|
||||
data object BackClicked : DeviceTransferSetupScreenEvents()
|
||||
|
||||
data object ConsumeOneTimeEvent : DeviceTransferSetupScreenEvents()
|
||||
/** The screen has launched the pending location permission request. */
|
||||
data object RequestLocationPermissionHandled : DeviceTransferSetupScreenEvents()
|
||||
|
||||
/** The screen has launched the pending open-location-settings action. */
|
||||
data object OpenLocationSettingsHandled : DeviceTransferSetupScreenEvents()
|
||||
|
||||
/** The screen has launched the pending open-wifi-settings action. */
|
||||
data object OpenWifiSettingsHandled : DeviceTransferSetupScreenEvents()
|
||||
|
||||
/** The screen has launched the pending open-app-settings action. */
|
||||
data object OpenAppSettingsHandled : DeviceTransferSetupScreenEvents()
|
||||
}
|
||||
|
||||
+9
-14
@@ -11,28 +11,23 @@ data class DeviceTransferSetupState(
|
||||
val takingTooLong: Boolean = false,
|
||||
val showVerifyRejectDialog: Boolean = false,
|
||||
val showErrorDialog: Boolean = false,
|
||||
val oneTimeEvent: OneTimeEvent? = null
|
||||
val pendingActions: PendingActions = PendingActions()
|
||||
) {
|
||||
|
||||
override fun toString(): String = "DeviceTransferSetupState(step=$step, authenticationCode=${authenticationCode?.let { "present" }}, takingTooLong=$takingTooLong, showVerifyRejectDialog=$showVerifyRejectDialog, showErrorDialog=$showErrorDialog, oneTimeEvent=$oneTimeEvent)"
|
||||
override fun toString(): String = "DeviceTransferSetupState(step=$step, authenticationCode=${authenticationCode?.let { "present" }}, takingTooLong=$takingTooLong, showVerifyRejectDialog=$showVerifyRejectDialog, showErrorDialog=$showErrorDialog, pendingActions=$pendingActions)"
|
||||
|
||||
sealed interface OneTimeEvent {
|
||||
/** One-shot actions the screen should launch. The screen clears these once launched. */
|
||||
data class PendingActions(
|
||||
/** The screen should launch a runtime permission request. */
|
||||
data object RequestLocationPermission : OneTimeEvent
|
||||
val requestLocationPermission: Boolean = false,
|
||||
|
||||
/** The screen should launch the system Location settings. */
|
||||
data object OpenLocationSettings : OneTimeEvent
|
||||
val openLocationSettings: Boolean = false,
|
||||
|
||||
/** The screen should launch the system Wi-Fi settings. */
|
||||
data object OpenWifiSettings : OneTimeEvent
|
||||
val openWifiSettings: Boolean = false,
|
||||
|
||||
/** The screen should launch this app's system settings (for permanent-denial recovery). */
|
||||
data object OpenAppSettings : OneTimeEvent
|
||||
|
||||
/** Both devices verified successfully; navigate to the Progress screen. */
|
||||
data object NavigateToProgress : OneTimeEvent
|
||||
|
||||
/** Unrecoverable setup path (e.g. Wi-Fi Direct unavailable); navigate back. */
|
||||
data object NavigateAway : OneTimeEvent
|
||||
}
|
||||
val openAppSettings: Boolean = false
|
||||
)
|
||||
}
|
||||
|
||||
+19
-7
@@ -105,7 +105,7 @@ class DeviceTransferSetupViewModel(
|
||||
stateEmitter(state.copy(step = SetupStep.LOCATION_CHECK, takingTooLong = false))
|
||||
checkLocation(parentEventEmitter, stateEmitter)
|
||||
} else {
|
||||
stateEmitter(state.copy(step = SetupStep.PERMISSIONS_CHECK, takingTooLong = false, oneTimeEvent = DeviceTransferSetupState.OneTimeEvent.RequestLocationPermission))
|
||||
stateEmitter(state.copy(step = SetupStep.PERMISSIONS_CHECK, takingTooLong = false, pendingActions = state.pendingActions.copy(requestLocationPermission = true)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,19 +119,19 @@ class DeviceTransferSetupViewModel(
|
||||
}
|
||||
|
||||
DeviceTransferSetupScreenEvents.RequestPermissionClicked -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = DeviceTransferSetupState.OneTimeEvent.RequestLocationPermission))
|
||||
stateEmitter(state.copy(pendingActions = state.pendingActions.copy(requestLocationPermission = true)))
|
||||
}
|
||||
|
||||
DeviceTransferSetupScreenEvents.OpenLocationSettingsClicked -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = DeviceTransferSetupState.OneTimeEvent.OpenLocationSettings))
|
||||
stateEmitter(state.copy(pendingActions = state.pendingActions.copy(openLocationSettings = true)))
|
||||
}
|
||||
|
||||
DeviceTransferSetupScreenEvents.OpenWifiSettingsClicked -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = DeviceTransferSetupState.OneTimeEvent.OpenWifiSettings))
|
||||
stateEmitter(state.copy(pendingActions = state.pendingActions.copy(openWifiSettings = true)))
|
||||
}
|
||||
|
||||
DeviceTransferSetupScreenEvents.OpenAppSettingsClicked -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = DeviceTransferSetupState.OneTimeEvent.OpenAppSettings))
|
||||
stateEmitter(state.copy(pendingActions = state.pendingActions.copy(openAppSettings = true)))
|
||||
}
|
||||
|
||||
DeviceTransferSetupScreenEvents.OnResume -> {
|
||||
@@ -181,8 +181,20 @@ class DeviceTransferSetupViewModel(
|
||||
parentEventEmitter.navigateBack()
|
||||
}
|
||||
|
||||
DeviceTransferSetupScreenEvents.ConsumeOneTimeEvent -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = null))
|
||||
DeviceTransferSetupScreenEvents.RequestLocationPermissionHandled -> {
|
||||
stateEmitter(state.copy(pendingActions = state.pendingActions.copy(requestLocationPermission = false)))
|
||||
}
|
||||
|
||||
DeviceTransferSetupScreenEvents.OpenLocationSettingsHandled -> {
|
||||
stateEmitter(state.copy(pendingActions = state.pendingActions.copy(openLocationSettings = false)))
|
||||
}
|
||||
|
||||
DeviceTransferSetupScreenEvents.OpenWifiSettingsHandled -> {
|
||||
stateEmitter(state.copy(pendingActions = state.pendingActions.copy(openWifiSettings = false)))
|
||||
}
|
||||
|
||||
DeviceTransferSetupScreenEvents.OpenAppSettingsHandled -> {
|
||||
stateEmitter(state.copy(pendingActions = state.pendingActions.copy(openAppSettings = false)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-22
@@ -54,7 +54,6 @@ import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -83,7 +82,6 @@ import org.signal.registration.screens.OnePaneRegistrationScaffold
|
||||
import org.signal.registration.screens.RegistrationScaffold
|
||||
import org.signal.registration.screens.TwoPaneRegistrationScaffold
|
||||
import org.signal.registration.screens.attachDebugLogHelper
|
||||
import org.signal.registration.screens.phonenumber.PhoneNumberEntryState.OneTimeEvent
|
||||
import org.signal.registration.test.TestTags
|
||||
import org.signal.core.ui.R as CoreR
|
||||
|
||||
@@ -116,10 +114,7 @@ fun PhoneNumberScreen(
|
||||
onEvent: (PhoneNumberEntryScreenEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val resources = LocalResources.current
|
||||
val context = LocalContext.current
|
||||
var simpleErrorMessage: String? by remember { mutableStateOf(null) }
|
||||
var showInvalidNumberDialog by remember { mutableStateOf(false) }
|
||||
var hasRequestedPhoneNumberHint by rememberSaveable { mutableStateOf(false) }
|
||||
val currentNationalNumber by rememberUpdatedState(state.nationalNumber)
|
||||
|
||||
@@ -171,7 +166,7 @@ fun PhoneNumberScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (state.showDialog) {
|
||||
if (state.dialogs.confirmNumber) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.RegistrationActivity_is_the_phone_number),
|
||||
body = "+${state.countryCode} ${state.formattedNumber}\n\n${stringResource(R.string.RegistrationActivity_a_verification_code)}",
|
||||
@@ -182,37 +177,36 @@ fun PhoneNumberScreen(
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.oneTimeEvent) {
|
||||
onEvent(PhoneNumberEntryScreenEvents.ConsumeOneTimeEvent)
|
||||
when (state.oneTimeEvent) {
|
||||
OneTimeEvent.NetworkError -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__network_error)
|
||||
is OneTimeEvent.RateLimited -> simpleErrorMessage = if (state.oneTimeEvent.retryAfter.isPositive()) {
|
||||
resources.getString(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, state.oneTimeEvent.retryAfter.toString())
|
||||
val simpleError: Pair<String, PhoneNumberEntryScreenEvents>? = when {
|
||||
state.dialogs.networkError -> stringResource(R.string.VerificationCodeScreen__network_error) to PhoneNumberEntryScreenEvents.NetworkErrorDialogDismissed
|
||||
state.dialogs.rateLimitedRetryAfter != null -> {
|
||||
val message = if (state.dialogs.rateLimitedRetryAfter.isPositive()) {
|
||||
stringResource(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, state.dialogs.rateLimitedRetryAfter.toString())
|
||||
} else {
|
||||
resources.getString(R.string.VerificationCodeScreen__too_many_attempts)
|
||||
stringResource(R.string.VerificationCodeScreen__too_many_attempts)
|
||||
}
|
||||
OneTimeEvent.UnknownError -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__an_unexpected_error_occurred)
|
||||
OneTimeEvent.CouldNotRequestCodeWithSelectedTransport -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__could_not_send_code_via_selected_method)
|
||||
OneTimeEvent.UnableToSendSms -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__unable_to_send_sms)
|
||||
OneTimeEvent.InvalidPhoneNumber -> showInvalidNumberDialog = true
|
||||
null -> Unit
|
||||
message to PhoneNumberEntryScreenEvents.RateLimitedDialogDismissed
|
||||
}
|
||||
state.dialogs.unknownError -> stringResource(R.string.VerificationCodeScreen__an_unexpected_error_occurred) to PhoneNumberEntryScreenEvents.UnknownErrorDialogDismissed
|
||||
state.dialogs.couldNotRequestCodeWithSelectedTransport -> stringResource(R.string.VerificationCodeScreen__could_not_send_code_via_selected_method) to PhoneNumberEntryScreenEvents.CouldNotRequestCodeWithSelectedTransportDialogDismissed
|
||||
state.dialogs.unableToSendSms -> stringResource(R.string.VerificationCodeScreen__unable_to_send_sms) to PhoneNumberEntryScreenEvents.UnableToSendSmsDialogDismissed
|
||||
else -> null
|
||||
}
|
||||
|
||||
simpleErrorMessage?.let { message ->
|
||||
simpleError?.let { (message, dismissedEvent) ->
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = message,
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = { simpleErrorMessage = null }
|
||||
onDismiss = { onEvent(dismissedEvent) }
|
||||
)
|
||||
}
|
||||
|
||||
if (showInvalidNumberDialog) {
|
||||
if (state.dialogs.invalidPhoneNumber) {
|
||||
Dialogs.SimpleMessageDialog(
|
||||
title = stringResource(R.string.RegistrationActivity_invalid_phone_number),
|
||||
message = stringResource(R.string.RegistrationActivity_the_number_you_entered_is_not_valid),
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = { showInvalidNumberDialog = false }
|
||||
onDismiss = { onEvent(PhoneNumberEntryScreenEvents.InvalidPhoneNumberDialogDismissed) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+18
-1
@@ -59,5 +59,22 @@ sealed class PhoneNumberEntryScreenEvents {
|
||||
data class LocalBackupRestoreCompleted(val result: LocalBackupRestoreResult) : PhoneNumberEntryScreenEvents() {
|
||||
override fun toString(): String = "LocalBackupRestoreCompleted(result=***)"
|
||||
}
|
||||
data object ConsumeOneTimeEvent : PhoneNumberEntryScreenEvents()
|
||||
|
||||
/** The user dismissed the network error dialog. */
|
||||
data object NetworkErrorDialogDismissed : PhoneNumberEntryScreenEvents()
|
||||
|
||||
/** The user dismissed the unknown error dialog. */
|
||||
data object UnknownErrorDialogDismissed : PhoneNumberEntryScreenEvents()
|
||||
|
||||
/** The user dismissed the rate limited dialog. */
|
||||
data object RateLimitedDialogDismissed : PhoneNumberEntryScreenEvents()
|
||||
|
||||
/** The user dismissed the unable-to-send-SMS dialog. */
|
||||
data object UnableToSendSmsDialogDismissed : PhoneNumberEntryScreenEvents()
|
||||
|
||||
/** The user dismissed the could-not-request-code-with-selected-transport dialog. */
|
||||
data object CouldNotRequestCodeWithSelectedTransportDialogDismissed : PhoneNumberEntryScreenEvents()
|
||||
|
||||
/** The user dismissed the invalid phone number dialog. */
|
||||
data object InvalidPhoneNumberDialogDismissed : PhoneNumberEntryScreenEvents()
|
||||
}
|
||||
|
||||
+13
-11
@@ -21,8 +21,7 @@ data class PhoneNumberEntryState(
|
||||
val sessionE164: String? = null,
|
||||
val sessionMetadata: SessionMetadata? = null,
|
||||
val showSpinner: Boolean = false,
|
||||
val showDialog: Boolean = false,
|
||||
val oneTimeEvent: OneTimeEvent? = null,
|
||||
val dialogs: Dialogs = Dialogs(),
|
||||
val preExistingRegistrationData: PreExistingRegistrationData? = null,
|
||||
val restoredSvrCredentials: List<NetworkController.SvrCredentials> = emptyList(),
|
||||
val pendingRestoreOption: PendingRestoreOption? = null,
|
||||
@@ -32,14 +31,17 @@ data class PhoneNumberEntryState(
|
||||
/** Whether the entered number is definitively invalid. A still-too-short number is not considered invalid, since the user may simply be mid-entry. */
|
||||
val isNumberInvalid: Boolean = false
|
||||
) {
|
||||
override fun toString(): String = "PhoneNumberEntryState(regionCode=$regionCode, countryCode=$countryCode, countryName=$countryName, countryEmoji=$countryEmoji, nationalNumber=$nationalNumber, formattedNumber=$formattedNumber, sessionE164=$sessionE164, sessionMetadata=${sessionMetadata?.let { "present" }}, showSpinner=$showSpinner, showDialog=$showDialog, oneTimeEvent=$oneTimeEvent, preExistingRegistrationData=${preExistingRegistrationData?.let { "present" }}, restoredSvrCredentials=${restoredSvrCredentials.size} items, pendingRestoreOption=$pendingRestoreOption, initialized=$initialized, isNumberPossible=$isNumberPossible, isNumberInvalid=$isNumberInvalid)"
|
||||
override fun toString(): String = "PhoneNumberEntryState(regionCode=$regionCode, countryCode=$countryCode, countryName=$countryName, countryEmoji=$countryEmoji, nationalNumber=$nationalNumber, formattedNumber=$formattedNumber, sessionE164=$sessionE164, sessionMetadata=${sessionMetadata?.let { "present" }}, showSpinner=$showSpinner, dialogs=$dialogs, preExistingRegistrationData=${preExistingRegistrationData?.let { "present" }}, restoredSvrCredentials=${restoredSvrCredentials.size} items, pendingRestoreOption=$pendingRestoreOption, initialized=$initialized, isNumberPossible=$isNumberPossible, isNumberInvalid=$isNumberInvalid)"
|
||||
|
||||
sealed interface OneTimeEvent {
|
||||
data object NetworkError : OneTimeEvent
|
||||
data object UnknownError : OneTimeEvent
|
||||
data class RateLimited(val retryAfter: Duration) : OneTimeEvent
|
||||
data object UnableToSendSms : OneTimeEvent
|
||||
data object CouldNotRequestCodeWithSelectedTransport : OneTimeEvent
|
||||
data object InvalidPhoneNumber : OneTimeEvent
|
||||
}
|
||||
data class Dialogs(
|
||||
/** Asks the user to confirm the number they entered before submitting it. */
|
||||
val confirmNumber: Boolean = false,
|
||||
val networkError: Boolean = false,
|
||||
val unknownError: Boolean = false,
|
||||
/** When non-null, shows a rate limit error dialog. A non-positive duration indicates the server didn't say how long to wait. */
|
||||
val rateLimitedRetryAfter: Duration? = null,
|
||||
val unableToSendSms: Boolean = false,
|
||||
val couldNotRequestCodeWithSelectedTransport: Boolean = false,
|
||||
val invalidPhoneNumber: Boolean = false
|
||||
)
|
||||
}
|
||||
|
||||
+54
-40
@@ -33,7 +33,6 @@ import org.signal.registration.RegistrationRoute
|
||||
import org.signal.registration.screens.countrycode.Country
|
||||
import org.signal.registration.screens.countrycode.CountryUtils
|
||||
import org.signal.registration.screens.localbackuprestore.LocalBackupRestoreResult
|
||||
import org.signal.registration.screens.phonenumber.PhoneNumberEntryState.OneTimeEvent
|
||||
import org.signal.registration.screens.util.navigateTo
|
||||
|
||||
class PhoneNumberEntryViewModel(
|
||||
@@ -103,22 +102,22 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.FullPhoneNumberEntered -> {
|
||||
val populatedState = applyFullPhoneNumberEntered(state, event.e164)
|
||||
stateEmitter(populatedState.copy(showDialog = event.autoConfirm && populatedState.isNumberPossible))
|
||||
stateEmitter(populatedState.copy(dialogs = populatedState.dialogs.copy(confirmNumber = event.autoConfirm && populatedState.isNumberPossible)))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.NationalNumberChanged -> {
|
||||
stateEmitter(applyPhoneNumberChanged(state, event.oldValue, event.newValue))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.NextClicked -> {
|
||||
stateEmitter(state.copy(showDialog = true))
|
||||
stateEmitter(state.copy(dialogs = state.dialogs.copy(confirmNumber = true)))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.PhoneNumberCancelled -> {
|
||||
stateEmitter(state.copy(showDialog = false))
|
||||
stateEmitter(state.copy(dialogs = state.dialogs.copy(confirmNumber = false)))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.PhoneNumberConfirmed -> {
|
||||
var localState = state.copy(showSpinner = true, showDialog = false)
|
||||
var localState = state.copy(showSpinner = true, dialogs = state.dialogs.copy(confirmNumber = false))
|
||||
stateEmitter(localState)
|
||||
localState = applyPhoneNumberSubmitted(localState, parentEventEmitter)
|
||||
stateEmitter(localState.copy(showSpinner = false, showDialog = false))
|
||||
stateEmitter(localState.copy(showSpinner = false))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.CountryPicker -> {
|
||||
state.also {
|
||||
@@ -148,8 +147,23 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.ConsumeOneTimeEvent -> {
|
||||
stateEmitter(state.copy(oneTimeEvent = null))
|
||||
is PhoneNumberEntryScreenEvents.NetworkErrorDialogDismissed -> {
|
||||
stateEmitter(state.copy(dialogs = state.dialogs.copy(networkError = false)))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.UnknownErrorDialogDismissed -> {
|
||||
stateEmitter(state.copy(dialogs = state.dialogs.copy(unknownError = false)))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.RateLimitedDialogDismissed -> {
|
||||
stateEmitter(state.copy(dialogs = state.dialogs.copy(rateLimitedRetryAfter = null)))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.UnableToSendSmsDialogDismissed -> {
|
||||
stateEmitter(state.copy(dialogs = state.dialogs.copy(unableToSendSms = false)))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.CouldNotRequestCodeWithSelectedTransportDialogDismissed -> {
|
||||
stateEmitter(state.copy(dialogs = state.dialogs.copy(couldNotRequestCodeWithSelectedTransport = false)))
|
||||
}
|
||||
is PhoneNumberEntryScreenEvents.InvalidPhoneNumberDialogDismissed -> {
|
||||
stateEmitter(state.copy(dialogs = state.dialogs.copy(invalidPhoneNumber = false)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,7 +315,7 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
is NetworkController.RegisterAccountError.RateLimited -> {
|
||||
Log.w(TAG, "[Register] Rate limited (retryAfter: ${error.retryAfter}).")
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.RateLimited(error.retryAfter))
|
||||
return state.copy(dialogs = state.dialogs.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
is NetworkController.RegisterAccountError.InvalidRequest -> {
|
||||
Log.w(TAG, "[Register] Invalid request when registering account with RRP. Ditching pre-existing data and continuing with session creation. Message: ${error.message}")
|
||||
@@ -317,11 +331,11 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[Register] Network error.", registerResult.networkError)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
return state.copy(dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[Register] Unknown error when registering account.", registerResult.cause)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
return state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,7 +422,7 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
is NetworkController.RegisterAccountError.RateLimited -> {
|
||||
Log.w(TAG, "[LocalRestore] Rate limited (retryAfter: ${error.retryAfter}).")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.RateLimited(error.retryAfter))
|
||||
state.copy(dialogs = state.dialogs.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
is NetworkController.RegisterAccountError.SessionNotFoundOrNotVerified -> {
|
||||
Log.w(TAG, "[LocalRestore] Session not found. Falling back to session-based registration.")
|
||||
@@ -422,11 +436,11 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[LocalRestore] Network error.", result.networkError)
|
||||
state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
state.copy(dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[LocalRestore] Application error.", result.cause)
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -488,21 +502,21 @@ class PhoneNumberEntryViewModel(
|
||||
return when (val error = response.error) {
|
||||
is NetworkController.CreateSessionError.InvalidRequest -> {
|
||||
Log.w(TAG, "[CreateSession] Invalid request when creating session, likely an invalid phone number. Message: ${error.message}")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.InvalidPhoneNumber)
|
||||
state.copy(dialogs = state.dialogs.copy(invalidPhoneNumber = true))
|
||||
}
|
||||
is NetworkController.CreateSessionError.RateLimited -> {
|
||||
Log.w(TAG, "[CreateSession] Rate limited (retryAfter: ${error.retryAfter}).")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.RateLimited(error.retryAfter))
|
||||
state.copy(dialogs = state.dialogs.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
}
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[CreateSession] Network error.", response.networkError)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
return state.copy(dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "Unknown error when creating session.", response.cause)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
return state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,7 +564,7 @@ class PhoneNumberEntryViewModel(
|
||||
|
||||
if (!sessionMetadata.allowedToRequestCode && sessionMetadata.requestedInformation.isEmpty()) {
|
||||
Log.w(TAG, "Not allowed to request code and no challenges requested. Unable to send SMS.")
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.UnableToSendSms)
|
||||
return state.copy(dialogs = state.dialogs.copy(unableToSendSms = true))
|
||||
}
|
||||
|
||||
val verificationCodeResponse = this@PhoneNumberEntryViewModel.repository.requestVerificationCode(
|
||||
@@ -568,15 +582,15 @@ class PhoneNumberEntryViewModel(
|
||||
return when (val error = verificationCodeResponse.error) {
|
||||
is NetworkController.RequestVerificationCodeError.InvalidRequest -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Invalid request when requesting verification code. Message: ${error.message}")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.RateLimited -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Rate limited (retryAfter: ${error.retryAfter}).")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.RateLimited(error.retryAfter))
|
||||
state.copy(dialogs = state.dialogs.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.CouldNotFulfillWithRequestedTransport -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Could not fulfill with requested transport.")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.CouldNotRequestCodeWithSelectedTransport)
|
||||
state.copy(dialogs = state.dialogs.copy(couldNotRequestCodeWithSelectedTransport = true))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.InvalidSessionId -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Invalid session ID when requesting verification code.")
|
||||
@@ -585,7 +599,7 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.MissingRequestInformationOrAlreadyVerified -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Missing request information or already verified.")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnableToSendSms)
|
||||
state.copy(dialogs = state.dialogs.copy(unableToSendSms = true))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.SessionNotFound -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Session not found when requesting verification code.")
|
||||
@@ -594,17 +608,17 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.ThirdPartyServiceError -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Third party service error.")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnableToSendSms)
|
||||
state.copy(dialogs = state.dialogs.copy(unableToSendSms = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Network error.", verificationCodeResponse.networkError)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
return state.copy(dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[RequestVerificationCode] Unknown error when creating session.", verificationCodeResponse.cause)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
return state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +637,7 @@ class PhoneNumberEntryViewModel(
|
||||
|
||||
private suspend fun applyCaptchaCompleted(inputState: PhoneNumberEntryState, token: String, parentEventEmitter: (RegistrationFlowEvent) -> Unit): PhoneNumberEntryState {
|
||||
var state = inputState.copy()
|
||||
var sessionMetadata = state.sessionMetadata ?: return state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
var sessionMetadata = state.sessionMetadata ?: return state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
|
||||
val updateResult = this@PhoneNumberEntryViewModel.repository.submitCaptchaToken(sessionMetadata.id, token)
|
||||
|
||||
@@ -632,22 +646,22 @@ class PhoneNumberEntryViewModel(
|
||||
is RequestResult.NonSuccess -> {
|
||||
return when (val error = updateResult.error) {
|
||||
is NetworkController.UpdateSessionError.InvalidRequest -> {
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
is NetworkController.UpdateSessionError.RejectedUpdate -> {
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
is NetworkController.UpdateSessionError.RateLimited -> {
|
||||
state.copy(oneTimeEvent = OneTimeEvent.RateLimited(error.retryAfter))
|
||||
state.copy(dialogs = state.dialogs.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
}
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
return state.copy(dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "Unknown error when submitting captcha.", updateResult.cause)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
return state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,7 +674,7 @@ class PhoneNumberEntryViewModel(
|
||||
|
||||
if (!sessionMetadata.allowedToRequestCode && sessionMetadata.requestedInformation.isEmpty()) {
|
||||
Log.w(TAG, "Not allowed to request code and no challenges requested after captcha. Unable to send SMS.")
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.UnableToSendSms)
|
||||
return state.copy(dialogs = state.dialogs.copy(unableToSendSms = true))
|
||||
}
|
||||
|
||||
val verificationCodeResponse = this@PhoneNumberEntryViewModel.repository.requestVerificationCode(
|
||||
@@ -674,13 +688,13 @@ class PhoneNumberEntryViewModel(
|
||||
is RequestResult.NonSuccess -> {
|
||||
return when (val error = verificationCodeResponse.error) {
|
||||
is NetworkController.RequestVerificationCodeError.InvalidRequest -> {
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.RateLimited -> {
|
||||
state.copy(oneTimeEvent = OneTimeEvent.RateLimited(error.retryAfter))
|
||||
state.copy(dialogs = state.dialogs.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.CouldNotFulfillWithRequestedTransport -> {
|
||||
state.copy(oneTimeEvent = OneTimeEvent.CouldNotRequestCodeWithSelectedTransport)
|
||||
state.copy(dialogs = state.dialogs.copy(couldNotRequestCodeWithSelectedTransport = true))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.InvalidSessionId -> {
|
||||
parentEventEmitter(RegistrationFlowEvent.ResetState)
|
||||
@@ -688,23 +702,23 @@ class PhoneNumberEntryViewModel(
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.MissingRequestInformationOrAlreadyVerified -> {
|
||||
Log.w(TAG, "When requesting verification code after captcha, missing request information or already verified.")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnableToSendSms)
|
||||
state.copy(dialogs = state.dialogs.copy(unableToSendSms = true))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.SessionNotFound -> {
|
||||
parentEventEmitter(RegistrationFlowEvent.ResetState)
|
||||
state
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.ThirdPartyServiceError -> {
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnableToSendSms)
|
||||
state.copy(dialogs = state.dialogs.copy(unableToSendSms = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
return state.copy(dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "Unknown error when requesting verification code.", verificationCodeResponse.cause)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
return state.copy(dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-18
@@ -51,7 +51,6 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -93,35 +92,30 @@ fun PinCreationScreen(
|
||||
) {
|
||||
val activePin = remember { mutableStateOf("") }
|
||||
val canSubmitPin = activePin.value.length >= 4
|
||||
val resources = LocalResources.current
|
||||
var errorMessage: String? by remember { mutableStateOf(null) }
|
||||
|
||||
BackHandler(enabled = state.isConfirmEnabled) {
|
||||
onEvent(PinCreationScreenEvents.BackToPinEntry)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.oneTimeEvent) {
|
||||
val event = state.oneTimeEvent ?: return@LaunchedEffect
|
||||
onEvent(PinCreationScreenEvents.ConsumeOneTimeEvent)
|
||||
errorMessage = when (event) {
|
||||
is PinCreationState.OneTimeEvent.ServiceError -> {
|
||||
resources.getString(R.string.PinCreationScreen__service_error)
|
||||
}
|
||||
is PinCreationState.OneTimeEvent.NetworkError -> {
|
||||
if (event.retryAfter != null) {
|
||||
resources.getString(R.string.PinCreationScreen__network_error_try_again_in_s, event.retryAfter.toString())
|
||||
} else {
|
||||
resources.getString(R.string.PinCreationScreen__network_error)
|
||||
}
|
||||
val errorDialog: Pair<String, PinCreationScreenEvents>? = when {
|
||||
state.dialogs.serviceError -> stringResource(R.string.PinCreationScreen__service_error) to PinCreationScreenEvents.ServiceErrorDialogDismissed
|
||||
state.dialogs.networkError != null -> {
|
||||
val retryAfter = state.dialogs.networkError.retryAfter
|
||||
val message = if (retryAfter != null) {
|
||||
stringResource(R.string.PinCreationScreen__network_error_try_again_in_s, retryAfter.toString())
|
||||
} else {
|
||||
stringResource(R.string.PinCreationScreen__network_error)
|
||||
}
|
||||
message to PinCreationScreenEvents.NetworkErrorDialogDismissed
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
errorMessage?.let { message ->
|
||||
errorDialog?.let { (message, dismissedEvent) ->
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = message,
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = { errorMessage = null }
|
||||
onDismiss = { onEvent(dismissedEvent) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -18,5 +18,6 @@ sealed class PinCreationScreenEvents {
|
||||
data object LearnMore : PinCreationScreenEvents()
|
||||
data object OptOut : PinCreationScreenEvents()
|
||||
data object BackToPinEntry : PinCreationScreenEvents()
|
||||
data object ConsumeOneTimeEvent : PinCreationScreenEvents()
|
||||
data object ServiceErrorDialogDismissed : PinCreationScreenEvents()
|
||||
data object NetworkErrorDialogDismissed : PinCreationScreenEvents()
|
||||
}
|
||||
|
||||
+7
-5
@@ -16,14 +16,16 @@ data class PinCreationState(
|
||||
val loading: Boolean = false,
|
||||
val firstPin: String? = null,
|
||||
val accountEntropyPool: AccountEntropyPool? = null,
|
||||
val oneTimeEvent: OneTimeEvent? = null
|
||||
val dialogs: Dialogs = Dialogs()
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "PinCreationState(isAlphanumericKeyboard=$isAlphanumericKeyboard, isConfirmEnabled=$isConfirmEnabled, pinMismatch=$pinMismatch, loading=$loading, firstPin=${firstPin?.let { "${it.length} chars" }}, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()}, oneTimeEvent=$oneTimeEvent)"
|
||||
return "PinCreationState(isAlphanumericKeyboard=$isAlphanumericKeyboard, isConfirmEnabled=$isConfirmEnabled, pinMismatch=$pinMismatch, loading=$loading, firstPin=${firstPin?.let { "${it.length} chars" }}, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()}, dialogs=$dialogs)"
|
||||
}
|
||||
|
||||
sealed interface OneTimeEvent {
|
||||
data object ServiceError : OneTimeEvent
|
||||
data class NetworkError(val retryAfter: Duration?) : OneTimeEvent
|
||||
data class Dialogs(
|
||||
val serviceError: Boolean = false,
|
||||
val networkError: NetworkError? = null
|
||||
) {
|
||||
data class NetworkError(val retryAfter: Duration?)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -100,8 +100,11 @@ class PinCreationViewModel(
|
||||
_state.value = state.copy(isConfirmEnabled = false)
|
||||
applyOptOut()
|
||||
}
|
||||
is PinCreationScreenEvents.ConsumeOneTimeEvent -> {
|
||||
_state.value = state.copy(oneTimeEvent = null)
|
||||
is PinCreationScreenEvents.ServiceErrorDialogDismissed -> {
|
||||
_state.value = state.copy(dialogs = state.dialogs.copy(serviceError = false))
|
||||
}
|
||||
is PinCreationScreenEvents.NetworkErrorDialogDismissed -> {
|
||||
_state.value = state.copy(dialogs = state.dialogs.copy(networkError = null))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +144,7 @@ class PinCreationViewModel(
|
||||
when (val error = result.error) {
|
||||
is NetworkController.BackupMasterKeyError.EnclaveNotFound -> {
|
||||
Log.w(TAG, "[PinSubmitted] SVR enclave not found.")
|
||||
state.copy(loading = false, oneTimeEvent = PinCreationState.OneTimeEvent.ServiceError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(serviceError = true))
|
||||
}
|
||||
|
||||
is NetworkController.BackupMasterKeyError.NotRegistered -> {
|
||||
@@ -154,12 +157,12 @@ class PinCreationViewModel(
|
||||
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[PinSubmitted] Network error when backing up master key.", result.networkError)
|
||||
state.copy(loading = false, oneTimeEvent = PinCreationState.OneTimeEvent.NetworkError(result.retryAfter?.toKotlinDuration()))
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(networkError = PinCreationState.Dialogs.NetworkError(result.retryAfter?.toKotlinDuration())))
|
||||
}
|
||||
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[PinSubmitted] Application error when backing up master key.", result.cause)
|
||||
state.copy(loading = false, oneTimeEvent = PinCreationState.OneTimeEvent.ServiceError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(serviceError = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-8
@@ -77,7 +77,10 @@ class PinEntryForRegistrationLockViewModel(
|
||||
is PinEntryScreenEvents.CreateNewPin,
|
||||
is PinEntryScreenEvents.ContactSupport,
|
||||
is PinEntryScreenEvents.ParentStateChanged -> Unit
|
||||
is PinEntryScreenEvents.ToggleKeyboard -> {
|
||||
is PinEntryScreenEvents.ToggleKeyboard,
|
||||
is PinEntryScreenEvents.NetworkErrorDialogDismissed,
|
||||
is PinEntryScreenEvents.RateLimitedDialogDismissed,
|
||||
is PinEntryScreenEvents.UnknownErrorDialogDismissed -> {
|
||||
stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event))
|
||||
}
|
||||
}
|
||||
@@ -114,11 +117,11 @@ class PinEntryForRegistrationLockViewModel(
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[PinEntered] Network error when restoring master key.", restoreResult.networkError)
|
||||
return state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
|
||||
return state.copy(loading = false, dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[PinEntered] Application error when restoring master key.", restoreResult.cause)
|
||||
return state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
|
||||
return state.copy(loading = false, dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,15 +183,15 @@ class PinEntryForRegistrationLockViewModel(
|
||||
}
|
||||
is NetworkController.RegisterAccountError.RateLimited -> {
|
||||
Log.w(TAG, "[PinEntered] Rate limited when registering. Retry After: ${error.retryAfter}")
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.RateLimited(error.retryAfter))
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
is NetworkController.RegisterAccountError.InvalidRequest -> {
|
||||
Log.w(TAG, "[PinEntered] Invalid request when registering: ${error.message}")
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
is NetworkController.RegisterAccountError.DeviceTransferPossible -> {
|
||||
Log.w(TAG, "[PinEntered] Device transfer possible. This shouldn't happen when skipDeviceTransfer is true.")
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
is NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect -> {
|
||||
Log.w(TAG, "[PinEntered] Registration recovery password incorrect: ${error.message}. Marking recovery password invalid and navigating back.")
|
||||
@@ -200,11 +203,11 @@ class PinEntryForRegistrationLockViewModel(
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[PinEntered] Network error when registering.", registerResult.networkError)
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[PinEntered] Application error when registering.", registerResult.cause)
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -86,7 +86,10 @@ class PinEntryForSmsBypassViewModel(
|
||||
}
|
||||
is PinEntryScreenEvents.CreateNewPin,
|
||||
is PinEntryScreenEvents.ContactSupport -> Unit
|
||||
is PinEntryScreenEvents.ToggleKeyboard -> {
|
||||
is PinEntryScreenEvents.ToggleKeyboard,
|
||||
is PinEntryScreenEvents.NetworkErrorDialogDismissed,
|
||||
is PinEntryScreenEvents.RateLimitedDialogDismissed,
|
||||
is PinEntryScreenEvents.UnknownErrorDialogDismissed -> {
|
||||
stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event))
|
||||
}
|
||||
}
|
||||
@@ -131,11 +134,11 @@ class PinEntryForSmsBypassViewModel(
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[PinEntered] Network error when restoring master key (sms-bypass).", result.networkError)
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[PinEntered] Application error when restoring master key (sms-bypass).", result.cause)
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,10 +168,10 @@ class PinEntryForSmsBypassViewModel(
|
||||
state
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
is RequestResult.NonSuccess -> {
|
||||
when (val error = result.error) {
|
||||
@@ -185,7 +188,7 @@ class PinEntryForSmsBypassViewModel(
|
||||
}
|
||||
is NetworkController.RegisterAccountError.RateLimited -> {
|
||||
Log.w(TAG, "[Register] Rate limited (retryAfter: ${error.retryAfter}).")
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.RateLimited(error.retryAfter))
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
is NetworkController.RegisterAccountError.RegistrationLock -> {
|
||||
if (provideRegistrationLock) {
|
||||
|
||||
+8
-5
@@ -84,7 +84,10 @@ class PinEntryForSvrRestoreViewModel(
|
||||
Log.i(TAG, "[ContactSupport] User opted to contact support after no data was found.")
|
||||
stateEmitter(state.copy(showNoDataToRestoreDialog = false))
|
||||
}
|
||||
is PinEntryScreenEvents.ToggleKeyboard -> {
|
||||
is PinEntryScreenEvents.ToggleKeyboard,
|
||||
is PinEntryScreenEvents.NetworkErrorDialogDismissed,
|
||||
is PinEntryScreenEvents.RateLimitedDialogDismissed,
|
||||
is PinEntryScreenEvents.UnknownErrorDialogDismissed -> {
|
||||
stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event))
|
||||
}
|
||||
is PinEntryScreenEvents.ParentStateChanged -> Unit
|
||||
@@ -117,10 +120,10 @@ class PinEntryForSvrRestoreViewModel(
|
||||
}
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
return state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
|
||||
return state.copy(loading = false, dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
return state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
|
||||
return state.copy(loading = false, dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,11 +151,11 @@ class PinEntryForSvrRestoreViewModel(
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[PinEntered] Network error when restoring master key.", result.networkError)
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[PinEntered] Application error when restoring master key.", result.cause)
|
||||
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
|
||||
state.copy(loading = false, dialogs = state.dialogs.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -121,6 +121,28 @@ fun PinEntryScreen(
|
||||
)
|
||||
}
|
||||
|
||||
val errorDialog: Pair<String, PinEntryScreenEvents>? = when {
|
||||
state.dialogs.networkError -> stringResource(R.string.VerificationCodeScreen__network_error) to PinEntryScreenEvents.NetworkErrorDialogDismissed
|
||||
state.dialogs.rateLimitedRetryAfter != null -> {
|
||||
val message = if (state.dialogs.rateLimitedRetryAfter.isPositive()) {
|
||||
stringResource(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, state.dialogs.rateLimitedRetryAfter.toString())
|
||||
} else {
|
||||
stringResource(R.string.VerificationCodeScreen__too_many_attempts)
|
||||
}
|
||||
message to PinEntryScreenEvents.RateLimitedDialogDismissed
|
||||
}
|
||||
state.dialogs.unknownError -> stringResource(R.string.VerificationCodeScreen__an_unexpected_error_occurred) to PinEntryScreenEvents.UnknownErrorDialogDismissed
|
||||
else -> null
|
||||
}
|
||||
|
||||
errorDialog?.let { (message, dismissedEvent) ->
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = message,
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = { onEvent(dismissedEvent) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.showNoDataToRestoreDialog) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = "",
|
||||
|
||||
+3
@@ -10,6 +10,9 @@ object PinEntryScreenEventHandler {
|
||||
fun applyEvent(state: PinEntryState, event: PinEntryScreenEvents): PinEntryState {
|
||||
return when (event) {
|
||||
PinEntryScreenEvents.ToggleKeyboard -> state.copy(isAlphanumericKeyboard = !state.isAlphanumericKeyboard)
|
||||
PinEntryScreenEvents.NetworkErrorDialogDismissed -> state.copy(dialogs = state.dialogs.copy(networkError = false))
|
||||
PinEntryScreenEvents.RateLimitedDialogDismissed -> state.copy(dialogs = state.dialogs.copy(rateLimitedRetryAfter = null))
|
||||
PinEntryScreenEvents.UnknownErrorDialogDismissed -> state.copy(dialogs = state.dialogs.copy(unknownError = false))
|
||||
else -> throw UnsupportedOperationException("This even is not handled generically!")
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -15,6 +15,15 @@ sealed class PinEntryScreenEvents {
|
||||
override fun toString(): String = "PinEntered(pin=${pin.length} chars)"
|
||||
}
|
||||
data object ToggleKeyboard : PinEntryScreenEvents()
|
||||
|
||||
/** The user dismissed the network error dialog. */
|
||||
data object NetworkErrorDialogDismissed : PinEntryScreenEvents()
|
||||
|
||||
/** The user dismissed the rate limited dialog. */
|
||||
data object RateLimitedDialogDismissed : PinEntryScreenEvents()
|
||||
|
||||
/** The user dismissed the unknown error dialog. */
|
||||
data object UnknownErrorDialogDismissed : PinEntryScreenEvents()
|
||||
data object Skip : PinEntryScreenEvents()
|
||||
data object CreateNewPin : PinEntryScreenEvents()
|
||||
data object ContactSupport : PinEntryScreenEvents()
|
||||
|
||||
+7
-7
@@ -14,7 +14,7 @@ data class PinEntryState(
|
||||
val showNoDataToRestoreDialog: Boolean = false,
|
||||
val triesRemaining: Int? = null,
|
||||
val mode: Mode = Mode.SvrRestore,
|
||||
val oneTimeEvent: OneTimeEvent? = null,
|
||||
val dialogs: Dialogs = Dialogs(),
|
||||
val e164: String? = null
|
||||
) {
|
||||
enum class Mode {
|
||||
@@ -23,10 +23,10 @@ data class PinEntryState(
|
||||
SvrRestore
|
||||
}
|
||||
|
||||
sealed interface OneTimeEvent {
|
||||
data object NetworkError : OneTimeEvent
|
||||
data class RateLimited(val retryAfter: Duration) : OneTimeEvent
|
||||
data object SvrDataMissing : OneTimeEvent
|
||||
data object UnknownError : OneTimeEvent
|
||||
}
|
||||
data class Dialogs(
|
||||
val networkError: Boolean = false,
|
||||
/** When non-null, shows a rate limit error dialog. A non-positive duration indicates the server didn't say how long to wait. */
|
||||
val rateLimitedRetryAfter: Duration? = null,
|
||||
val unknownError: Boolean = false
|
||||
)
|
||||
}
|
||||
|
||||
+13
-32
@@ -94,39 +94,20 @@ fun VerificationCodeScreen(
|
||||
onEvent(VerificationCodeScreenEvents.ConsumeAutoFillCode)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.oneTimeEvent) {
|
||||
val event = state.oneTimeEvent ?: return@LaunchedEffect
|
||||
|
||||
when (event) {
|
||||
VerificationCodeState.OneTimeEvent.IncorrectVerificationCode -> {
|
||||
snackbarHostState.showSnackbar(resources.getString(R.string.VerificationCodeScreen__incorrect_code))
|
||||
}
|
||||
|
||||
VerificationCodeState.OneTimeEvent.NetworkError -> {
|
||||
snackbarHostState.showSnackbar(resources.getString(R.string.VerificationCodeScreen__network_error))
|
||||
}
|
||||
|
||||
is VerificationCodeState.OneTimeEvent.RateLimited -> {
|
||||
snackbarHostState.showSnackbar(resources.getString(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, event.retryAfter.toString()))
|
||||
}
|
||||
|
||||
VerificationCodeState.OneTimeEvent.UnableToSendSms -> {
|
||||
snackbarHostState.showSnackbar(resources.getString(R.string.VerificationCodeScreen__unable_to_send_sms))
|
||||
}
|
||||
|
||||
VerificationCodeState.OneTimeEvent.CouldNotRequestCodeWithSelectedTransport -> {
|
||||
snackbarHostState.showSnackbar(resources.getString(R.string.VerificationCodeScreen__could_not_send_code_via_selected_method))
|
||||
}
|
||||
|
||||
VerificationCodeState.OneTimeEvent.UnknownError -> {
|
||||
snackbarHostState.showSnackbar(resources.getString(R.string.VerificationCodeScreen__an_unexpected_error_occurred))
|
||||
}
|
||||
|
||||
VerificationCodeState.OneTimeEvent.RegistrationError -> {
|
||||
snackbarHostState.showSnackbar(resources.getString(R.string.VerificationCodeScreen__registration_error))
|
||||
}
|
||||
LaunchedEffect(state.snackbars) {
|
||||
val (message, dismissedEvent) = when {
|
||||
state.snackbars.incorrectVerificationCode -> resources.getString(R.string.VerificationCodeScreen__incorrect_code) to VerificationCodeScreenEvents.IncorrectVerificationCodeSnackbarDismissed
|
||||
state.snackbars.networkError -> resources.getString(R.string.VerificationCodeScreen__network_error) to VerificationCodeScreenEvents.NetworkErrorSnackbarDismissed
|
||||
state.snackbars.rateLimitedRetryAfter != null -> resources.getString(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, state.snackbars.rateLimitedRetryAfter.toString()) to VerificationCodeScreenEvents.RateLimitedSnackbarDismissed
|
||||
state.snackbars.unableToSendSms -> resources.getString(R.string.VerificationCodeScreen__unable_to_send_sms) to VerificationCodeScreenEvents.UnableToSendSmsSnackbarDismissed
|
||||
state.snackbars.couldNotRequestCodeWithSelectedTransport -> resources.getString(R.string.VerificationCodeScreen__could_not_send_code_via_selected_method) to VerificationCodeScreenEvents.CouldNotRequestCodeWithSelectedTransportSnackbarDismissed
|
||||
state.snackbars.unknownError -> resources.getString(R.string.VerificationCodeScreen__an_unexpected_error_occurred) to VerificationCodeScreenEvents.UnknownErrorSnackbarDismissed
|
||||
state.snackbars.registrationError -> resources.getString(R.string.VerificationCodeScreen__registration_error) to VerificationCodeScreenEvents.RegistrationErrorSnackbarDismissed
|
||||
else -> return@LaunchedEffect
|
||||
}
|
||||
onEvent(VerificationCodeScreenEvents.ConsumeInnerOneTimeEvent)
|
||||
|
||||
snackbarHostState.showSnackbar(message)
|
||||
onEvent(dismissedEvent)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.focusedDigitIndex) {
|
||||
|
||||
+20
-1
@@ -45,7 +45,26 @@ sealed class VerificationCodeScreenEvents {
|
||||
|
||||
data object DismissContactSupport : VerificationCodeScreenEvents()
|
||||
|
||||
data object ConsumeInnerOneTimeEvent : VerificationCodeScreenEvents()
|
||||
/** The network error snackbar was shown and dismissed. */
|
||||
data object NetworkErrorSnackbarDismissed : VerificationCodeScreenEvents()
|
||||
|
||||
/** The unknown error snackbar was shown and dismissed. */
|
||||
data object UnknownErrorSnackbarDismissed : VerificationCodeScreenEvents()
|
||||
|
||||
/** The rate limited snackbar was shown and dismissed. */
|
||||
data object RateLimitedSnackbarDismissed : VerificationCodeScreenEvents()
|
||||
|
||||
/** The unable-to-send-SMS snackbar was shown and dismissed. */
|
||||
data object UnableToSendSmsSnackbarDismissed : VerificationCodeScreenEvents()
|
||||
|
||||
/** The could-not-request-code-with-selected-transport snackbar was shown and dismissed. */
|
||||
data object CouldNotRequestCodeWithSelectedTransportSnackbarDismissed : VerificationCodeScreenEvents()
|
||||
|
||||
/** The incorrect verification code snackbar was shown and dismissed. */
|
||||
data object IncorrectVerificationCodeSnackbarDismissed : VerificationCodeScreenEvents()
|
||||
|
||||
/** The registration error snackbar was shown and dismissed. */
|
||||
data object RegistrationErrorSnackbarDismissed : VerificationCodeScreenEvents()
|
||||
|
||||
/**
|
||||
* Event to update countdown timers. Should be triggered periodically (e.g., every second).
|
||||
|
||||
+12
-17
@@ -19,9 +19,9 @@ data class VerificationCodeState(
|
||||
val digits: List<String> = List(CODE_LENGTH) { "" },
|
||||
val focusedDigitIndex: Int = 0,
|
||||
val showContactSupportSheet: Boolean = false,
|
||||
val oneTimeEvent: OneTimeEvent? = null
|
||||
val snackbars: Snackbars = Snackbars()
|
||||
) {
|
||||
override fun toString(): String = "VerificationCodeState(sessionMetadata=${sessionMetadata?.let { "present" }}, e164=$e164, isSubmittingCode=$isSubmittingCode, rateLimits=$rateLimits, incorrectCodeAttempts=$incorrectCodeAttempts, autoFillCode=${autoFillCode?.let { "present" }}, digitsEntered=${digits.count { it.isNotEmpty() }}, focusedDigitIndex=$focusedDigitIndex, showContactSupportSheet=$showContactSupportSheet, oneTimeEvent=$oneTimeEvent)"
|
||||
override fun toString(): String = "VerificationCodeState(sessionMetadata=${sessionMetadata?.let { "present" }}, e164=$e164, isSubmittingCode=$isSubmittingCode, rateLimits=$rateLimits, incorrectCodeAttempts=$incorrectCodeAttempts, autoFillCode=${autoFillCode?.let { "present" }}, digitsEntered=${digits.count { it.isNotEmpty() }}, focusedDigitIndex=$focusedDigitIndex, showContactSupportSheet=$showContactSupportSheet, snackbars=$snackbars)"
|
||||
|
||||
/**
|
||||
* The full code as currently entered. Only meaningful when [isComplete] is true.
|
||||
@@ -42,21 +42,16 @@ data class VerificationCodeState(
|
||||
fun emptyDigits(): List<String> = List(CODE_LENGTH) { "" }
|
||||
}
|
||||
|
||||
sealed interface OneTimeEvent {
|
||||
data object NetworkError : OneTimeEvent
|
||||
|
||||
data object UnknownError : OneTimeEvent
|
||||
|
||||
data class RateLimited(val retryAfter: Duration) : OneTimeEvent
|
||||
|
||||
data object UnableToSendSms : OneTimeEvent
|
||||
|
||||
data object CouldNotRequestCodeWithSelectedTransport : OneTimeEvent
|
||||
|
||||
data object IncorrectVerificationCode : OneTimeEvent
|
||||
|
||||
data object RegistrationError : OneTimeEvent
|
||||
}
|
||||
/** Transient error messages shown as snackbars. Cleared once the snackbar has been shown and dismissed. */
|
||||
data class Snackbars(
|
||||
val networkError: Boolean = false,
|
||||
val unknownError: Boolean = false,
|
||||
val rateLimitedRetryAfter: Duration? = null,
|
||||
val unableToSendSms: Boolean = false,
|
||||
val couldNotRequestCodeWithSelectedTransport: Boolean = false,
|
||||
val incorrectVerificationCode: Boolean = false,
|
||||
val registrationError: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns true if the user can resend SMS (timer has expired)
|
||||
|
||||
+23
-18
@@ -37,7 +37,6 @@ import org.signal.registration.RegistrationRepository
|
||||
import org.signal.registration.RegistrationRoute
|
||||
import org.signal.registration.screens.util.navigateBack
|
||||
import org.signal.registration.screens.util.navigateTo
|
||||
import org.signal.registration.screens.verificationcode.VerificationCodeState.OneTimeEvent
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
@@ -135,7 +134,13 @@ class VerificationCodeViewModel(
|
||||
is VerificationCodeScreenEvents.CallMe -> applyResendCode(state, NetworkController.VerificationCodeTransport.VOICE)
|
||||
is VerificationCodeScreenEvents.HavingTrouble -> state.copy(showContactSupportSheet = true)
|
||||
is VerificationCodeScreenEvents.DismissContactSupport -> state.copy(showContactSupportSheet = false)
|
||||
is VerificationCodeScreenEvents.ConsumeInnerOneTimeEvent -> state.copy(oneTimeEvent = null)
|
||||
is VerificationCodeScreenEvents.NetworkErrorSnackbarDismissed -> state.copy(snackbars = state.snackbars.copy(networkError = false))
|
||||
is VerificationCodeScreenEvents.UnknownErrorSnackbarDismissed -> state.copy(snackbars = state.snackbars.copy(unknownError = false))
|
||||
is VerificationCodeScreenEvents.RateLimitedSnackbarDismissed -> state.copy(snackbars = state.snackbars.copy(rateLimitedRetryAfter = null))
|
||||
is VerificationCodeScreenEvents.UnableToSendSmsSnackbarDismissed -> state.copy(snackbars = state.snackbars.copy(unableToSendSms = false))
|
||||
is VerificationCodeScreenEvents.CouldNotRequestCodeWithSelectedTransportSnackbarDismissed -> state.copy(snackbars = state.snackbars.copy(couldNotRequestCodeWithSelectedTransport = false))
|
||||
is VerificationCodeScreenEvents.IncorrectVerificationCodeSnackbarDismissed -> state.copy(snackbars = state.snackbars.copy(incorrectVerificationCode = false))
|
||||
is VerificationCodeScreenEvents.RegistrationErrorSnackbarDismissed -> state.copy(snackbars = state.snackbars.copy(registrationError = false))
|
||||
is VerificationCodeScreenEvents.CountdownTick -> applyCountdownTick(state)
|
||||
is VerificationCodeScreenEvents.Foregrounded -> applyForegrounded(state)
|
||||
}
|
||||
@@ -309,7 +314,7 @@ class VerificationCodeViewModel(
|
||||
is NetworkController.SubmitVerificationCodeError.InvalidSessionIdOrVerificationCode -> {
|
||||
Log.w(TAG, "[SubmitCode] Invalid sessionId or verification code entered. This is distinct from an *incorrect* verification code. Body: ${error.message}")
|
||||
val newAttempts = state.incorrectCodeAttempts + 1
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.IncorrectVerificationCode, incorrectCodeAttempts = newAttempts, digits = VerificationCodeState.emptyDigits(), focusedDigitIndex = 0)
|
||||
return state.copy(snackbars = state.snackbars.copy(incorrectVerificationCode = true), incorrectCodeAttempts = newAttempts, digits = VerificationCodeState.emptyDigits(), focusedDigitIndex = 0)
|
||||
}
|
||||
is NetworkController.SubmitVerificationCodeError.SessionNotFound -> {
|
||||
Log.w(TAG, "[SubmitCode] Session not found: ${error.message}. Navigating back to phone number entry.")
|
||||
@@ -328,17 +333,17 @@ class VerificationCodeViewModel(
|
||||
}
|
||||
is NetworkController.SubmitVerificationCodeError.RateLimited -> {
|
||||
Log.w(TAG, "[SubmitCode] Rate limited (retryAfter: ${error.retryAfter}).")
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.RateLimited(error.retryAfter))
|
||||
return state.copy(snackbars = state.snackbars.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
}
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[SubmitCode] Network error.", result.networkError)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
return state.copy(snackbars = state.snackbars.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[SubmitCode] Unknown error when submitting verification code.", result.cause)
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
return state.copy(snackbars = state.snackbars.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +352,7 @@ class VerificationCodeViewModel(
|
||||
if (!sessionMetadata.verified) {
|
||||
Log.w(TAG, "[SubmitCode] Verification code was incorrect.")
|
||||
val newAttempts = state.incorrectCodeAttempts + 1
|
||||
return state.copy(oneTimeEvent = OneTimeEvent.IncorrectVerificationCode, incorrectCodeAttempts = newAttempts, digits = VerificationCodeState.emptyDigits(), focusedDigitIndex = 0)
|
||||
return state.copy(snackbars = state.snackbars.copy(incorrectVerificationCode = true), incorrectCodeAttempts = newAttempts, digits = VerificationCodeState.emptyDigits(), focusedDigitIndex = 0)
|
||||
}
|
||||
|
||||
// Attempt to register
|
||||
@@ -388,11 +393,11 @@ class VerificationCodeViewModel(
|
||||
}
|
||||
is NetworkController.RegisterAccountError.RateLimited -> {
|
||||
Log.w(TAG, "[Register] Rate limited (retryAfter: ${error.retryAfter}).")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.RateLimited(error.retryAfter))
|
||||
state.copy(snackbars = state.snackbars.copy(rateLimitedRetryAfter = error.retryAfter))
|
||||
}
|
||||
is NetworkController.RegisterAccountError.InvalidRequest -> {
|
||||
Log.w(TAG, "[Register] Invalid request when registering account: ${error.message}")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.RegistrationError)
|
||||
state.copy(snackbars = state.snackbars.copy(registrationError = true))
|
||||
}
|
||||
is NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect -> {
|
||||
error("[Register] Got told the registration recovery password incorrect. We don't use the RRP in this flow, and should never get this error. Resetting. Message: ${error.message}")
|
||||
@@ -401,11 +406,11 @@ class VerificationCodeViewModel(
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[Register] Network error.", registerResult.networkError)
|
||||
state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
state.copy(snackbars = state.snackbars.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[Register] Unknown error when registering account.", registerResult.cause)
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
state.copy(snackbars = state.snackbars.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,13 +443,13 @@ class VerificationCodeViewModel(
|
||||
when (val error = result.error) {
|
||||
is NetworkController.RequestVerificationCodeError.InvalidRequest -> {
|
||||
Log.w(TAG, "[RequestCode][$transport] Invalid request: ${error.message}")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
state.copy(snackbars = state.snackbars.copy(unknownError = true))
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.RateLimited -> {
|
||||
Log.w(TAG, "[RequestCode][$transport] Rate limited (retryAfter: ${error.retryAfter}).")
|
||||
parentEventEmitter(RegistrationFlowEvent.SessionUpdated(error.session))
|
||||
state.copy(
|
||||
oneTimeEvent = OneTimeEvent.RateLimited(error.retryAfter),
|
||||
snackbars = state.snackbars.copy(rateLimitedRetryAfter = error.retryAfter),
|
||||
sessionMetadata = error.session,
|
||||
rateLimits = computeRateLimits(error.session)
|
||||
)
|
||||
@@ -453,7 +458,7 @@ class VerificationCodeViewModel(
|
||||
Log.w(TAG, "[RequestCode][$transport] Could not fulfill with requested transport.")
|
||||
parentEventEmitter(RegistrationFlowEvent.SessionUpdated(error.session))
|
||||
state.copy(
|
||||
oneTimeEvent = OneTimeEvent.CouldNotRequestCodeWithSelectedTransport,
|
||||
snackbars = state.snackbars.copy(couldNotRequestCodeWithSelectedTransport = true),
|
||||
sessionMetadata = error.session,
|
||||
rateLimits = computeRateLimits(error.session)
|
||||
)
|
||||
@@ -467,7 +472,7 @@ class VerificationCodeViewModel(
|
||||
Log.w(TAG, "[RequestCode][$transport] Missing request information or already verified.")
|
||||
parentEventEmitter(RegistrationFlowEvent.SessionUpdated(error.session))
|
||||
state.copy(
|
||||
oneTimeEvent = OneTimeEvent.UnableToSendSms,
|
||||
snackbars = state.snackbars.copy(unableToSendSms = true),
|
||||
sessionMetadata = error.session,
|
||||
rateLimits = computeRateLimits(error.session)
|
||||
)
|
||||
@@ -479,17 +484,17 @@ class VerificationCodeViewModel(
|
||||
}
|
||||
is NetworkController.RequestVerificationCodeError.ThirdPartyServiceError -> {
|
||||
Log.w(TAG, "[RequestCode][$transport] Third party service error. ${error.data}")
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnableToSendSms)
|
||||
state.copy(snackbars = state.snackbars.copy(unableToSendSms = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
is RequestResult.RetryableNetworkError -> {
|
||||
Log.w(TAG, "[RequestCode][$transport] Network error.", result.networkError)
|
||||
state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
|
||||
state.copy(snackbars = state.snackbars.copy(networkError = true))
|
||||
}
|
||||
is RequestResult.ApplicationError -> {
|
||||
Log.w(TAG, "[RequestCode][$transport] Unknown application error.", result.cause)
|
||||
state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
|
||||
state.copy(snackbars = state.snackbars.copy(unknownError = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-18
@@ -7,9 +7,7 @@ package org.signal.registration.screens.devicetransfer.complete
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.contains
|
||||
import assertk.assertions.hasSize
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isNull
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.mockk
|
||||
@@ -98,22 +96,6 @@ class DeviceTransferCompleteViewModelTest {
|
||||
assertThat(emittedStates).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeOneTimeEvent clears the one-time event without touching the repository`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferCompleteState(),
|
||||
DeviceTransferCompleteScreenEvents.ConsumeOneTimeEvent,
|
||||
parentEventEmitter,
|
||||
mockRepository,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
coVerify(exactly = 0) { mockRepository.setRestoreDecision(any()) }
|
||||
coVerify(exactly = 0) { mockRepository.restoreAccountRecord(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClicked through the real event channel hands off to the repository`() = runTest {
|
||||
viewModel.onEvent(DeviceTransferCompleteScreenEvents.ContinueClicked)
|
||||
|
||||
-16
@@ -7,9 +7,7 @@ package org.signal.registration.screens.devicetransfer.instructions
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.hasSize
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
@@ -74,20 +72,6 @@ class DeviceTransferInstructionsViewModelTest {
|
||||
assertThat(emittedStates).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeOneTimeEvent clears the one-time event and emits no navigation`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferInstructionsState(),
|
||||
DeviceTransferInstructionsScreenEvents.ConsumeOneTimeEvent,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
assertThat(emittedEvents).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClicked through the real event channel navigates to Setup`() = runTest {
|
||||
viewModel.onEvent(DeviceTransferInstructionsScreenEvents.ContinueClicked)
|
||||
|
||||
-12
@@ -212,18 +212,6 @@ class DeviceTransferProgressViewModelTest {
|
||||
assertThat(emittedEvents).containsExactly(RegistrationFlowEvent.NavigateToScreen(RegistrationRoute.DeviceTransferInstructions))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeOneTimeEvent clears the one-time event`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferProgressState(oneTimeEvent = DeviceTransferProgressState.OneTimeEvent.TransferCanceled),
|
||||
DeviceTransferProgressScreenEvents.ConsumeOneTimeEvent,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CancelConfirmed through the real event channel navigates back`() = runTest {
|
||||
viewModel.onEvent(DeviceTransferProgressScreenEvents.CancelConfirmed)
|
||||
|
||||
+11
-12
@@ -14,7 +14,6 @@ import assertk.assertions.doesNotContain
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -235,11 +234,11 @@ class DeviceTransferSetupViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(DeviceTransferSetupState.OneTimeEvent.RequestLocationPermission)
|
||||
assertThat(emittedStates.last().pendingActions.requestLocationPermission).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OpenLocationSettingsClicked emits the open-location one-time event`() = runTest {
|
||||
fun `OpenLocationSettingsClicked sets the open-location pending action`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(),
|
||||
DeviceTransferSetupScreenEvents.OpenLocationSettingsClicked,
|
||||
@@ -247,11 +246,11 @@ class DeviceTransferSetupViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(DeviceTransferSetupState.OneTimeEvent.OpenLocationSettings)
|
||||
assertThat(emittedStates.last().pendingActions.openLocationSettings).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OpenWifiSettingsClicked emits the open-wifi one-time event`() = runTest {
|
||||
fun `OpenWifiSettingsClicked sets the open-wifi pending action`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(),
|
||||
DeviceTransferSetupScreenEvents.OpenWifiSettingsClicked,
|
||||
@@ -259,11 +258,11 @@ class DeviceTransferSetupViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(DeviceTransferSetupState.OneTimeEvent.OpenWifiSettings)
|
||||
assertThat(emittedStates.last().pendingActions.openWifiSettings).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OpenAppSettingsClicked emits the open-app-settings one-time event`() = runTest {
|
||||
fun `OpenAppSettingsClicked sets the open-app-settings pending action`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(),
|
||||
DeviceTransferSetupScreenEvents.OpenAppSettingsClicked,
|
||||
@@ -271,7 +270,7 @@ class DeviceTransferSetupViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(DeviceTransferSetupState.OneTimeEvent.OpenAppSettings)
|
||||
assertThat(emittedStates.last().pendingActions.openAppSettings).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -338,15 +337,15 @@ class DeviceTransferSetupViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeOneTimeEvent clears the one-time event`() = runTest {
|
||||
fun `OpenWifiSettingsHandled clears only the open-wifi pending action`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(oneTimeEvent = DeviceTransferSetupState.OneTimeEvent.OpenWifiSettings),
|
||||
DeviceTransferSetupScreenEvents.ConsumeOneTimeEvent,
|
||||
DeviceTransferSetupState(pendingActions = DeviceTransferSetupState.PendingActions(openWifiSettings = true, requestLocationPermission = true)),
|
||||
DeviceTransferSetupScreenEvents.OpenWifiSettingsHandled,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
assertThat(emittedStates.last().pendingActions).isEqualTo(DeviceTransferSetupState.PendingActions(requestLocationPermission = true))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
+21
-30
@@ -433,20 +433,20 @@ class PhoneNumberEntryViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeInnerOneTimeEvent clears inner event`() = runTest {
|
||||
fun `NetworkErrorDialogDismissed clears only the network error dialog`() = runTest {
|
||||
val initialState = PhoneNumberEntryState(
|
||||
oneTimeEvent = PhoneNumberEntryState.OneTimeEvent.NetworkError
|
||||
dialogs = PhoneNumberEntryState.Dialogs(networkError = true, unknownError = true)
|
||||
)
|
||||
|
||||
viewModel.applyEvent(
|
||||
initialState,
|
||||
PhoneNumberEntryScreenEvents.ConsumeOneTimeEvent,
|
||||
PhoneNumberEntryScreenEvents.NetworkErrorDialogDismissed,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
assertThat(emittedStates.last().dialogs).isEqualTo(PhoneNumberEntryState.Dialogs(unknownError = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -526,7 +526,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
val result = emittedStates.last()
|
||||
assertThat(result.nationalNumber).isEqualTo("5551234567")
|
||||
assertThat(result.showDialog).isTrue()
|
||||
assertThat(result.dialogs.confirmNumber).isTrue()
|
||||
|
||||
// We only open the dialog; we do not submit on our own.
|
||||
assertThat(emittedEvents).isEmpty()
|
||||
@@ -543,7 +543,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().showDialog).isFalse()
|
||||
assertThat(emittedStates.last().dialogs.confirmNumber).isFalse()
|
||||
assertThat(emittedEvents).isEmpty()
|
||||
}
|
||||
|
||||
@@ -558,7 +558,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().nationalNumber).isEqualTo("5551234567")
|
||||
assertThat(emittedStates.last().showDialog).isFalse()
|
||||
assertThat(emittedStates.last().dialogs.confirmNumber).isFalse()
|
||||
assertThat(emittedEvents).isEmpty()
|
||||
}
|
||||
|
||||
@@ -722,10 +722,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
assertThat(emittedStates.first().showSpinner).isTrue()
|
||||
assertThat(emittedStates.last().showSpinner).isFalse()
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<PhoneNumberEntryState.OneTimeEvent.RateLimited>()
|
||||
.prop(PhoneNumberEntryState.OneTimeEvent.RateLimited::retryAfter)
|
||||
.isEqualTo(60.seconds)
|
||||
assertThat(emittedStates.last().dialogs.rateLimitedRetryAfter).isEqualTo(60.seconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -746,7 +743,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
assertThat(emittedStates.first().showSpinner).isTrue()
|
||||
assertThat(emittedStates.last().showSpinner).isFalse()
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.InvalidPhoneNumber)
|
||||
assertThat(emittedStates.last().dialogs.invalidPhoneNumber).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -765,7 +762,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
assertThat(emittedStates.first().showSpinner).isTrue()
|
||||
assertThat(emittedStates.last().showSpinner).isFalse()
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().dialogs.networkError).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -784,7 +781,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
assertThat(emittedStates.first().showSpinner).isTrue()
|
||||
assertThat(emittedStates.last().showSpinner).isFalse()
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -837,7 +834,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
assertThat(emittedStates.first().showSpinner).isTrue()
|
||||
assertThat(emittedStates.last().showSpinner).isFalse()
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull().isInstanceOf<PhoneNumberEntryState.OneTimeEvent.RateLimited>()
|
||||
assertThat(emittedStates.last().dialogs.rateLimitedRetryAfter).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -888,7 +885,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
assertThat(emittedStates.first().showSpinner).isTrue()
|
||||
assertThat(emittedStates.last().showSpinner).isFalse()
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.CouldNotRequestCodeWithSelectedTransport)
|
||||
assertThat(emittedStates.last().dialogs.couldNotRequestCodeWithSelectedTransport).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -915,7 +912,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
assertThat(emittedStates.first().showSpinner).isTrue()
|
||||
assertThat(emittedStates.last().showSpinner).isFalse()
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.UnableToSendSms)
|
||||
assertThat(emittedStates.last().dialogs.unableToSendSms).isTrue()
|
||||
}
|
||||
|
||||
// ==================== Push Challenge Tests ====================
|
||||
@@ -1154,7 +1151,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.CaptchaCompleted("captcha-token"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1187,10 +1184,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.CaptchaCompleted("captcha-token"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<PhoneNumberEntryState.OneTimeEvent.RateLimited>()
|
||||
.prop(PhoneNumberEntryState.OneTimeEvent.RateLimited::retryAfter)
|
||||
.isEqualTo(45.seconds)
|
||||
assertThat(emittedStates.last().dialogs.rateLimitedRetryAfter).isEqualTo(45.seconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1206,7 +1200,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.CaptchaCompleted("captcha-token"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1220,7 +1214,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.CaptchaCompleted("captcha-token"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().dialogs.networkError).isTrue()
|
||||
}
|
||||
|
||||
// ==================== ParentStateChanged Tests ====================
|
||||
@@ -1482,10 +1476,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.PhoneNumberConfirmed, parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<PhoneNumberEntryState.OneTimeEvent.RateLimited>()
|
||||
.prop(PhoneNumberEntryState.OneTimeEvent.RateLimited::retryAfter)
|
||||
.isEqualTo(30.seconds)
|
||||
assertThat(emittedStates.last().dialogs.rateLimitedRetryAfter).isEqualTo(30.seconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1566,7 +1557,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.PhoneNumberConfirmed, parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().dialogs.networkError).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1587,7 +1578,7 @@ class PhoneNumberEntryViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.PhoneNumberConfirmed, parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PhoneNumberEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+18
-13
@@ -10,11 +10,8 @@ import assertk.assertions.contains
|
||||
import assertk.assertions.hasSize
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isInstanceOf
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
@@ -187,7 +184,7 @@ class PinCreationViewModelTest {
|
||||
viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456"))
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(states.last().oneTimeEvent).isEqualTo(PinCreationState.OneTimeEvent.ServiceError)
|
||||
assertThat(states.last().dialogs.serviceError).isTrue()
|
||||
assertThat(states.last().loading).isFalse()
|
||||
}
|
||||
|
||||
@@ -203,7 +200,7 @@ class PinCreationViewModelTest {
|
||||
viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456"))
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(states.last().oneTimeEvent).isEqualTo(PinCreationState.OneTimeEvent.ServiceError)
|
||||
assertThat(states.last().dialogs.serviceError).isTrue()
|
||||
assertThat(states.last().loading).isFalse()
|
||||
}
|
||||
|
||||
@@ -220,21 +217,29 @@ class PinCreationViewModelTest {
|
||||
viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456"))
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(states.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<PinCreationState.OneTimeEvent.NetworkError>()
|
||||
.prop(PinCreationState.OneTimeEvent.NetworkError::retryAfter)
|
||||
.isEqualTo(retryAfter)
|
||||
assertThat(states.last().dialogs.networkError).isEqualTo(PinCreationState.Dialogs.NetworkError(retryAfter))
|
||||
assertThat(states.last().loading).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeOneTimeEvent clears the one-time event`() = runTest(testDispatcher) {
|
||||
fun `ServiceErrorDialogDismissed clears only the service error dialog`() = runTest(testDispatcher) {
|
||||
val states = collectStates()
|
||||
val stateWithEvent = PinCreationState(oneTimeEvent = PinCreationState.OneTimeEvent.ServiceError)
|
||||
val networkError = PinCreationState.Dialogs.NetworkError(retryAfter = null)
|
||||
val stateWithEvent = PinCreationState(dialogs = PinCreationState.Dialogs(serviceError = true, networkError = networkError))
|
||||
|
||||
viewModel.applyEvent(stateWithEvent, PinCreationScreenEvents.ConsumeOneTimeEvent)
|
||||
viewModel.applyEvent(stateWithEvent, PinCreationScreenEvents.ServiceErrorDialogDismissed)
|
||||
|
||||
assertThat(states.last().oneTimeEvent).isNull()
|
||||
assertThat(states.last().dialogs).isEqualTo(PinCreationState.Dialogs(networkError = networkError))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NetworkErrorDialogDismissed clears only the network error dialog`() = runTest(testDispatcher) {
|
||||
val states = collectStates()
|
||||
val stateWithEvent = PinCreationState(dialogs = PinCreationState.Dialogs(serviceError = true, networkError = PinCreationState.Dialogs.NetworkError(retryAfter = null)))
|
||||
|
||||
viewModel.applyEvent(stateWithEvent, PinCreationScreenEvents.NetworkErrorDialogDismissed)
|
||||
|
||||
assertThat(states.last().dialogs).isEqualTo(PinCreationState.Dialogs(serviceError = true))
|
||||
}
|
||||
|
||||
// ==================== OptOut Tests ====================
|
||||
|
||||
+8
-11
@@ -10,7 +10,7 @@ import assertk.assertThat
|
||||
import assertk.assertions.hasSize
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
@@ -184,7 +184,7 @@ class PinEntryForRegistrationLockViewModelTest {
|
||||
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().dialogs.networkError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ class PinEntryForRegistrationLockViewModelTest {
|
||||
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -338,10 +338,7 @@ class PinEntryForRegistrationLockViewModelTest {
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(1)
|
||||
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<PinEntryState.OneTimeEvent.RateLimited>()
|
||||
.prop(PinEntryState.OneTimeEvent.RateLimited::retryAfter)
|
||||
.isEqualTo(retryAfter)
|
||||
assertThat(emittedStates.last().dialogs.rateLimitedRetryAfter).isEqualTo(retryAfter)
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -361,7 +358,7 @@ class PinEntryForRegistrationLockViewModelTest {
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(1)
|
||||
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -381,7 +378,7 @@ class PinEntryForRegistrationLockViewModelTest {
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(1)
|
||||
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -399,7 +396,7 @@ class PinEntryForRegistrationLockViewModelTest {
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(1)
|
||||
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().dialogs.networkError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -417,7 +414,7 @@ class PinEntryForRegistrationLockViewModelTest {
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(1)
|
||||
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
|
||||
+6
-10
@@ -9,8 +9,7 @@ import assertk.assertThat
|
||||
import assertk.assertions.hasSize
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.prop
|
||||
import assertk.assertions.isTrue
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
@@ -142,7 +141,7 @@ class PinEntryForSmsBypassViewModelTest {
|
||||
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().dialogs.networkError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -156,7 +155,7 @@ class PinEntryForSmsBypassViewModelTest {
|
||||
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -187,7 +186,7 @@ class PinEntryForSmsBypassViewModelTest {
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(1)
|
||||
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().dialogs.networkError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -205,7 +204,7 @@ class PinEntryForSmsBypassViewModelTest {
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(1)
|
||||
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -266,10 +265,7 @@ class PinEntryForSmsBypassViewModelTest {
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(1)
|
||||
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<PinEntryState.OneTimeEvent.RateLimited>()
|
||||
.prop(PinEntryState.OneTimeEvent.RateLimited::retryAfter)
|
||||
.isEqualTo(retryAfter)
|
||||
assertThat(emittedStates.last().dialogs.rateLimitedRetryAfter).isEqualTo(retryAfter)
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -9,6 +9,7 @@ import assertk.assertThat
|
||||
import assertk.assertions.hasSize
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
@@ -126,7 +127,7 @@ class PinEntryForSvrRestoreViewModelTest {
|
||||
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().dialogs.networkError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -140,7 +141,7 @@ class PinEntryForSvrRestoreViewModelTest {
|
||||
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -233,7 +234,7 @@ class PinEntryForSvrRestoreViewModelTest {
|
||||
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().dialogs.networkError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
@@ -253,7 +254,7 @@ class PinEntryForSvrRestoreViewModelTest {
|
||||
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
|
||||
|
||||
assertThat(emittedParentEvents).hasSize(0)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().dialogs.unknownError).isTrue()
|
||||
assertThat(emittedStates.last().loading).isEqualTo(false)
|
||||
}
|
||||
|
||||
|
||||
+32
-45
@@ -9,7 +9,6 @@ import assertk.assertThat
|
||||
import assertk.assertions.hasSize
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
@@ -139,8 +138,8 @@ class VerificationCodeViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ParentStateChanged preserves existing oneTimeEvent`() = runTest {
|
||||
val state = VerificationCodeState(oneTimeEvent = VerificationCodeState.OneTimeEvent.NetworkError)
|
||||
fun `ParentStateChanged preserves existing snackbars`() = runTest {
|
||||
val state = VerificationCodeState(snackbars = VerificationCodeState.Snackbars(networkError = true))
|
||||
val sessionMetadata = createSessionMetadata()
|
||||
val parentFlowState = RegistrationFlowState(
|
||||
sessionMetadata = sessionMetadata,
|
||||
@@ -149,37 +148,37 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(state, VerificationCodeScreenEvents.ParentStateChanged(parentFlowState), stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().snackbars.networkError).isTrue()
|
||||
}
|
||||
|
||||
// ==================== applyEvent: ConsumeInnerOneTimeEvent Tests ====================
|
||||
// ==================== applyEvent: Snackbar Dismissal Tests ====================
|
||||
|
||||
@Test
|
||||
fun `ConsumeInnerOneTimeEvent clears oneTimeEvent`() = runTest {
|
||||
fun `NetworkErrorSnackbarDismissed clears only the network error snackbar`() = runTest {
|
||||
val initialState = VerificationCodeState(
|
||||
oneTimeEvent = VerificationCodeState.OneTimeEvent.NetworkError
|
||||
snackbars = VerificationCodeState.Snackbars(networkError = true, incorrectVerificationCode = true)
|
||||
)
|
||||
|
||||
viewModel.applyEvent(
|
||||
initialState,
|
||||
VerificationCodeScreenEvents.ConsumeInnerOneTimeEvent,
|
||||
VerificationCodeScreenEvents.NetworkErrorSnackbarDismissed,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
assertThat(emittedStates.last().snackbars).isEqualTo(VerificationCodeState.Snackbars(incorrectVerificationCode = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeInnerOneTimeEvent with null event returns state with null event`() = runTest {
|
||||
val initialState = VerificationCodeState(oneTimeEvent = null)
|
||||
fun `NetworkErrorSnackbarDismissed with no snackbars showing leaves snackbars cleared`() = runTest {
|
||||
val initialState = VerificationCodeState()
|
||||
|
||||
viewModel.applyEvent(
|
||||
initialState,
|
||||
VerificationCodeScreenEvents.ConsumeInnerOneTimeEvent,
|
||||
VerificationCodeScreenEvents.NetworkErrorSnackbarDismissed,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
assertThat(emittedStates.last().snackbars).isEqualTo(VerificationCodeState.Snackbars())
|
||||
}
|
||||
|
||||
// ==================== applyEvent: SMS Auto-Fill Tests ====================
|
||||
@@ -431,7 +430,7 @@ class VerificationCodeViewModelTest {
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().digits).isEqualTo(listOf("", "", "", "", "", ""))
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.IncorrectVerificationCode)
|
||||
assertThat(emittedStates.last().snackbars.incorrectVerificationCode).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -626,7 +625,7 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.IncorrectVerificationCode)
|
||||
assertThat(emittedStates.last().snackbars.incorrectVerificationCode).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -714,10 +713,7 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<VerificationCodeState.OneTimeEvent.RateLimited>()
|
||||
.prop(VerificationCodeState.OneTimeEvent.RateLimited::retryAfter)
|
||||
.isEqualTo(60.seconds)
|
||||
assertThat(emittedStates.last().snackbars.rateLimitedRetryAfter).isEqualTo(60.seconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -737,7 +733,7 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().snackbars.networkError).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -757,7 +753,7 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().snackbars.unknownError).isTrue()
|
||||
}
|
||||
|
||||
// ==================== applyEvent: CodeEntered - Registration Errors ====================
|
||||
@@ -806,10 +802,7 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<VerificationCodeState.OneTimeEvent.RateLimited>()
|
||||
.prop(VerificationCodeState.OneTimeEvent.RateLimited::retryAfter)
|
||||
.isEqualTo(30.seconds)
|
||||
assertThat(emittedStates.last().snackbars.rateLimitedRetryAfter).isEqualTo(30.seconds)
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@@ -834,7 +827,7 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.RegistrationError)
|
||||
assertThat(emittedStates.last().snackbars.registrationError).isTrue()
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@@ -859,7 +852,7 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.RegistrationError)
|
||||
assertThat(emittedStates.last().snackbars.registrationError).isTrue()
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@@ -882,7 +875,7 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().snackbars.networkError).isTrue()
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@@ -905,7 +898,7 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().snackbars.unknownError).isTrue()
|
||||
}
|
||||
|
||||
// ==================== applyEvent: ResendSms Tests ====================
|
||||
@@ -967,10 +960,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<VerificationCodeState.OneTimeEvent.RateLimited>()
|
||||
.prop(VerificationCodeState.OneTimeEvent.RateLimited::retryAfter)
|
||||
.isEqualTo(45.seconds)
|
||||
assertThat(emittedStates.last().snackbars.rateLimitedRetryAfter).isEqualTo(45.seconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -985,7 +975,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().snackbars.unknownError).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1000,7 +990,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.CouldNotRequestCodeWithSelectedTransport)
|
||||
assertThat(emittedStates.last().snackbars.couldNotRequestCodeWithSelectedTransport).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1047,7 +1037,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.UnableToSendSms)
|
||||
assertThat(emittedStates.last().snackbars.unableToSendSms).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1064,7 +1054,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.UnableToSendSms)
|
||||
assertThat(emittedStates.last().snackbars.unableToSendSms).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1077,7 +1067,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.NetworkError)
|
||||
assertThat(emittedStates.last().snackbars.networkError).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1090,7 +1080,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.UnknownError)
|
||||
assertThat(emittedStates.last().snackbars.unknownError).isTrue()
|
||||
}
|
||||
|
||||
// ==================== applyEvent: CallMe Tests ====================
|
||||
@@ -1132,10 +1122,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.CallMe, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNotNull()
|
||||
.isInstanceOf<VerificationCodeState.OneTimeEvent.RateLimited>()
|
||||
.prop(VerificationCodeState.OneTimeEvent.RateLimited::retryAfter)
|
||||
.isEqualTo(90.seconds)
|
||||
assertThat(emittedStates.last().snackbars.rateLimitedRetryAfter).isEqualTo(90.seconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1150,7 +1137,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.CallMe, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.CouldNotRequestCodeWithSelectedTransport)
|
||||
assertThat(emittedStates.last().snackbars.couldNotRequestCodeWithSelectedTransport).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1167,7 +1154,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.CallMe, stateEmitter)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(VerificationCodeState.OneTimeEvent.UnableToSendSms)
|
||||
assertThat(emittedStates.last().snackbars.unableToSendSms).isTrue()
|
||||
}
|
||||
|
||||
// ==================== applyEvent: Foregrounded Tests ====================
|
||||
|
||||
Reference in New Issue
Block a user