Add support for login link (#4752)
* Add support for login link https://mobile.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org * Update screenshots * Reduce code duplication * Add test on OnBoardingPresenter * Fix tool * Ignore login parameter if user is not allowed to connect to the provided server. * Improve tests. * Cleanup * Revert change on Project.xml. * Add documentation * Improve LoginHelper * Rename LoginFlow to LoginMode Move LoginFlow to package io.element.android.features.login.impl.login Rename some implementation of LoginMode Rename LoginFlowView to LoginModeView * Change launchMode of MainActivity from `singleTop` to `singleTask` Using launchMode singleTask to avoid multiple instances of the Activity when the app is already open. This is important for incoming share and for opening the application from a mobile.element.io link. Closes #4074 --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
bc759e4129
commit
f20caebdcb
52 changed files with 1092 additions and 363 deletions
|
|
@ -22,6 +22,14 @@ class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint {
|
|||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : LoginEntryPoint.NodeBuilder {
|
||||
override fun params(params: LoginEntryPoint.Params): LoginEntryPoint.NodeBuilder {
|
||||
plugins += LoginFlowNode.Params(
|
||||
accountProvider = params.accountProvider,
|
||||
loginHint = params.loginHint,
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun callback(callback: LoginEntryPoint.Callback): LoginEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.login.api.LoginIntentResolver
|
||||
import io.element.android.features.login.api.LoginParams
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLoginIntentResolver @Inject constructor() : LoginIntentResolver {
|
||||
override fun parse(uriString: String): LoginParams? {
|
||||
val uri = uriString.toUri()
|
||||
if (uri.host != "mobile.element.io") return null
|
||||
if (uri.path?.startsWith("/element")?.not() == true) return null
|
||||
val accountProvider = uri.getQueryParameter("account_provider") ?: return null
|
||||
val loginHint = uri.getQueryParameter("login_hint")
|
||||
return LoginParams(
|
||||
accountProvider = accountProvider,
|
||||
loginHint = loginHint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,16 +28,18 @@ import io.element.android.anvilannotations.ContributesNode
|
|||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.login.api.LoginEntryPoint
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.onboarding.OnBoardingNode
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
|
||||
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
|
||||
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
|
||||
import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode
|
||||
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
||||
import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode
|
||||
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
|
|
@ -63,6 +65,11 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
data class Params(
|
||||
val accountProvider: String?,
|
||||
val loginHint: String?,
|
||||
) : NodeInputs
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var darkTheme: Boolean = false
|
||||
|
||||
|
|
@ -139,8 +146,25 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
override fun onReportProblem() {
|
||||
plugins<LoginEntryPoint.Callback>().forEach { it.onReportProblem() }
|
||||
}
|
||||
|
||||
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||
navigateToMas(oidcDetails)
|
||||
}
|
||||
|
||||
override fun onCreateAccountContinue(url: String) {
|
||||
backstack.push(NavTarget.CreateAccount(url))
|
||||
}
|
||||
|
||||
override fun onLoginPasswordNeeded() {
|
||||
backstack.push(NavTarget.LoginPassword)
|
||||
}
|
||||
}
|
||||
createNode<OnBoardingNode>(buildContext, listOf(callback))
|
||||
val params = inputs<Params>()
|
||||
val inputs = OnBoardingNode.Params(
|
||||
accountProvider = params.accountProvider,
|
||||
loginHint = params.loginHint,
|
||||
)
|
||||
createNode<OnBoardingNode>(buildContext, listOf(callback, inputs))
|
||||
}
|
||||
NavTarget.QrCode -> {
|
||||
createNode<QrCodeLoginFlowNode>(buildContext)
|
||||
|
|
@ -151,16 +175,7 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
)
|
||||
val callback = object : ConfirmAccountProviderNode.Callback {
|
||||
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||
if (oidcEntryPoint.canUseCustomTab()) {
|
||||
// In this case open a Chrome Custom tab
|
||||
activity?.let {
|
||||
customChromeTabStarted = true
|
||||
oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url)
|
||||
}
|
||||
} else {
|
||||
// Fallback to WebView mode
|
||||
backstack.push(NavTarget.OidcView(oidcDetails))
|
||||
}
|
||||
navigateToMas(oidcDetails)
|
||||
}
|
||||
|
||||
override fun onCreateAccountContinue(url: String) {
|
||||
|
|
@ -222,6 +237,19 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun navigateToMas(oidcDetails: OidcDetails) {
|
||||
if (oidcEntryPoint.canUseCustomTab()) {
|
||||
// In this case open a Chrome Custom tab
|
||||
activity?.let {
|
||||
customChromeTabStarted = true
|
||||
oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url)
|
||||
}
|
||||
} else {
|
||||
// Fallback to WebView mode
|
||||
backstack.push(NavTarget.OidcView(oidcDetails))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
activity = requireNotNull(LocalActivity.current)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.login
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter
|
||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.OidcPrompt
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This class is responsible for managing the login flow, including handling OIDC actions and
|
||||
* submitting login requests.
|
||||
* It's an helper to avoid code duplication. It is used by [OnBoardingPresenter] and [ConfirmAccountProviderPresenter].
|
||||
*/
|
||||
class LoginHelper @Inject constructor(
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
|
||||
) {
|
||||
private val loginModeState: MutableState<AsyncData<LoginMode>> = mutableStateOf(AsyncData.Uninitialized)
|
||||
|
||||
@Composable
|
||||
fun collectLoginMode(): State<AsyncData<LoginMode>> {
|
||||
LaunchedEffect(Unit) {
|
||||
oidcActionFlow.collect { oidcAction ->
|
||||
if (oidcAction != null) {
|
||||
onOidcAction(oidcAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
return loginModeState
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
loginModeState.value = AsyncData.Uninitialized
|
||||
}
|
||||
|
||||
fun submit(
|
||||
coroutineScope: CoroutineScope,
|
||||
isAccountCreation: Boolean,
|
||||
homeserverUrl: String,
|
||||
loginHint: String?,
|
||||
) = coroutineScope.launch {
|
||||
suspend {
|
||||
authenticationService.setHomeserver(homeserverUrl).map {
|
||||
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
|
||||
if (matrixHomeServerDetails.supportsOidcLogin) {
|
||||
// Retrieve the details right now
|
||||
val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login
|
||||
LoginMode.Oidc(
|
||||
authenticationService.getOidcUrl(prompt = oidcPrompt, loginHint = loginHint).getOrThrow()
|
||||
)
|
||||
} else if (isAccountCreation) {
|
||||
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
|
||||
LoginMode.AccountCreation(url)
|
||||
} else if (matrixHomeServerDetails.supportsPasswordLogin) {
|
||||
LoginMode.PasswordLogin
|
||||
} else {
|
||||
error("Unsupported login flow")
|
||||
}
|
||||
}.getOrThrow()
|
||||
}.runCatchingUpdatingState(
|
||||
state = loginModeState,
|
||||
errorTransform = {
|
||||
when (it) {
|
||||
is AccountCreationNotSupported -> it
|
||||
else -> ChangeServerError.from(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun onOidcAction(oidcAction: OidcAction) {
|
||||
loginModeState.value = AsyncData.Loading()
|
||||
when (oidcAction) {
|
||||
OidcAction.GoBack -> {
|
||||
authenticationService.cancelOidcLogin()
|
||||
.onSuccess {
|
||||
loginModeState.value = AsyncData.Uninitialized
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loginModeState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
is OidcAction.Success -> {
|
||||
authenticationService.loginWithOidc(oidcAction.url)
|
||||
.onSuccess { _ ->
|
||||
defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loginModeState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
oidcActionFlow.reset()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.login
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
sealed interface LoginMode {
|
||||
data object PasswordLogin : LoginMode
|
||||
data class Oidc(val oidcDetails: OidcDetails) : LoginMode
|
||||
data class AccountCreation(val url: String) : LoginMode
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.login
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun LoginModeView(
|
||||
loginMode: AsyncData<LoginMode>,
|
||||
onClearError: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit
|
||||
) {
|
||||
when (loginMode) {
|
||||
is AsyncData.Failure -> {
|
||||
when (val error = loginMode.error) {
|
||||
is ChangeServerError -> {
|
||||
when (error) {
|
||||
is ChangeServerError.Error -> {
|
||||
ErrorDialog(
|
||||
content = error.message(),
|
||||
onSubmit = onClearError,
|
||||
)
|
||||
}
|
||||
is ChangeServerError.SlidingSyncAlert -> {
|
||||
SlidingSyncNotSupportedDialog(
|
||||
onLearnMoreClick = {
|
||||
onLearnMoreClick()
|
||||
onClearError()
|
||||
},
|
||||
onDismiss = onClearError,
|
||||
)
|
||||
}
|
||||
is ChangeServerError.UnauthorizedAccountProvider -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(
|
||||
id = R.string.screen_change_server_error_unauthorized_homeserver,
|
||||
LocalBuildMeta.current.applicationName,
|
||||
error.unauthorisedAccountProviderTitle,
|
||||
),
|
||||
onSubmit = onClearError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AccountCreationNotSupported -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_account_creation_not_possible),
|
||||
onSubmit = onClearError,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onSubmit = onClearError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncData.Loading -> Unit // The Continue button shows the loading state
|
||||
is AsyncData.Success -> {
|
||||
when (val loginModeData = loginMode.data) {
|
||||
is LoginMode.Oidc -> onOidcDetails(loginModeData.oidcDetails)
|
||||
LoginMode.PasswordLogin -> onNeedLoginPassword()
|
||||
is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url)
|
||||
}
|
||||
}
|
||||
AsyncData.Uninitialized -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.onboarding
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.appconfig.OnBoardingConfig
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Note: this Presenter is ignored regarding code coverage because it cannot reach the coverage threshold.
|
||||
* When this presenter get more code in it, please remove the ignore rule in the kover configuration.
|
||||
*/
|
||||
class OnBoardingPresenter @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
) : Presenter<OnBoardingState> {
|
||||
@Composable
|
||||
override fun present(): OnBoardingState {
|
||||
val canLoginWithQrCode by produceState(initialValue = false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
||||
}
|
||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||
return OnBoardingState(
|
||||
productionApplicationName = buildMeta.productionApplicationName,
|
||||
canLoginWithQrCode = canLoginWithQrCode,
|
||||
canCreateAccount = OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||
canReportBug = canReportBug,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.onboarding
|
||||
|
||||
data class OnBoardingState(
|
||||
val productionApplicationName: String,
|
||||
val canLoginWithQrCode: Boolean,
|
||||
val canCreateAccount: Boolean,
|
||||
val canReportBug: Boolean,
|
||||
)
|
||||
|
|
@ -8,38 +8,20 @@
|
|||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.features.login.impl.login.LoginHelper
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.OidcPrompt
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||
@Assisted private val params: Params,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
|
||||
private val loginHelper: LoginHelper,
|
||||
) : Presenter<ConfirmAccountProviderState> {
|
||||
data class Params(
|
||||
val isAccountCreation: Boolean,
|
||||
|
|
@ -55,91 +37,27 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
val accountProvider by accountProviderDataSource.flow.collectAsState()
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val loginFlowAction: MutableState<AsyncData<LoginFlow>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
oidcActionFlow.collect { oidcAction ->
|
||||
if (oidcAction != null) {
|
||||
onOidcAction(oidcAction, loginFlowAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
val loginMode by loginHelper.collectLoginMode()
|
||||
|
||||
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
||||
when (event) {
|
||||
ConfirmAccountProviderEvents.Continue -> {
|
||||
localCoroutineScope.submit(accountProvider.url, loginFlowAction)
|
||||
loginHelper.submit(
|
||||
coroutineScope = localCoroutineScope,
|
||||
isAccountCreation = params.isAccountCreation,
|
||||
homeserverUrl = accountProvider.url,
|
||||
loginHint = null,
|
||||
)
|
||||
}
|
||||
ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = AsyncData.Uninitialized
|
||||
ConfirmAccountProviderEvents.ClearError -> loginHelper.clearError()
|
||||
}
|
||||
}
|
||||
|
||||
return ConfirmAccountProviderState(
|
||||
accountProvider = accountProvider,
|
||||
isAccountCreation = params.isAccountCreation,
|
||||
loginFlow = loginFlowAction.value,
|
||||
loginMode = loginMode,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.submit(
|
||||
homeserverUrl: String,
|
||||
loginFlowAction: MutableState<AsyncData<LoginFlow>>,
|
||||
) = launch {
|
||||
suspend {
|
||||
authenticationService.setHomeserver(homeserverUrl).map {
|
||||
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
|
||||
if (matrixHomeServerDetails.supportsOidcLogin) {
|
||||
// Retrieve the details right now
|
||||
val oidcPrompt = if (params.isAccountCreation) OidcPrompt.Create else OidcPrompt.Login
|
||||
LoginFlow.OidcFlow(authenticationService.getOidcUrl(oidcPrompt).getOrThrow())
|
||||
} else if (params.isAccountCreation) {
|
||||
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
|
||||
LoginFlow.AccountCreationFlow(url)
|
||||
} else if (matrixHomeServerDetails.supportsPasswordLogin) {
|
||||
LoginFlow.PasswordLogin
|
||||
} else {
|
||||
error("Unsupported login flow")
|
||||
}
|
||||
}.getOrThrow()
|
||||
}.runCatchingUpdatingState(
|
||||
state = loginFlowAction,
|
||||
errorTransform = {
|
||||
when (it) {
|
||||
is AccountCreationNotSupported -> it
|
||||
else -> ChangeServerError.from(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun onOidcAction(
|
||||
oidcAction: OidcAction,
|
||||
loginFlowAction: MutableState<AsyncData<LoginFlow>>,
|
||||
) {
|
||||
loginFlowAction.value = AsyncData.Loading()
|
||||
when (oidcAction) {
|
||||
OidcAction.GoBack -> {
|
||||
authenticationService.cancelOidcLogin()
|
||||
.onSuccess {
|
||||
loginFlowAction.value = AsyncData.Uninitialized
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loginFlowAction.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
is OidcAction.Success -> {
|
||||
authenticationService.loginWithOidc(oidcAction.url)
|
||||
.onSuccess { _ ->
|
||||
defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loginFlowAction.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
oidcActionFlow.reset()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,21 +8,15 @@
|
|||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
// Do not use default value, so no member get forgotten in the presenters.
|
||||
data class ConfirmAccountProviderState(
|
||||
val accountProvider: AccountProvider,
|
||||
val isAccountCreation: Boolean,
|
||||
val loginFlow: AsyncData<LoginFlow>,
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val eventSink: (ConfirmAccountProviderEvents) -> Unit
|
||||
) {
|
||||
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is AsyncData.Uninitialized || loginFlow is AsyncData.Loading)
|
||||
}
|
||||
|
||||
sealed interface LoginFlow {
|
||||
data object PasswordLogin : LoginFlow
|
||||
data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow
|
||||
data class AccountCreationFlow(val url: String) : LoginFlow
|
||||
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
|||
),
|
||||
aConfirmAccountProviderState(
|
||||
isAccountCreation = true,
|
||||
loginFlow = AsyncData.Failure(AccountCreationNotSupported())
|
||||
loginMode = AsyncData.Failure(AccountCreationNotSupported())
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -30,11 +31,11 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
|||
private fun aConfirmAccountProviderState(
|
||||
accountProvider: AccountProvider = anAccountProvider(),
|
||||
isAccountCreation: Boolean = false,
|
||||
loginFlow: AsyncData<LoginFlow> = AsyncData.Uninitialized,
|
||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
eventSink: (ConfirmAccountProviderEvents) -> Unit = {},
|
||||
) = ConfirmAccountProviderState(
|
||||
accountProvider = accountProvider,
|
||||
isAccountCreation = isAccountCreation,
|
||||
loginFlow = loginFlow,
|
||||
loginMode = loginMode,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,15 +19,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.features.login.impl.login.LoginModeView
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
|
|
@ -47,9 +44,9 @@ fun ConfirmAccountProviderView(
|
|||
onChange: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isLoading by remember(state.loginFlow) {
|
||||
val isLoading by remember(state.loginMode) {
|
||||
derivedStateOf {
|
||||
state.loginFlow is AsyncData.Loading
|
||||
state.loginMode is AsyncData.Loading
|
||||
}
|
||||
}
|
||||
val eventSink = state.eventSink
|
||||
|
|
@ -99,48 +96,16 @@ fun ConfirmAccountProviderView(
|
|||
}
|
||||
}
|
||||
) {
|
||||
when (state.loginFlow) {
|
||||
is AsyncData.Failure -> {
|
||||
when (val error = state.loginFlow.error) {
|
||||
is ChangeServerError.Error -> {
|
||||
ErrorDialog(
|
||||
content = error.message(),
|
||||
onSubmit = {
|
||||
eventSink.invoke(ConfirmAccountProviderEvents.ClearError)
|
||||
}
|
||||
)
|
||||
}
|
||||
is ChangeServerError.SlidingSyncAlert -> {
|
||||
SlidingSyncNotSupportedDialog(
|
||||
onLearnMoreClick = {
|
||||
onLearnMoreClick()
|
||||
eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
},
|
||||
onDismiss = {
|
||||
eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
}
|
||||
)
|
||||
}
|
||||
is AccountCreationNotSupported -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_account_creation_not_possible),
|
||||
onSubmit = {
|
||||
eventSink.invoke(ConfirmAccountProviderEvents.ClearError)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncData.Loading -> Unit // The Continue button shows the loading state
|
||||
is AsyncData.Success -> {
|
||||
when (val loginFlowState = state.loginFlow.data) {
|
||||
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
|
||||
LoginFlow.PasswordLogin -> onNeedLoginPassword()
|
||||
is LoginFlow.AccountCreationFlow -> onCreateAccountContinue(loginFlowState.url)
|
||||
}
|
||||
}
|
||||
AsyncData.Uninitialized -> Unit
|
||||
}
|
||||
LoginModeView(
|
||||
loginMode = state.loginMode,
|
||||
onClearError = {
|
||||
eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.onboarding
|
||||
|
||||
sealed interface OnBoardingEvents {
|
||||
data class OnSignIn(
|
||||
val defaultAccountProvider: String
|
||||
) : OnBoardingEvents
|
||||
|
||||
data object ClearError : OnBoardingEvents
|
||||
}
|
||||
|
|
@ -5,10 +5,11 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.onboarding
|
||||
package io.element.android.features.login.impl.screens.onboarding
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
|
|
@ -16,13 +17,17 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class OnBoardingNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: OnBoardingPresenter,
|
||||
presenterFactory: OnBoardingPresenter.Factory,
|
||||
) : Node(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
|
|
@ -32,8 +37,22 @@ class OnBoardingNode @AssistedInject constructor(
|
|||
fun onSignIn()
|
||||
fun onSignInWithQrCode()
|
||||
fun onReportProblem()
|
||||
fun onLoginPasswordNeeded()
|
||||
fun onOidcDetails(oidcDetails: OidcDetails)
|
||||
fun onCreateAccountContinue(url: String)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val accountProvider: String?,
|
||||
val loginHint: String?,
|
||||
) : NodeInputs
|
||||
|
||||
private val params = inputs<Params>()
|
||||
|
||||
private val presenter = presenterFactory.create(
|
||||
params = params,
|
||||
)
|
||||
|
||||
private fun onSignIn() {
|
||||
plugins<Callback>().forEach { it.onSignIn() }
|
||||
}
|
||||
|
|
@ -50,9 +69,22 @@ class OnBoardingNode @AssistedInject constructor(
|
|||
plugins<Callback>().forEach { it.onReportProblem() }
|
||||
}
|
||||
|
||||
private fun onOidcDetails(data: OidcDetails) {
|
||||
plugins<Callback>().forEach { it.onOidcDetails(data) }
|
||||
}
|
||||
|
||||
private fun onLoginPasswordNeeded() {
|
||||
plugins<Callback>().forEach { it.onLoginPasswordNeeded() }
|
||||
}
|
||||
|
||||
private fun onCreateAccountContinue(url: String) {
|
||||
plugins<Callback>().forEach { it.onCreateAccountContinue(url) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val context = LocalContext.current
|
||||
OnBoardingView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
|
|
@ -60,6 +92,10 @@ class OnBoardingNode @AssistedInject constructor(
|
|||
onCreateAccount = ::onSignUp,
|
||||
onSignInWithQrCode = ::onSignInWithQrCode,
|
||||
onReportProblem = ::onReportProblem,
|
||||
onOidcDetails = ::onOidcDetails,
|
||||
onNeedLoginPassword = ::onLoginPasswordNeeded,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
onCreateAccountContinue = ::onCreateAccountContinue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.onboarding
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.appconfig.OnBoardingConfig
|
||||
import io.element.android.features.login.impl.login.LoginHelper
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
|
||||
class OnBoardingPresenter @AssistedInject constructor(
|
||||
@Assisted private val params: OnBoardingNode.Params,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
private val loginHelper: LoginHelper,
|
||||
) : Presenter<OnBoardingState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
params: OnBoardingNode.Params,
|
||||
): OnBoardingPresenter
|
||||
}
|
||||
|
||||
private val defaultAccountProvider = params.accountProvider
|
||||
private val loginHint = params.loginHint
|
||||
|
||||
@Composable
|
||||
override fun present(): OnBoardingState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val canLoginWithQrCode by produceState(initialValue = false) {
|
||||
value = defaultAccountProvider == null &&
|
||||
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
||||
}
|
||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||
|
||||
val loginMode by loginHelper.collectLoginMode()
|
||||
|
||||
fun handleEvent(event: OnBoardingEvents) {
|
||||
when (event) {
|
||||
is OnBoardingEvents.OnSignIn -> loginHelper.submit(
|
||||
coroutineScope = localCoroutineScope,
|
||||
isAccountCreation = false,
|
||||
homeserverUrl = event.defaultAccountProvider,
|
||||
loginHint = loginHint,
|
||||
)
|
||||
OnBoardingEvents.ClearError -> loginHelper.clearError()
|
||||
}
|
||||
}
|
||||
|
||||
return OnBoardingState(
|
||||
productionApplicationName = buildMeta.productionApplicationName,
|
||||
defaultAccountProvider = defaultAccountProvider,
|
||||
canLoginWithQrCode = canLoginWithQrCode,
|
||||
canCreateAccount = defaultAccountProvider == null && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||
canReportBug = canReportBug,
|
||||
loginMode = loginMode,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.onboarding
|
||||
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class OnBoardingState(
|
||||
val productionApplicationName: String,
|
||||
val defaultAccountProvider: String?,
|
||||
val canLoginWithQrCode: Boolean,
|
||||
val canCreateAccount: Boolean,
|
||||
val canReportBug: Boolean,
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val eventSink: (OnBoardingEvents) -> Unit,
|
||||
) {
|
||||
val submitEnabled: Boolean
|
||||
get() = defaultAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
||||
}
|
||||
|
|
@ -5,9 +5,11 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.onboarding
|
||||
package io.element.android.features.login.impl.screens.onboarding
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
|
||||
override val values: Sequence<OnBoardingState>
|
||||
|
|
@ -17,17 +19,24 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
|
|||
anOnBoardingState(canCreateAccount = true),
|
||||
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true),
|
||||
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true),
|
||||
anOnBoardingState(defaultAccountProvider = "element.io", canCreateAccount = false, canReportBug = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun anOnBoardingState(
|
||||
productionApplicationName: String = "Element",
|
||||
defaultAccountProvider: String? = null,
|
||||
canLoginWithQrCode: Boolean = false,
|
||||
canCreateAccount: Boolean = false,
|
||||
canReportBug: Boolean = false,
|
||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
eventSink: (OnBoardingEvents) -> Unit = {},
|
||||
) = OnBoardingState(
|
||||
productionApplicationName = productionApplicationName,
|
||||
defaultAccountProvider = defaultAccountProvider,
|
||||
canLoginWithQrCode = canLoginWithQrCode,
|
||||
canCreateAccount = canCreateAccount,
|
||||
canReportBug = canReportBug,
|
||||
loginMode = loginMode,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.onboarding
|
||||
package io.element.android.features.login.impl.screens.onboarding
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -16,6 +16,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.BiasAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -27,6 +30,8 @@ import androidx.compose.ui.unit.sp
|
|||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.login.LoginModeView
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
|
|
@ -37,6 +42,7 @@ import io.element.android.libraries.designsystem.theme.components.Button
|
|||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -52,6 +58,10 @@ fun OnBoardingView(
|
|||
onSignInWithQrCode: () -> Unit,
|
||||
onSignIn: () -> Unit,
|
||||
onCreateAccount: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
onReportProblem: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -59,6 +69,16 @@ fun OnBoardingView(
|
|||
modifier = modifier,
|
||||
content = {
|
||||
OnBoardingContent(state = state)
|
||||
LoginModeView(
|
||||
loginMode = state.loginMode,
|
||||
onClearError = {
|
||||
state.eventSink(OnBoardingEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
},
|
||||
footer = {
|
||||
OnBoardingButtons(
|
||||
|
|
@ -127,6 +147,12 @@ private fun OnBoardingButtons(
|
|||
onCreateAccount: () -> Unit,
|
||||
onReportProblem: () -> Unit,
|
||||
) {
|
||||
val isLoading by remember(state.loginMode) {
|
||||
derivedStateOf {
|
||||
state.loginMode is AsyncData.Loading
|
||||
}
|
||||
}
|
||||
|
||||
ButtonColumnMolecule {
|
||||
val signInButtonStringRes = if (state.canLoginWithQrCode || state.canCreateAccount) {
|
||||
R.string.screen_onboarding_sign_in_manually
|
||||
|
|
@ -141,13 +167,27 @@ private fun OnBoardingButtons(
|
|||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Button(
|
||||
text = stringResource(id = signInButtonStringRes),
|
||||
onClick = onSignIn,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.onBoardingSignIn)
|
||||
)
|
||||
val defaultAccountProvider = state.defaultAccountProvider
|
||||
if (defaultAccountProvider == null) {
|
||||
Button(
|
||||
text = stringResource(id = signInButtonStringRes),
|
||||
onClick = onSignIn,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.onBoardingSignIn)
|
||||
)
|
||||
} else {
|
||||
Button(
|
||||
text = stringResource(id = R.string.screen_onboarding_sign_in_to, defaultAccountProvider),
|
||||
showProgress = isLoading,
|
||||
onClick = {
|
||||
state.eventSink(OnBoardingEvents.OnSignIn(defaultAccountProvider))
|
||||
},
|
||||
enabled = state.submitEnabled || isLoading,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
if (state.canCreateAccount) {
|
||||
TextButton(
|
||||
text = stringResource(id = R.string.screen_onboarding_sign_up),
|
||||
|
|
@ -181,5 +221,9 @@ internal fun OnBoardingViewPreview(
|
|||
onSignIn = {},
|
||||
onCreateAccount = {},
|
||||
onReportProblem = {},
|
||||
onOidcDetails = {},
|
||||
onNeedLoginPassword = {},
|
||||
onLearnMoreClick = {},
|
||||
onCreateAccountContinue = {},
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue