Validate server on user click.

This commit is contained in:
Benoit Marty 2023-06-08 15:52:45 +02:00 committed by Benoit Marty
parent 48a95c6e01
commit 9a07b72a0e
21 changed files with 353 additions and 51 deletions

View file

@ -33,7 +33,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.accountprovider.AccountProviderNode import io.element.android.features.login.impl.accountprovider.AccountProviderNode
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.changeaccountprovider.ChangeAccountProviderNode
import io.element.android.features.login.impl.changeaccountprovider.form.ChangeAccountProviderFormNode import io.element.android.features.login.impl.changeaccountprovider.form.ChangeAccountProviderFormNode
import io.element.android.features.login.impl.datasource.AccountProviderDataSource import io.element.android.features.login.impl.datasource.AccountProviderDataSource
@ -125,8 +124,7 @@ class LoginFlowNode @AssistedInject constructor(
} }
NavTarget.ChangeAccountProvider -> { NavTarget.ChangeAccountProvider -> {
val callback = object : ChangeAccountProviderNode.Callback { val callback = object : ChangeAccountProviderNode.Callback {
override fun onAccountProviderClicked(data: AccountProvider) { override fun onDone() {
accountProviderDataSource.userSelection(data)
// Go back to the Account Provider screen // Go back to the Account Provider screen
backstack.singleTop(NavTarget.AccountProvider) backstack.singleTop(NavTarget.AccountProvider)
} }
@ -140,8 +138,7 @@ class LoginFlowNode @AssistedInject constructor(
} }
NavTarget.ChangeAccountProviderForm -> { NavTarget.ChangeAccountProviderForm -> {
val callback = object : ChangeAccountProviderFormNode.Callback { val callback = object : ChangeAccountProviderFormNode.Callback {
override fun onAccountProviderClicked(data: AccountProvider) { override fun onDone() {
accountProviderDataSource.userSelection(data)
// Go back to the Account Provider screen // Go back to the Account Provider screen
backstack.singleTop(NavTarget.AccountProvider) backstack.singleTop(NavTarget.AccountProvider)
} }

View file

@ -16,9 +16,6 @@
package io.element.android.features.login.impl.accountprovider package io.element.android.features.login.impl.accountprovider
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -29,10 +26,9 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.util.LoginConstants import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
@ -72,11 +68,6 @@ class AccountProviderNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onChangeAccountProvider() } plugins<Callback>().forEach { it.onChangeAccountProvider() }
} }
private fun openLearnMorePage(context: Context) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL))
tryOrNull { context.startActivity(intent) }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()

View file

@ -60,8 +60,6 @@ fun AccountProviderView(
} }
} }
val eventSink = state.eventSink val eventSink = state.eventSink
val invalidHomeserverError = (state.loginFlow as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage
val slidingSyncNotSupportedError = (state.loginFlow as? Async.Failure)?.error as? ChangeServerError.SlidingSyncAlert
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier,
@ -112,7 +110,26 @@ fun AccountProviderView(
} }
) { ) {
when (state.loginFlow) { when (state.loginFlow) {
is Async.Failure -> Unit // Error dialog will be displayed is Async.Failure -> {
when (val error = state.loginFlow.error) {
is ChangeServerError.InlineErrorMessage -> {
ErrorDialog(
content = error.message(),
onDismiss = {
eventSink.invoke(AccountProviderEvents.ClearError)
}
)
}
is ChangeServerError.SlidingSyncAlert -> {
SlidingSyncNotSupportedDialog(onLearnMoreClicked = {
onLearnMoreClicked()
eventSink(AccountProviderEvents.ClearError)
}, onDismiss = {
eventSink(AccountProviderEvents.ClearError)
})
}
}
}
is Async.Loading -> Unit // The Continue button shows the loading state is Async.Loading -> Unit // The Continue button shows the loading state
is Async.Success -> { is Async.Success -> {
when (val loginFlowState = state.loginFlow.state) { when (val loginFlowState = state.loginFlow.state) {
@ -122,22 +139,6 @@ fun AccountProviderView(
} }
Async.Uninitialized -> Unit Async.Uninitialized -> Unit
} }
if (slidingSyncNotSupportedError != null) {
SlidingSyncNotSupportedDialog(onLearnMoreClicked = {
onLearnMoreClicked()
eventSink(AccountProviderEvents.ClearError)
}, onDismiss = {
eventSink(AccountProviderEvents.ClearError)
})
}
if (invalidHomeserverError != null) {
ErrorDialog(
content = invalidHomeserverError.message(),
onDismiss = {
eventSink.invoke(AccountProviderEvents.ClearError)
}
)
}
} }
} }

