Merge pull request #6888 from vector-im/feature/adm/change-server-on-error

FTUE - Allow changing server on onboarding start error
This commit is contained in:
Benoit Marty 2022-08-23 15:05:51 +02:00 committed by GitHub
commit 94e755552c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 103 additions and 42 deletions

1
changelog.d/6718.bugfix Normal file
View File

@ -0,0 +1 @@
Fixes onboarding requiring matrix.org to be accessible on the first step, the server can now be manually changed

View File

@ -89,6 +89,8 @@ fun Throwable.isInvalidUIAAuth() = this is Failure.ServerError &&
fun Throwable.isHomeserverUnavailable() = this is Failure.NetworkConnection && fun Throwable.isHomeserverUnavailable() = this is Failure.NetworkConnection &&
this.ioException is UnknownHostException this.ioException is UnknownHostException
fun Throwable.isHomeserverConnectionError() = this is Failure.NetworkConnection
fun Throwable.isMissingEmailVerification() = this is Failure.ServerError && fun Throwable.isMissingEmailVerification() = this is Failure.ServerError &&
error.code == MatrixError.M_UNAUTHORIZED && error.code == MatrixError.M_UNAUTHORIZED &&
error.message == "Unable to get validated threepid" error.message == "Unable to get validated threepid"

View File

