Iterate on the consent dialog of the identity server - handle the other places.

This commit is contained in:
Benoit Marty 2021-11-30 16:34:16 +01:00
parent 3d5d9ad154
commit b66aff457a
12 changed files with 147 additions and 52 deletions

View File

@ -23,7 +23,7 @@ import android.webkit.WebViewClient
import android.widget.TextView import android.widget.TextView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.discovery.IdentityServerPolicy import im.vector.app.features.discovery.IdentityServerWithTerms
import me.gujun.android.span.link import me.gujun.android.span.link
import me.gujun.android.span.span import me.gujun.android.span.span
@ -45,19 +45,18 @@ fun Context.displayInWebView(url: String) {
.show() .show()
} }
fun Context.showIdentityServerConsentDialog(configuredIdentityServer: String?, fun Context.showIdentityServerConsentDialog(identityServerWithTerms: IdentityServerWithTerms?,
policies: List<IdentityServerPolicy>?,
consentCallBack: (() -> Unit)) { consentCallBack: (() -> Unit)) {
// Build the message // Build the message
val content = span { val content = span {
+getString(R.string.identity_server_consent_dialog_content_3) +getString(R.string.identity_server_consent_dialog_content_3)
+"\n\n" +"\n\n"
if (!policies.isNullOrEmpty()) { if (identityServerWithTerms?.policies?.isNullOrEmpty() == false) {
span { span {
textStyle = "bold" textStyle = "bold"
text = getString(R.string.settings_privacy_policy) text = getString(R.string.settings_privacy_policy)
} }
policies.forEach { identityServerWithTerms.policies.forEach {
+"\n" +"\n"
// Use the url as the text too // Use the url as the text too
link(it.url, it.url) link(it.url, it.url)
@ -67,7 +66,7 @@ fun Context.showIdentityServerConsentDialog(configuredIdentityServer: String?,
+getString(R.string.identity_server_consent_dialog_content_question) +getString(R.string.identity_server_consent_dialog_content_question)
} }
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.identity_server_consent_dialog_title_2, configuredIdentityServer.orEmpty())) .setTitle(getString(R.string.identity_server_consent_dialog_title_2, identityServerWithTerms?.serverUrl.orEmpty()))
.setMessage(content) .setMessage(content)
.setPositiveButton(R.string.reactions_agree) { _, _ -> .setPositiveButton(R.string.reactions_agree) { _, _ ->
consentCallBack.invoke() consentCallBack.invoke()

View File

@ -21,5 +21,6 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class ContactsBookAction : VectorViewModelAction { sealed class ContactsBookAction : VectorViewModelAction {
data class FilterWith(val filter: String) : ContactsBookAction() data class FilterWith(val filter: String) : ContactsBookAction()
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction() data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
object UserConsentRequest : ContactsBookAction()
object UserConsentGranted : ContactsBookAction() object UserConsentGranted : ContactsBookAction()
} }

View File

@ -26,6 +26,7 @@ import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.showIdentityServerConsentDialog import im.vector.app.core.utils.showIdentityServerConsentDialog
@ -67,20 +68,27 @@ class ContactsBookFragment @Inject constructor(
setupConsentView() setupConsentView()
setupOnlyBoundContactsView() setupOnlyBoundContactsView()
setupCloseView() setupCloseView()
contactsBookViewModel.observeViewEvents {
when (it) {
is ContactsBookViewEvents.Failure -> showFailure(it.throwable)
is ContactsBookViewEvents.OnPoliciesRetrieved -> showConsentDialog(it)
}.exhaustive
}
} }
private fun setupConsentView() { private fun setupConsentView() {
views.phoneBookSearchForMatrixContacts.setOnClickListener { views.phoneBookSearchForMatrixContacts.setOnClickListener {
withState(contactsBookViewModel) { state -> contactsBookViewModel.handle(ContactsBookAction.UserConsentRequest)
requireContext().showIdentityServerConsentDialog(
state.identityServerUrl,
/* TODO */ emptyList(),
consentCallBack = { contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) }
)
}
} }
} }
private fun showConsentDialog(event: ContactsBookViewEvents.OnPoliciesRetrieved) {
requireContext().showIdentityServerConsentDialog(
event.identityServerWithTerms,
consentCallBack = { contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) }
)
}
private fun setupOnlyBoundContactsView() { private fun setupOnlyBoundContactsView() {
views.phoneBookOnlyBoundContacts.checkedChanges() views.phoneBookOnlyBoundContacts.checkedChanges()
.onEach { .onEach {

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2021 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 im.vector.app.features.contactsbook
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.features.discovery.IdentityServerWithTerms
sealed class ContactsBookViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : ContactsBookViewEvents()
data class OnPoliciesRetrieved(val identityServerWithTerms: IdentityServerWithTerms?) : ContactsBookViewEvents()
}

View File

@ -16,20 +16,21 @@
package im.vector.app.features.contactsbook package im.vector.app.features.contactsbook
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.contacts.ContactsDataSource import im.vector.app.core.contacts.ContactsDataSource
import im.vector.app.core.contacts.MappedContact import im.vector.app.core.contacts.MappedContact
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.discovery.fetchIdentityServerWithTerms
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -37,11 +38,12 @@ import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import timber.log.Timber import timber.log.Timber
class ContactsBookViewModel @AssistedInject constructor(@Assisted class ContactsBookViewModel @AssistedInject constructor(
initialState: ContactsBookViewState, @Assisted initialState: ContactsBookViewState,
private val contactsDataSource: ContactsDataSource, private val contactsDataSource: ContactsDataSource,
private val session: Session) : private val stringProvider: StringProvider,
VectorViewModel<ContactsBookViewState, ContactsBookAction, EmptyViewEvents>(initialState) { private val session: Session
) : VectorViewModel<ContactsBookViewState, ContactsBookAction, ContactsBookViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<ContactsBookViewModel, ContactsBookViewState> { interface Factory : MavericksAssistedViewModelFactory<ContactsBookViewModel, ContactsBookViewState> {
@ -162,9 +164,22 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
is ContactsBookAction.FilterWith -> handleFilterWith(action) is ContactsBookAction.FilterWith -> handleFilterWith(action)
is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action) is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
ContactsBookAction.UserConsentGranted -> handleUserConsentGranted() ContactsBookAction.UserConsentGranted -> handleUserConsentGranted()
ContactsBookAction.UserConsentRequest -> handleUserConsentRequest()
}.exhaustive }.exhaustive
} }
private fun handleUserConsentRequest() {
viewModelScope.launch {
val event = try {
val result = session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language))
ContactsBookViewEvents.OnPoliciesRetrieved(result)
} catch (throwable: Throwable) {
ContactsBookViewEvents.Failure(throwable)
}
_viewEvents.post(event)
}
}
private fun handleUserConsentGranted() { private fun handleUserConsentGranted() {
session.identityService().setUserConsent(true) session.identityService().setUserConsent(true)

View File

@ -186,8 +186,7 @@ class DiscoverySettingsFragment @Inject constructor(
if (newValue) { if (newValue) {
withState(viewModel) { state -> withState(viewModel) { state ->
requireContext().showIdentityServerConsentDialog( requireContext().showIdentityServerConsentDialog(
state.identityServer.invoke()?.serverUrl, state.identityServer.invoke(),
state.identityServer.invoke()?.policies,
consentCallBack = { viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) } consentCallBack = { viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) }
) )
} }

View File

@ -30,7 +30,6 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.ensureProtocol
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -39,7 +38,6 @@ import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.identity.IdentityServiceListener
import org.matrix.android.sdk.api.session.identity.SharedState import org.matrix.android.sdk.api.session.identity.SharedState
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.flow
class DiscoverySettingsViewModel @AssistedInject constructor( class DiscoverySettingsViewModel @AssistedInject constructor(
@ -56,7 +54,6 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<DiscoverySettingsViewModel, DiscoverySettingsState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<DiscoverySettingsViewModel, DiscoverySettingsState> by hiltMavericksViewModelFactory()
private val identityService = session.identityService() private val identityService = session.identityService()
private val termsService: TermsService = session
private val identityServerManagerListener = object : IdentityServiceListener { private val identityServerManagerListener = object : IdentityServiceListener {
override fun onIdentityServerChange() = withState { state -> override fun onIdentityServerChange() = withState { state ->
@ -397,7 +394,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
} }
} }
viewModelScope.launch { viewModelScope.launch {
runCatching { fetchIdentityServerWithTerms() }.fold( runCatching { session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language)) }.fold(
onSuccess = { setState { copy(identityServer = Success(it)) } }, onSuccess = { setState { copy(identityServer = Success(it)) } },
onFailure = { _viewEvents.post(DiscoverySettingsViewEvents.Failure(it)) } onFailure = { _viewEvents.post(DiscoverySettingsViewEvents.Failure(it)) }
) )
@ -405,21 +402,6 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
} }
private suspend fun fetchIdentityServerWithTerms(): IdentityServerWithTerms? { private suspend fun fetchIdentityServerWithTerms(): IdentityServerWithTerms? {
val identityServerUrl = identityService.getCurrentIdentityServerUrl() return session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language))
return identityServerUrl?.let {
val terms = termsService.getTerms(TermsService.ServiceType.IdentityService, identityServerUrl.ensureProtocol())
.serverResponse
.getLocalizedTerms(stringProvider.getString(R.string.resources_language))
val policyUrls = terms.mapNotNull {
val name = it.localizedName ?: it.policyName
val url = it.localizedUrl
if (name == null || url == null) {
null
} else {
IdentityServerPolicy(name = name, url = url)
}
}
IdentityServerWithTerms(identityServerUrl, policyUrls)
}
} }
} }

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2021 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 im.vector.app.features.discovery
import im.vector.app.core.utils.ensureProtocol
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.terms.TermsService
suspend fun Session.fetchIdentityServerWithTerms(userLanguage: String): IdentityServerWithTerms? {
val identityServerUrl = identityService().getCurrentIdentityServerUrl()
return identityServerUrl?.let {
val terms = getTerms(TermsService.ServiceType.IdentityService, identityServerUrl.ensureProtocol())
.serverResponse
.getLocalizedTerms(userLanguage)
val policyUrls = terms.mapNotNull {
val name = it.localizedName ?: it.policyName
val url = it.localizedUrl
if (name == null || url == null) {
null
} else {
IdentityServerPolicy(name = name, url = url)
}
}
IdentityServerWithTerms(identityServerUrl, policyUrls)
}
}

View File

@ -24,6 +24,7 @@ sealed class UserListAction : VectorViewModelAction {
data class AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction() data class AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction()
data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction() data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction()
object ComputeMatrixToLinkForSharing : UserListAction() object ComputeMatrixToLinkForSharing : UserListAction()
object UserConsentRequest : UserListAction()
data class UpdateUserConsent(val consent: Boolean) : UserListAction() data class UpdateUserConsent(val consent: Boolean) : UserListAction()
object Resumed : UserListAction() object Resumed : UserListAction()
} }

View File

@ -102,6 +102,8 @@ class UserListFragment @Inject constructor(
extraTitle = getString(R.string.invite_friends_rich_title) extraTitle = getString(R.string.invite_friends_rich_title)
) )
} }
is UserListViewEvents.Failure -> showFailure(it.throwable)
is UserListViewEvents.OnPoliciesRetrieved -> showConsentDialog(it)
} }
} }
} }
@ -230,13 +232,14 @@ class UserListFragment @Inject constructor(
} }
override fun giveIdentityServerConsent() { override fun giveIdentityServerConsent() {
withState(viewModel) { state -> viewModel.handle(UserListAction.UserConsentRequest)
requireContext().showIdentityServerConsentDialog( }
state.configuredIdentityServer,
/* TODO */ emptyList(), private fun showConsentDialog(event: UserListViewEvents.OnPoliciesRetrieved) {
consentCallBack = { viewModel.handle(UserListAction.UpdateUserConsent(true)) } requireContext().showIdentityServerConsentDialog(
) event.identityServerWithTerms,
} consentCallBack = { viewModel.handle(UserListAction.UpdateUserConsent(true)) }
)
} }
override fun onUseQRCode() { override fun onUseQRCode() {

View File

@ -17,10 +17,13 @@
package im.vector.app.features.userdirectory package im.vector.app.features.userdirectory
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.features.discovery.IdentityServerWithTerms
/** /**
* Transient events for invite users to room screen * Transient events for invite users to room screen
*/ */
sealed class UserListViewEvents : VectorViewEvents { sealed class UserListViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : UserListViewEvents()
data class OnPoliciesRetrieved(val identityServerWithTerms: IdentityServerWithTerms?) : UserListViewEvents()
data class OpenShareMatrixToLink(val link: String) : UserListViewEvents() data class OpenShareMatrixToLink(val link: String) : UserListViewEvents()
} }

View File

@ -23,12 +23,15 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.toggle import im.vector.app.core.extensions.toggle
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.discovery.fetchIdentityServerWithTerms
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
@ -36,6 +39,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -51,9 +55,11 @@ data class ThreePidUser(
val user: User? val user: User?
) )
class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState, class UserListViewModel @AssistedInject constructor(
private val session: Session) : @Assisted initialState: UserListViewState,
VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) { private val stringProvider: StringProvider,
private val session: Session
) : VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {
private val knownUsersSearch = MutableStateFlow("") private val knownUsersSearch = MutableStateFlow("")
private val directoryUsersSearch = MutableStateFlow("") private val directoryUsersSearch = MutableStateFlow("")
@ -104,11 +110,24 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
is UserListAction.AddPendingSelection -> handleSelectUser(action) is UserListAction.AddPendingSelection -> handleSelectUser(action)
is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action) is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action)
UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink() UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
UserListAction.UserConsentRequest -> handleUserConsentRequest()
is UserListAction.UpdateUserConsent -> handleISUpdateConsent(action) is UserListAction.UpdateUserConsent -> handleISUpdateConsent(action)
UserListAction.Resumed -> handleResumed() UserListAction.Resumed -> handleResumed()
}.exhaustive }.exhaustive
} }
private fun handleUserConsentRequest() {
viewModelScope.launch {
val event = try {
val result = session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language))
UserListViewEvents.OnPoliciesRetrieved(result)
} catch (throwable: Throwable) {
UserListViewEvents.Failure(throwable)
}
_viewEvents.post(event)
}
}
private fun handleISUpdateConsent(action: UserListAction.UpdateUserConsent) { private fun handleISUpdateConsent(action: UserListAction.UpdateUserConsent) {
session.identityService().setUserConsent(action.consent) session.identityService().setUserConsent(action.consent)
withState { withState {