View file

@ -17,14 +17,20 @@
package io.element.android.features.login.impl.accountprovider package io.element.android.features.login.impl.accountprovider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.R as StringR import io.element.android.libraries.ui.strings.R as StringR
@Composable @Composable
internal fun SlidingSyncNotSupportedDialog(onLearnMoreClicked: () -> Unit, onDismiss: () -> Unit) { internal fun SlidingSyncNotSupportedDialog(
onLearnMoreClicked: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
ConfirmationDialog( ConfirmationDialog(
modifier = modifier,
onDismiss = onDismiss, onDismiss = onDismiss,
submitText = stringResource(StringR.string.action_learn_more), submitText = stringResource(StringR.string.action_learn_more),
onSubmitClicked = onLearnMoreClicked, onSubmitClicked = onLearnMoreClicked,

View file

@ -18,6 +18,7 @@ package io.element.android.features.login.impl.changeaccountprovider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@ -25,7 +26,7 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@ -36,12 +37,12 @@ class ChangeAccountProviderNode @AssistedInject constructor(
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onAccountProviderClicked(data: AccountProvider) fun onDone()
fun onOtherClicked() fun onOtherClicked()
} }
private fun onAccountProviderClicked(data: AccountProvider) { private fun onDone() {
plugins<Callback>().forEach { it.onAccountProviderClicked(data) } plugins<Callback>().forEach { it.onDone() }
} }
private fun onOtherClicked() { private fun onOtherClicked() {
@ -51,11 +52,13 @@ class ChangeAccountProviderNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
val context = LocalContext.current
ChangeAccountProviderView( ChangeAccountProviderView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackPressed = ::navigateUp, onBackPressed = ::navigateUp,
onAccountProviderClicked = ::onAccountProviderClicked, onLearnMoreClicked = { openLearnMorePage(context) },
onDone = ::onDone,
onOtherProviderClicked = ::onOtherClicked, onOtherProviderClicked = ::onOtherClicked,
) )
} }

View file

@ -18,14 +18,17 @@ package io.element.android.features.login.impl.changeaccountprovider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject import javax.inject.Inject
class ChangeAccountProviderPresenter @Inject constructor( class ChangeAccountProviderPresenter @Inject constructor(
private val changeServerPresenter: ChangeServerPresenter,
) : Presenter<ChangeAccountProviderState> { ) : Presenter<ChangeAccountProviderState> {
@Composable @Composable
override fun present(): ChangeAccountProviderState { override fun present(): ChangeAccountProviderState {
val changeServerState = changeServerPresenter.present()
return ChangeAccountProviderState( return ChangeAccountProviderState(
// Just matrix.org by default for now // Just matrix.org by default for now
accountProviders = listOf( accountProviders = listOf(
@ -38,6 +41,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
supportSlidingSync = true, supportSlidingSync = true,
) )
), ),
changeServerState = changeServerState,
) )
} }
} }

View file

@ -17,8 +17,10 @@
package io.element.android.features.login.impl.changeaccountprovider package io.element.android.features.login.impl.changeaccountprovider
import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerState
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderState constructor( data class ChangeAccountProviderState constructor(
val accountProviders: List<AccountProvider>, val accountProviders: List<AccountProvider>,
val changeServerState: ChangeServerState,
) )

View file

@ -18,6 +18,7 @@ package io.element.android.features.login.impl.changeaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.item.anAccountProvider import io.element.android.features.login.impl.accountprovider.item.anAccountProvider
import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> { open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
override val values: Sequence<ChangeAccountProviderState> override val values: Sequence<ChangeAccountProviderState>
@ -31,4 +32,5 @@ fun aChangeAccountProviderState() = ChangeAccountProviderState(
accountProviders = listOf( accountProviders = listOf(
anAccountProvider() anAccountProvider()
), ),
changeServerState = aChangeServerState(),
) )

View file