@ -29,7 +29,6 @@ import org.matrix.android.sdk.api.failure.Failure as SdkFailure
sealed class OnboardingViewEvents : VectorViewEvents { sealed class OnboardingViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : OnboardingViewEvents() data class Loading(val message: CharSequence? = null) : OnboardingViewEvents()
data class Failure(val throwable: Throwable) : OnboardingViewEvents() data class Failure(val throwable: Throwable) : OnboardingViewEvents()
data class DeeplinkAuthenticationFailure(val retryAction: OnboardingAction) : OnboardingViewEvents()
data class UnrecognisedCertificateFailure(val retryAction: OnboardingAction, val cause: SdkFailure.UnrecognizedCertificateFailure) : OnboardingViewEvents() data class UnrecognisedCertificateFailure(val retryAction: OnboardingAction, val cause: SdkFailure.UnrecognizedCertificateFailure) : OnboardingViewEvents()
object DisplayRegistrationFallback : OnboardingViewEvents() object DisplayRegistrationFallback : OnboardingViewEvents()

View File

@ -61,6 +61,7 @@ import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.isHomeserverConnectionError
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
import org.matrix.android.sdk.api.failure.isUnrecognisedCertificate import org.matrix.android.sdk.api.failure.isUnrecognisedCertificate
import org.matrix.android.sdk.api.network.ssl.Fingerprint import org.matrix.android.sdk.api.network.ssl.Fingerprint
@ -701,14 +702,15 @@ class OnboardingViewModel @AssistedInject constructor(
private fun onAuthenticationStartError(error: Throwable, trigger: OnboardingAction.HomeServerChange) { private fun onAuthenticationStartError(error: Throwable, trigger: OnboardingAction.HomeServerChange) {
when { when {
error.isHomeserverUnavailable() && applicationContext.inferNoConnectivity(sdkIntProvider) -> _viewEvents.post( error.isHomeserverUnavailable() && applicationContext.inferNoConnectivity(sdkIntProvider) -> _viewEvents.post(OnboardingViewEvents.Failure(error))
OnboardingViewEvents.Failure(error) isUnableToSelectServer(error, trigger) -> {
) withState { state ->
deeplinkUrlIsUnavailable(error, trigger) -> _viewEvents.post( when {
OnboardingViewEvents.DeeplinkAuthenticationFailure( canEditServerSelectionError(state) -> handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection))
retryAction = (trigger as OnboardingAction.HomeServerChange.SelectHomeServer).resetToDefaultUrl() else -> _viewEvents.post(OnboardingViewEvents.Failure(error))
) }
) }
}
error.isUnrecognisedCertificate() -> { error.isUnrecognisedCertificate() -> {
_viewEvents.post(OnboardingViewEvents.UnrecognisedCertificateFailure(trigger, error as Failure.UnrecognizedCertificateFailure)) _viewEvents.post(OnboardingViewEvents.UnrecognisedCertificateFailure(trigger, error as Failure.UnrecognizedCertificateFailure))
} }
@ -716,11 +718,12 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun deeplinkUrlIsUnavailable(error: Throwable, trigger: OnboardingAction.HomeServerChange) = error.isHomeserverUnavailable() && private fun canEditServerSelectionError(state: OnboardingViewState) =
loginConfig != null && (state.onboardingFlow == OnboardingFlow.SignIn && vectorFeatures.isOnboardingCombinedLoginEnabled()) ||
trigger is OnboardingAction.HomeServerChange.SelectHomeServer (state.onboardingFlow == OnboardingFlow.SignUp && vectorFeatures.isOnboardingCombinedRegisterEnabled())
private fun OnboardingAction.HomeServerChange.SelectHomeServer.resetToDefaultUrl() = copy(homeServerUrl = defaultHomeserverUrl) private fun isUnableToSelectServer(error: Throwable, trigger: OnboardingAction.HomeServerChange) =
trigger is OnboardingAction.HomeServerChange.SelectHomeServer && error.isHomeserverConnectionError()
private suspend fun onAuthenticationStartedSuccess( private suspend fun onAuthenticationStartedSuccess(
trigger: OnboardingAction.HomeServerChange, trigger: OnboardingAction.HomeServerChange,
@ -807,6 +810,8 @@ class OnboardingViewModel @AssistedInject constructor(
return loginConfig?.homeServerUrl return loginConfig?.homeServerUrl
} }
fun getDefaultHomeserverUrl() = defaultHomeserverUrl
fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?): String? { fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?): String? {
setState { setState {
val authDescription = AuthenticationDescription.Register(provider.toAuthenticationType()) val authDescription = AuthenticationDescription.Register(provider.toAuthenticationType())

View File

@ -86,7 +86,7 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt
) )
if (views.chooseServerInput.content().isEmpty()) { if (views.chooseServerInput.content().isEmpty()) {
val userUrlInput = state.selectedHomeserver.userFacingUrl?.toReducedUrlKeepingSchemaIfInsecure() val userUrlInput = state.selectedHomeserver.userFacingUrl?.toReducedUrlKeepingSchemaIfInsecure() ?: viewModel.getDefaultHomeserverUrl()
views.chooseServerInput.editText().setText(userUrlInput) views.chooseServerInput.editText().setText(userUrlInput)
} }
} }

View File

@ -46,6 +46,7 @@ import im.vector.app.features.login.SignMode
import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.login.TextInputFormFragmentMode
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingActivity import im.vector.app.features.onboarding.OnboardingActivity
import im.vector.app.features.onboarding.OnboardingFlow
import im.vector.app.features.onboarding.OnboardingVariant import im.vector.app.features.onboarding.OnboardingVariant
import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewModel import im.vector.app.features.onboarding.OnboardingViewModel
@ -213,7 +214,7 @@ class FtueAuthVariant(
option = commonOption option = commonOption
) )
} }
OnboardingViewEvents.OpenCombinedRegister -> openStartCombinedRegister() OnboardingViewEvents.OpenCombinedRegister -> onStartCombinedRegister()
is OnboardingViewEvents.OnAccountCreated -> onAccountCreated() is OnboardingViewEvents.OnAccountCreated -> onAccountCreated()
OnboardingViewEvents.OnAccountSignedIn -> onAccountSignedIn() OnboardingViewEvents.OnAccountSignedIn -> onAccountSignedIn()
OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName() OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName()
@ -229,42 +230,47 @@ class FtueAuthVariant(
tag = FRAGMENT_EDIT_HOMESERVER_TAG tag = FRAGMENT_EDIT_HOMESERVER_TAG
) )
} }
OnboardingViewEvents.OnHomeserverEdited -> supportFragmentManager.popBackStack( OnboardingViewEvents.OnHomeserverEdited -> {
FRAGMENT_EDIT_HOMESERVER_TAG, supportFragmentManager.popBackStack(
FragmentManager.POP_BACK_STACK_INCLUSIVE FRAGMENT_EDIT_HOMESERVER_TAG,
) FragmentManager.POP_BACK_STACK_INCLUSIVE
)
ensureEditServerBackstack()
}
OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin() OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin()
is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents)
OnboardingViewEvents.DisplayRegistrationFallback -> displayFallbackWebDialog() OnboardingViewEvents.DisplayRegistrationFallback -> displayFallbackWebDialog()
is OnboardingViewEvents.DisplayRegistrationStage -> doStage(viewEvents.stage) is OnboardingViewEvents.DisplayRegistrationStage -> doStage(viewEvents.stage)
OnboardingViewEvents.DisplayStartRegistration -> when { OnboardingViewEvents.DisplayStartRegistration -> when {
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> openStartCombinedRegister() vectorFeatures.isOnboardingCombinedRegisterEnabled() -> onStartCombinedRegister()
else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG) else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
} }
} }
} }
private fun onDeeplinkedHomeserverUnavailable(viewEvents: OnboardingViewEvents.DeeplinkAuthenticationFailure) { private fun ensureEditServerBackstack() {
showHomeserverUnavailableDialog(onboardingViewModel.getInitialHomeServerUrl().orEmpty()) { when (activity.supportFragmentManager.findFragmentById(views.loginFragmentContainer.id)) {
onboardingViewModel.handle(OnboardingAction.ResetDeeplinkConfig) is FtueAuthCombinedLoginFragment,
onboardingViewModel.handle(viewEvents.retryAction) is FtueAuthCombinedRegisterFragment -> {
// do nothing
}
else -> {
withState(onboardingViewModel) { state ->
when (state.onboardingFlow) {
OnboardingFlow.SignIn -> onStartCombinedLogin()
OnboardingFlow.SignUp -> onStartCombinedRegister()
OnboardingFlow.SignInSignUp,
null -> error("${state.onboardingFlow} does not support editing server url")
}
}
}
} }
} }
private fun showHomeserverUnavailableDialog(url: String, action: () -> Unit) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.dialog_title_error)
.setMessage(activity.getString(R.string.login_error_homeserver_from_url_not_found, url))
.setPositiveButton(R.string.login_error_homeserver_from_url_not_found_enter_manual) { _, _ -> action() }
.setNegativeButton(R.string.action_cancel, null)
.show()
}
private fun onStartCombinedLogin() { private fun onStartCombinedLogin() {
addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java, allowStateLoss = true) addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java, allowStateLoss = true)
} }
private fun openStartCombinedRegister() { private fun onStartCombinedRegister() {
addRegistrationStageFragmentToBackstack(FtueAuthCombinedRegisterFragment::class.java, allowStateLoss = true) addRegistrationStageFragmentToBackstack(FtueAuthCombinedRegisterFragment::class.java, allowStateLoss = true)
} }

