Add userConsent UI to the Discovery screen
This commit is contained in:
parent
ccf5d759a4
commit
d1e2d06538
|
@ -92,9 +92,24 @@ interface IdentityService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search MatrixId of users providing email and phone numbers
|
* Search MatrixId of users providing email and phone numbers
|
||||||
|
* Note the the user consent has to be set to true, or it will throw a UserConsentNotProvided failure
|
||||||
|
* Application has to explicitly ask for the user consent.
|
||||||
|
* Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details.
|
||||||
*/
|
*/
|
||||||
fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable
|
fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current user consent
|
||||||
|
*/
|
||||||
|
fun getUserConsent(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the user consent. Application may have explicitly ask for the user consent to send their private data
|
||||||
|
* (email and phone numbers) to the identity server.
|
||||||
|
* Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details.
|
||||||
|
*/
|
||||||
|
fun setUserConsent(newValue: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the status of the current user's threePid
|
* Get the status of the current user's threePid
|
||||||
* A lookup will be performed, but also pending binding state will be restored
|
* A lookup will be performed, but also pending binding state will be restored
|
||||||
|
|
|
@ -24,6 +24,7 @@ sealed class IdentityServiceError : Failure.FeatureFailure() {
|
||||||
object NoIdentityServerConfigured : IdentityServiceError()
|
object NoIdentityServerConfigured : IdentityServiceError()
|
||||||
object TermsNotSignedException : IdentityServiceError()
|
object TermsNotSignedException : IdentityServiceError()
|
||||||
object BulkLookupSha256NotSupported : IdentityServiceError()
|
object BulkLookupSha256NotSupported : IdentityServiceError()
|
||||||
|
object UserConsentNotProvided : IdentityServiceError()
|
||||||
object BindingError : IdentityServiceError()
|
object BindingError : IdentityServiceError()
|
||||||
object NoCurrentBindingError : IdentityServiceError()
|
object NoCurrentBindingError : IdentityServiceError()
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.internal.util.ensureProtocol
|
import org.matrix.android.sdk.internal.util.ensureProtocol
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
@ -243,7 +244,20 @@ internal class DefaultIdentityService @Inject constructor(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getUserConsent(): Boolean {
|
||||||
|
return identityStore.getIdentityData()?.userConsent.orFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserConsent(newValue: Boolean) {
|
||||||
|
identityStore.setUserConsent(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable {
|
override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable {
|
||||||
|
if (!getUserConsent()) {
|
||||||
|
callback.onFailure(IdentityServiceError.UserConsentNotProvided)
|
||||||
|
return NoOpCancellable
|
||||||
|
}
|
||||||
|
|
||||||
if (threePids.isEmpty()) {
|
if (threePids.isEmpty()) {
|
||||||
callback.onSuccess(emptyList())
|
callback.onSuccess(emptyList())
|
||||||
return NoOpCancellable
|
return NoOpCancellable
|
||||||
|
@ -255,6 +269,9 @@ internal class DefaultIdentityService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable {
|
override fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable {
|
||||||
|
// Note: we do not require user consent here, because it is used for email and phone numbers that the user has already sent
|
||||||
|
// to the home server. Identity server is another service though...
|
||||||
|
|
||||||
if (threePids.isEmpty()) {
|
if (threePids.isEmpty()) {
|
||||||
callback.onSuccess(emptyMap())
|
callback.onSuccess(emptyMap())
|
||||||
return NoOpCancellable
|
return NoOpCancellable
|
||||||
|
|
|
@ -52,6 +52,13 @@ internal fun IdentityDataEntity.Companion.setToken(realm: Realm,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun IdentityDataEntity.Companion.setUserConsent(realm: Realm,
|
||||||
|
newConsent: Boolean) {
|
||||||
|
get(realm)?.apply {
|
||||||
|
userConsent = newConsent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm,
|
internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm,
|
||||||
pepper: String,
|
pepper: String,
|
||||||
algorithms: List<String>) {
|
algorithms: List<String>) {
|
||||||
|
|
|
@ -136,6 +136,7 @@ class DefaultErrorFormatter @Inject constructor(
|
||||||
IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported
|
IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported
|
||||||
IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error
|
IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error
|
||||||
IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error
|
IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error
|
||||||
|
IdentityServiceError.UserConsentNotProvided -> R.string.identity_server_user_consent_not_provided
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ sealed class DiscoverySettingsAction : VectorViewModelAction {
|
||||||
|
|
||||||
object DisconnectIdentityServer : DiscoverySettingsAction()
|
object DisconnectIdentityServer : DiscoverySettingsAction()
|
||||||
data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction()
|
data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction()
|
||||||
|
data class UpdateUserConsent(val newConsent: Boolean) : DiscoverySettingsAction()
|
||||||
data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
|
data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
|
||||||
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
|
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
|
||||||
data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction()
|
data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction()
|
||||||
|
|
|
@ -65,6 +65,7 @@ class DiscoverySettingsController @Inject constructor(
|
||||||
buildIdentityServerSection(data)
|
buildIdentityServerSection(data)
|
||||||
val hasIdentityServer = data.identityServer().isNullOrBlank().not()
|
val hasIdentityServer = data.identityServer().isNullOrBlank().not()
|
||||||
if (hasIdentityServer && !data.termsNotSigned) {
|
if (hasIdentityServer && !data.termsNotSigned) {
|
||||||
|
buildConsentSection(data)
|
||||||
buildEmailsSection(data.emailList)
|
buildEmailsSection(data.emailList)
|
||||||
buildMsisdnSection(data.phoneNumbersList)
|
buildMsisdnSection(data.phoneNumbersList)
|
||||||
}
|
}
|
||||||
|
@ -72,6 +73,38 @@ class DiscoverySettingsController @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildConsentSection(data: DiscoverySettingsState) {
|
||||||
|
settingsSectionTitleItem {
|
||||||
|
id("idConsentTitle")
|
||||||
|
titleResId(R.string.settings_discovery_consent_title)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.userConsent) {
|
||||||
|
settingsInfoItem {
|
||||||
|
id("idConsentInfo")
|
||||||
|
helperTextResId(R.string.settings_discovery_consent_notice_on)
|
||||||
|
}
|
||||||
|
settingsButtonItem {
|
||||||
|
id("idConsentButton")
|
||||||
|
colorProvider(colorProvider)
|
||||||
|
buttonTitleId(R.string.settings_discovery_consent_action_revoke)
|
||||||
|
buttonStyle(ButtonStyle.DESTRUCTIVE)
|
||||||
|
buttonClickListener { listener?.onTapUpdateUserConsent(false) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
settingsInfoItem {
|
||||||
|
id("idConsentInfo")
|
||||||
|
helperTextResId(R.string.settings_discovery_consent_notice_off)
|
||||||
|
}
|
||||||
|
settingsButtonItem {
|
||||||
|
id("idConsentButton")
|
||||||
|
colorProvider(colorProvider)
|
||||||
|
buttonTitleId(R.string.settings_discovery_consent_action_give_consent)
|
||||||
|
buttonClickListener { listener?.onTapUpdateUserConsent(true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildIdentityServerSection(data: DiscoverySettingsState) {
|
private fun buildIdentityServerSection(data: DiscoverySettingsState) {
|
||||||
val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none)
|
val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none)
|
||||||
|
|
||||||
|
@ -359,6 +392,7 @@ class DiscoverySettingsController @Inject constructor(
|
||||||
fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String)
|
fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String)
|
||||||
fun onTapChangeIdentityServer()
|
fun onTapChangeIdentityServer()
|
||||||
fun onTapDisconnectIdentityServer()
|
fun onTapDisconnectIdentityServer()
|
||||||
|
fun onTapUpdateUserConsent(newValue: Boolean)
|
||||||
fun onTapRetryToRetrieveBindings()
|
fun onTapRetryToRetrieveBindings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,6 +170,23 @@ class DiscoverySettingsFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTapUpdateUserConsent(newValue: Boolean) {
|
||||||
|
if (newValue) {
|
||||||
|
withState(viewModel) { state ->
|
||||||
|
AlertDialog.Builder(requireActivity())
|
||||||
|
.setTitle(R.string.identity_server_consent_dialog_title)
|
||||||
|
.setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke()))
|
||||||
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
|
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true))
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.no, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTapRetryToRetrieveBindings() {
|
override fun onTapRetryToRetrieveBindings() {
|
||||||
viewModel.handle(DiscoverySettingsAction.RetrieveBinding)
|
viewModel.handle(DiscoverySettingsAction.RetrieveBinding)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,5 +25,6 @@ data class DiscoverySettingsState(
|
||||||
val emailList: Async<List<PidInfo>> = Uninitialized,
|
val emailList: Async<List<PidInfo>> = Uninitialized,
|
||||||
val phoneNumbersList: Async<List<PidInfo>> = Uninitialized,
|
val phoneNumbersList: Async<List<PidInfo>> = Uninitialized,
|
||||||
// Can be true if terms are updated
|
// Can be true if terms are updated
|
||||||
val termsNotSigned: Boolean = false
|
val termsNotSigned: Boolean = false,
|
||||||
|
val userConsent: Boolean = false
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
|
@ -63,7 +63,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||||
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
|
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
|
||||||
val currentIS = state.identityServer()
|
val currentIS = state.identityServer()
|
||||||
setState {
|
setState {
|
||||||
copy(identityServer = Success(identityServerUrl))
|
copy(
|
||||||
|
identityServer = Success(identityServerUrl),
|
||||||
|
userConsent = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (currentIS != identityServerUrl) retrieveBinding()
|
if (currentIS != identityServerUrl) retrieveBinding()
|
||||||
}
|
}
|
||||||
|
@ -71,7 +74,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setState {
|
setState {
|
||||||
copy(identityServer = Success(identityService.getCurrentIdentityServerUrl()))
|
copy(
|
||||||
|
identityServer = Success(identityService.getCurrentIdentityServerUrl()),
|
||||||
|
userConsent = identityService.getUserConsent()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
startListenToIdentityManager()
|
startListenToIdentityManager()
|
||||||
observeThreePids()
|
observeThreePids()
|
||||||
|
@ -97,6 +103,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||||
DiscoverySettingsAction.RetrieveBinding -> retrieveBinding()
|
DiscoverySettingsAction.RetrieveBinding -> retrieveBinding()
|
||||||
DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer()
|
DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer()
|
||||||
is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action)
|
is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action)
|
||||||
|
is DiscoverySettingsAction.UpdateUserConsent -> handleUpdateUserConsent(action)
|
||||||
is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action)
|
is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action)
|
||||||
is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action)
|
is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action)
|
||||||
is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true)
|
is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true)
|
||||||
|
@ -105,13 +112,23 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateUserConsent(action: DiscoverySettingsAction.UpdateUserConsent) {
|
||||||
|
identityService.setUserConsent(action.newConsent)
|
||||||
|
setState { copy(userConsent = action.newConsent) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun disconnectIdentityServer() {
|
private fun disconnectIdentityServer() {
|
||||||
setState { copy(identityServer = Loading()) }
|
setState { copy(identityServer = Loading()) }
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
awaitCallback<Unit> { session.identityService().disconnect(it) }
|
awaitCallback<Unit> { session.identityService().disconnect(it) }
|
||||||
setState { copy(identityServer = Success(null)) }
|
setState {
|
||||||
|
copy(
|
||||||
|
identityServer = Success(null),
|
||||||
|
userConsent = false
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
setState { copy(identityServer = Fail(failure)) }
|
setState { copy(identityServer = Fail(failure)) }
|
||||||
}
|
}
|
||||||
|
@ -126,7 +143,12 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||||
val data = awaitCallback<String?> {
|
val data = awaitCallback<String?> {
|
||||||
session.identityService().setNewIdentityServer(action.url, it)
|
session.identityService().setNewIdentityServer(action.url, it)
|
||||||
}
|
}
|
||||||
setState { copy(identityServer = Success(data)) }
|
setState {
|
||||||
|
copy(
|
||||||
|
identityServer = Success(data),
|
||||||
|
userConsent = false
|
||||||
|
)
|
||||||
|
}
|
||||||
retrieveBinding()
|
retrieveBinding()
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
setState { copy(identityServer = Fail(failure)) }
|
setState { copy(identityServer = Fail(failure)) }
|
||||||
|
|
|
@ -1793,6 +1793,14 @@
|
||||||
<string name="settings_discovery_confirm_mail">We sent you a confirm email to %s, check your email and click on the confirmation link</string>
|
<string name="settings_discovery_confirm_mail">We sent you a confirm email to %s, check your email and click on the confirmation link</string>
|
||||||
<string name="settings_discovery_confirm_mail_not_clicked">We sent you a confirm email to %s, please first check your email and click on the confirmation link</string>
|
<string name="settings_discovery_confirm_mail_not_clicked">We sent you a confirm email to %s, please first check your email and click on the confirmation link</string>
|
||||||
<string name="settings_discovery_mail_pending">Pending</string>
|
<string name="settings_discovery_mail_pending">Pending</string>
|
||||||
|
<string name="settings_discovery_consent_title">Send emails and phone numbers</string>
|
||||||
|
<string name="settings_discovery_consent_notice_on">You have given your consent to send emails and phone numbers to this identity server to discover other users from your contacts.</string>
|
||||||
|
<string name="settings_discovery_consent_notice_off">You have not given your consent to send emails and phone numbers to this identity server to discover other users from your contacts.</string>
|
||||||
|
<string name="settings_discovery_consent_action_revoke">Revoke my consent</string>
|
||||||
|
<string name="settings_discovery_consent_action_give_consent">Give consent</string>
|
||||||
|
|
||||||
|
<string name="identity_server_consent_dialog_title">Send emails and phone numbers</string>
|
||||||
|
<string name="identity_server_consent_dialog_content">In order to discover existing contacts you know, do you accept to send your contact data (phone numbers and/or emails) to the configured Identity Server (%1$s)?\n\nFor more privacy, the sent data will be hashed before being sent.</string>
|
||||||
|
|
||||||
<string name="settings_discovery_enter_identity_server">Enter an identity server URL</string>
|
<string name="settings_discovery_enter_identity_server">Enter an identity server URL</string>
|
||||||
<string name="settings_discovery_bad_identity_server">Could not connect to identity server</string>
|
<string name="settings_discovery_bad_identity_server">Could not connect to identity server</string>
|
||||||
|
@ -2527,6 +2535,7 @@
|
||||||
<string name="identity_server_error_bulk_sha256_not_supported">For your privacy, Element only supports sending hashed user emails and phone number.</string>
|
<string name="identity_server_error_bulk_sha256_not_supported">For your privacy, Element only supports sending hashed user emails and phone number.</string>
|
||||||
<string name="identity_server_error_binding_error">The association has failed.</string>
|
<string name="identity_server_error_binding_error">The association has failed.</string>
|
||||||
<string name="identity_server_error_no_current_binding_error">The is no current association with this identifier.</string>
|
<string name="identity_server_error_no_current_binding_error">The is no current association with this identifier.</string>
|
||||||
|
<string name="identity_server_user_consent_not_provided">The user consent has not been provided.</string>
|
||||||
|
|
||||||
<string name="identity_server_set_default_notice">Your homeserver (%1$s) proposes to use %2$s for your identity server</string>
|
<string name="identity_server_set_default_notice">Your homeserver (%1$s) proposes to use %2$s for your identity server</string>
|
||||||
<string name="identity_server_set_default_submit">Use %1$s</string>
|
<string name="identity_server_set_default_submit">Use %1$s</string>
|
||||||
|
|
Loading…
Reference in New Issue