@ -42,6 +42,8 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.accountprovider.item.AccountProviderView import io.element.android.features.login.impl.accountprovider.item.AccountProviderView
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerView
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -57,7 +59,8 @@ fun ChangeAccountProviderView(
state: ChangeAccountProviderState, state: ChangeAccountProviderState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {}, onBackPressed: () -> Unit = {},
onAccountProviderClicked: (AccountProvider) -> Unit = {}, onLearnMoreClicked: () -> Unit = {},
onDone: () -> Unit = {},
onOtherProviderClicked: () -> Unit = {}, onOtherProviderClicked: () -> Unit = {},
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -104,7 +107,7 @@ fun ChangeAccountProviderView(
AccountProviderView( AccountProviderView(
item = alteredItem, item = alteredItem,
onClick = { onClick = {
onAccountProviderClicked(alteredItem) state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(alteredItem))
} }
) )
} }
@ -117,6 +120,11 @@ fun ChangeAccountProviderView(
) )
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
} }
ChangeServerView(
state = state.changeServerState,
onLearnMoreClicked = onLearnMoreClicked,
onDone = onDone,
)
} }
} }
} }

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
sealed interface ChangeServerEvents {
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
object ClearError : ChangeServerEvents
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.net.URL
import javax.inject.Inject
class ChangeServerPresenter @Inject constructor(
private val authenticationService: MatrixAuthenticationService,
private val accountProviderDataSource: AccountProviderDataSource,
) : Presenter<ChangeServerState> {
@Composable
override fun present(): ChangeServerState {
val localCoroutineScope = rememberCoroutineScope()
val changeServerAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized)
}
fun handleEvents(event: ChangeServerEvents) {
when (event) {
is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction)
ChangeServerEvents.ClearError -> changeServerAction.value = Async.Uninitialized
}
}
return ChangeServerState(
changeServerAction = changeServerAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.changeServer(
data: AccountProvider,
changeServerAction: MutableState<Async<Unit>>,
) = launch {
suspend {
val domain = tryOrNull { URL(data.title) }?.host ?: data.title
authenticationService.setHomeserver(domain).map {
authenticationService.getHomeserverDetails().value!!
// Valid, remember user choice
accountProviderDataSource.userSelection(data)
}.getOrThrow()
}.execute(changeServerAction, errorMapping = ChangeServerError::from)
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
import io.element.android.libraries.architecture.Async
data class ChangeServerState(
val changeServerAction: Async<Unit>,
val eventSink: (ChangeServerEvents) -> Unit
)

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
override val values: Sequence<ChangeServerState>
get() = sequenceOf(
aChangeServerState(),
)
}
fun aChangeServerState() = ChangeServerState(
changeServerAction = Async.Uninitialized,
eventSink = {}
)

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.login.impl.accountprovider.SlidingSyncNotSupportedDialog
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun ChangeServerView(
state: ChangeServerState,
modifier: Modifier = Modifier,
onLearnMoreClicked: () -> Unit = {},
onDone: () -> Unit = {},
) {
val eventSink = state.eventSink
when (state.changeServerAction) {
is Async.Failure -> {
when (val error = state.changeServerAction.error) {
is ChangeServerError.InlineErrorMessage -> {
ErrorDialog(
modifier = modifier,
content = error.message(),
onDismiss = {
eventSink.invoke(ChangeServerEvents.ClearError)
}
)
}
is ChangeServerError.SlidingSyncAlert -> {
SlidingSyncNotSupportedDialog(
modifier = modifier,
onLearnMoreClicked = {
onLearnMoreClicked()
eventSink.invoke(ChangeServerEvents.ClearError)
}, onDismiss = {
eventSink.invoke(ChangeServerEvents.ClearError)
})
}
}
}
is Async.Loading -> ProgressDialog()
is Async.Success -> onDone()
Async.Uninitialized -> Unit
}
}
@Preview
@Composable
fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: ChangeServerState) {
ChangeServerView(
state = state,
)
}

View file

@ -18,6 +18,7 @@ package io.element.android.features.login.impl.changeaccountprovider.form
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@ -25,7 +26,7 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@ -36,21 +37,23 @@ class ChangeAccountProviderFormNode @AssistedInject constructor(
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onAccountProviderClicked(data: AccountProvider) fun onDone()
} }
private fun onAccountProviderClicked(data: AccountProvider) { private fun onDone() {
plugins<Callback>().forEach { it.onAccountProviderClicked(data) } plugins<Callback>().forEach { it.onDone() }
} }
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
val context = LocalContext.current
ChangeAccountProviderFormView( ChangeAccountProviderFormView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackPressed = ::navigateUp, onBackPressed = ::navigateUp,
onAccountProviderClicked = ::onAccountProviderClicked onLearnMoreClicked = { openLearnMorePage(context) },
onDone = ::onDone,
) )
} }
} }

View file