View File

@ -412,23 +412,59 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given unavailable deeplink, when selecting homeserver, then emits failure with default homeserver as retry action`() = runTest { fun `given in sign in flow, when selecting homeserver fails with network error, then emits Failure`() = runTest {
fakeContext.givenHasConnection() viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignIn))
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, fingerprint = null, A_HOMESERVER_CONFIG) fakeVectorFeatures.givenCombinedLoginEnabled()
fakeStartAuthenticationFlowUseCase.givenHomeserverUnavailable(A_HOMESERVER_CONFIG) givenHomeserverSelectionFailsWith(AN_ERROR)
val test = viewModel.test() val test = viewModel.test()
viewModel.handle(OnboardingAction.InitWith(LoginConfig(A_HOMESERVER_URL, null)))
viewModel.handle(OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL)) viewModel.handle(OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL))
val expectedRetryAction = OnboardingAction.HomeServerChange.SelectHomeServer("${R.string.matrix_org_server_url.toTestString()}/")
test test
.assertStatesChanges( .assertStatesChanges(
initialState, initialState,
{ copy(isLoading = true) }, { copy(isLoading = true) },
{ copy(isLoading = false) } { copy(isLoading = false) }
) )
.assertEvents(OnboardingViewEvents.DeeplinkAuthenticationFailure(expectedRetryAction)) .assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
.finish()
}
@Test
fun `given in sign in flow, when selecting homeserver fails with network error, then emits EditServerSelection`() = runTest {
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignIn))
fakeVectorFeatures.givenCombinedLoginEnabled()
givenHomeserverSelectionFailsWithNetworkError()
val test = viewModel.test()
viewModel.handle(OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL))
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.EditServerSelection)
.finish()
}
@Test
fun `given in sign up flow, when selecting homeserver fails with network error, then emits EditServerSelection`() = runTest {
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
fakeVectorFeatures.givenCombinedRegisterEnabled()
givenHomeserverSelectionFailsWithNetworkError()
val test = viewModel.test()
viewModel.handle(OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL))
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.EditServerSelection)
.finish() .finish()
} }
@ -1142,6 +1178,18 @@ class OnboardingViewModelTest {
private fun initialRegistrationState(homeServerUrl: String) = initialState.copy( private fun initialRegistrationState(homeServerUrl: String) = initialState.copy(
onboardingFlow = OnboardingFlow.SignUp, selectedHomeserver = SelectedHomeserverState(userFacingUrl = homeServerUrl) onboardingFlow = OnboardingFlow.SignUp, selectedHomeserver = SelectedHomeserverState(userFacingUrl = homeServerUrl)
) )
private fun givenHomeserverSelectionFailsWithNetworkError() {
fakeContext.givenHasConnection()
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, fingerprint = null, A_HOMESERVER_CONFIG)
fakeStartAuthenticationFlowUseCase.givenHomeserverUnavailable(A_HOMESERVER_CONFIG)
}
private fun givenHomeserverSelectionFailsWith(cause: Throwable) {
fakeContext.givenHasConnection()
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, fingerprint = null, A_HOMESERVER_CONFIG)
fakeStartAuthenticationFlowUseCase.givenErrors(A_HOMESERVER_CONFIG, cause)
}
} }
private fun HomeServerCapabilities.toPersonalisationState(displayName: String? = null) = PersonalizationState( private fun HomeServerCapabilities.toPersonalisationState(displayName: String? = null) = PersonalizationState(