mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Prompt for local network permission when changing server
This commit is contained in:
+2
@@ -13,4 +13,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
|||||||
sealed interface ChangeServerEvents {
|
sealed interface ChangeServerEvents {
|
||||||
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
|
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
|
||||||
data object ClearError : ChangeServerEvents
|
data object ClearError : ChangeServerEvents
|
||||||
|
data object RequestLocalNetworkPermission : ChangeServerEvents
|
||||||
|
data object DismissLocalNetworkPermission : ChangeServerEvents
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-8
@@ -12,50 +12,55 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
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.error.ChangeServerError
|
||||||
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class ChangeServerPresenter(
|
class ChangeServerPresenter(
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
|
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
|
||||||
|
private val localNetworkPermissionGate: LocalNetworkPermissionGate,
|
||||||
) : Presenter<ChangeServerState> {
|
) : Presenter<ChangeServerState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ChangeServerState {
|
override fun present(): ChangeServerState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val changeServerAction: MutableState<AsyncData<Unit>> = remember {
|
val changeServerAction: MutableState<AsyncData<Unit>> = remember {
|
||||||
mutableStateOf(AsyncData.Uninitialized)
|
mutableStateOf(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val gateState = localNetworkPermissionGate.present<AccountProvider>(
|
||||||
|
urlOf = { provider -> provider.url },
|
||||||
|
onProceed = { provider -> changeServer(provider, changeServerAction) },
|
||||||
|
)
|
||||||
|
|
||||||
fun handleEvent(event: ChangeServerEvents) {
|
fun handleEvent(event: ChangeServerEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction)
|
|
||||||
ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized
|
ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized
|
||||||
|
is ChangeServerEvents.ChangeServer -> gateState.submit(event.accountProvider)
|
||||||
|
ChangeServerEvents.DismissLocalNetworkPermission -> gateState.abort()
|
||||||
|
ChangeServerEvents.RequestLocalNetworkPermission -> gateState.requestPermission()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChangeServerState(
|
return ChangeServerState(
|
||||||
changeServerAction = changeServerAction.value,
|
changeServerAction = changeServerAction.value,
|
||||||
|
localNetworkPermissionDialog = gateState.dialog,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.changeServer(
|
private suspend fun changeServer(
|
||||||
data: AccountProvider,
|
data: AccountProvider,
|
||||||
changeServerAction: MutableState<AsyncData<Unit>>,
|
changeServerAction: MutableState<AsyncData<Unit>>,
|
||||||
) = launch {
|
) {
|
||||||
suspend {
|
suspend {
|
||||||
defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider(
|
defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider(
|
||||||
title = data.title,
|
title = data.title,
|
||||||
|
|||||||
+2
@@ -8,9 +8,11 @@
|
|||||||
|
|
||||||
package io.element.android.features.login.impl.changeserver
|
package io.element.android.features.login.impl.changeserver
|
||||||
|
|
||||||
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
data class ChangeServerState(
|
data class ChangeServerState(
|
||||||
val changeServerAction: AsyncData<Unit>,
|
val changeServerAction: AsyncData<Unit>,
|
||||||
|
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
||||||
val eventSink: (ChangeServerEvents) -> Unit
|
val eventSink: (ChangeServerEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|||||||
+4
@@ -10,6 +10,7 @@ package io.element.android.features.login.impl.changeserver
|
|||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
|
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
|
||||||
@@ -39,12 +40,15 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
|
|||||||
ChangeServerError.UnsupportedServer
|
ChangeServerError.UnsupportedServer
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
aChangeServerState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aChangeServerState(
|
fun aChangeServerState(
|
||||||
changeServerAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
changeServerAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||||
|
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
|
||||||
) = ChangeServerState(
|
) = ChangeServerState(
|
||||||
changeServerAction = changeServerAction,
|
changeServerAction = changeServerAction,
|
||||||
|
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|||||||
+10
@@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
|
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.error.ChangeServerError
|
||||||
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView
|
||||||
import io.element.android.libraries.androidutils.system.openGooglePlay
|
import io.element.android.libraries.androidutils.system.openGooglePlay
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
@@ -121,6 +122,15 @@ fun ChangeServerView(
|
|||||||
}
|
}
|
||||||
AsyncData.Uninitialized -> Unit
|
AsyncData.Uninitialized -> Unit
|
||||||
}
|
}
|
||||||
|
LocalNetworkPermissionDialogView(
|
||||||
|
dialog = state.localNetworkPermissionDialog,
|
||||||
|
onSubmit = {
|
||||||
|
eventSink.invoke(ChangeServerEvents.RequestLocalNetworkPermission)
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
|
|||||||
+80
@@ -15,6 +15,8 @@ import io.element.android.features.login.impl.accesscontrol.DefaultAccountProvid
|
|||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
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.error.ChangeServerError
|
||||||
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
@@ -23,6 +25,9 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
|||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||||
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
@@ -43,6 +48,7 @@ class ChangeServerPresenterTest {
|
|||||||
createPresenter().test {
|
createPresenter().test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
assertThat(initialState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,11 +189,81 @@ class ChangeServerPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - advisor advises prompt, dialog shown before setHomeserver runs`() = runTest {
|
||||||
|
val authenticationService = FakeMatrixAuthenticationService(
|
||||||
|
setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) },
|
||||||
|
)
|
||||||
|
createPresenter(
|
||||||
|
authenticationService = authenticationService,
|
||||||
|
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }),
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
|
||||||
|
permissionsPresenter = FakePermissionsPresenter(),
|
||||||
|
).test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||||
|
val promptState = expectMostRecentItem()
|
||||||
|
// Dialog is shown, permission has not been requested yet, setHomeserver has not been called.
|
||||||
|
assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
assertThat(promptState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - dismissing the dialog aborts the submit`() = runTest {
|
||||||
|
createPresenter(
|
||||||
|
authenticationService = FakeMatrixAuthenticationService(
|
||||||
|
setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) },
|
||||||
|
),
|
||||||
|
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }),
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
|
||||||
|
permissionsPresenter = FakePermissionsPresenter(),
|
||||||
|
).test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||||
|
val promptState = expectMostRecentItem()
|
||||||
|
assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
promptState.eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission)
|
||||||
|
val dismissedState = expectMostRecentItem()
|
||||||
|
assertThat(dismissedState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
assertThat(dismissedState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - advisor advises prompt, permission granted resumes setHomeserver`() = runTest {
|
||||||
|
val authenticationService = FakeMatrixAuthenticationService(
|
||||||
|
setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) },
|
||||||
|
)
|
||||||
|
val permissionsPresenter = FakePermissionsPresenter()
|
||||||
|
createPresenter(
|
||||||
|
authenticationService = authenticationService,
|
||||||
|
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }),
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
|
||||||
|
permissionsPresenter = permissionsPresenter,
|
||||||
|
).test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||||
|
val promptState = expectMostRecentItem()
|
||||||
|
assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
permissionsPresenter.setPermissionGranted()
|
||||||
|
// Await recompositions until the deferred setHomeserver completes.
|
||||||
|
var finalState = awaitItem()
|
||||||
|
while (finalState.changeServerAction !is AsyncData.Success) {
|
||||||
|
finalState = awaitItem()
|
||||||
|
}
|
||||||
|
assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Success(Unit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
||||||
|
localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
permissionsPresenter: FakePermissionsPresenter = FakePermissionsPresenter(),
|
||||||
) = ChangeServerPresenter(
|
) = ChangeServerPresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
@@ -195,5 +271,9 @@ class ChangeServerPresenterTest {
|
|||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
wellknownRetriever = wellknownRetriever,
|
wellknownRetriever = wellknownRetriever,
|
||||||
),
|
),
|
||||||
|
localNetworkPermissionGate = LocalNetworkPermissionGate(
|
||||||
|
advisor = localNetworkPermissionAdvisor,
|
||||||
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user