@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -29,6 +30,7 @@ import javax.inject.Inject
class ChangeAccountProviderFormPresenter @Inject constructor( class ChangeAccountProviderFormPresenter @Inject constructor(
private val homeserverResolver: HomeserverResolver, private val homeserverResolver: HomeserverResolver,
private val changeServerPresenter: ChangeServerPresenter,
) : Presenter<ChangeAccountProviderFormState> { ) : Presenter<ChangeAccountProviderFormState> {
@Composable @Composable
@ -38,6 +40,7 @@ class ChangeAccountProviderFormPresenter @Inject constructor(
val userInput = rememberSaveable { val userInput = rememberSaveable {
mutableStateOf("") mutableStateOf("")
} }
val changeServerState = changeServerPresenter.present()
val data by homeserverResolver.flow().collectAsState() val data by homeserverResolver.flow().collectAsState()
fun handleEvents(event: ChangeAccountProviderFormEvents) { fun handleEvents(event: ChangeAccountProviderFormEvents) {
@ -52,6 +55,7 @@ class ChangeAccountProviderFormPresenter @Inject constructor(
return ChangeAccountProviderFormState( return ChangeAccountProviderFormState(
userInput = userInput.value, userInput = userInput.value,
userInputResult = data, userInputResult = data,
changeServerState = changeServerState,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }

View file

@ -16,11 +16,13 @@
package io.element.android.features.login.impl.changeaccountprovider.form package io.element.android.features.login.impl.changeaccountprovider.form
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderFormState( data class ChangeAccountProviderFormState(
val userInput: String, val userInput: String,
val userInputResult: Async<List<HomeserverData>>, val userInputResult: Async<List<HomeserverData>>,
val changeServerState: ChangeServerState,
val eventSink: (ChangeAccountProviderFormEvents) -> Unit val eventSink: (ChangeAccountProviderFormEvents) -> Unit
) )

View file

@ -17,6 +17,7 @@
package io.element.android.features.login.impl.changeaccountprovider.form package io.element.android.features.login.impl.changeaccountprovider.form
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
open class ChangeAccountProviderFormStateProvider : PreviewParameterProvider<ChangeAccountProviderFormState> { open class ChangeAccountProviderFormStateProvider : PreviewParameterProvider<ChangeAccountProviderFormState> {
@ -34,6 +35,7 @@ fun aChangeAccountProviderFormState(
) = ChangeAccountProviderFormState( ) = ChangeAccountProviderFormState(
userInput = userInput, userInput = userInput,
userInputResult = userInputResult, userInputResult = userInputResult,
changeServerState = aChangeServerState(),
eventSink = {} eventSink = {}
) )

View file

@ -52,6 +52,8 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.accountprovider.item.AccountProviderView import io.element.android.features.login.impl.accountprovider.item.AccountProviderView
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerView
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
@ -72,7 +74,8 @@ fun ChangeAccountProviderFormView(
state: ChangeAccountProviderFormState, state: ChangeAccountProviderFormState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {}, onBackPressed: () -> Unit = {},
onAccountProviderClicked: (AccountProvider) -> Unit = {}, onLearnMoreClicked: () -> Unit = {},
onDone: () -> Unit = {},
) { ) {
val eventSink = state.eventSink val eventSink = state.eventSink
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -164,7 +167,7 @@ fun ChangeAccountProviderFormView(
AccountProviderView( AccountProviderView(
item = item, item = item,
onClick = { onClick = {
onAccountProviderClicked(item) state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(item))
} }
) )
} }
@ -173,6 +176,11 @@ fun ChangeAccountProviderFormView(
} }
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
} }
ChangeServerView(
state = state.changeServerState,
onLearnMoreClicked = onLearnMoreClicked,
onDone = onDone,
)
} }
} }
} }

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.impl.util
import android.content.Context
import android.content.Intent
import android.net.Uri
import io.element.android.libraries.core.data.tryOrNull
fun openLearnMorePage(context: Context) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL))
tryOrNull { context.startActivity(intent) }
}

View file

@ -24,12 +24,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.R as StringR
@Composable @Composable
fun AsyncFailure( fun AsyncFailure(
@ -43,11 +45,11 @@ fun AsyncFailure(
.padding(vertical = 32.dp), .padding(vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text(text = throwable.message ?: "An error occurred") Text(text = throwable.message ?: stringResource(id = StringR.string.error_unknown))
if (onRetry != null) { if (onRetry != null) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onRetry) { Button(onClick = onRetry) {
Text(text = "Retry") Text(text = stringResource(id = StringR.string.action_retry))
} }
} }
} }