Identity: bind/unbnd WIP

This commit is contained in:
Benoit Marty 2020-05-09 03:42:58 +02:00
parent 637f4a8350
commit 3e808dec90
30 changed files with 807 additions and 230 deletions

View file

@ -39,12 +39,30 @@ interface IdentityService {
*/
fun setNewIdentityServer(url: String?, callback: MatrixCallback<String?>): Cancelable
fun startBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback<ThreePid>)
fun finalizeBindSessionFor3PID(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>)
fun submitValidationToken(pid: ThreePid, code: String, matrixCallback: MatrixCallback<Unit>)
/**
* This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid,
* and then the threePid will be associated with the matrix account
*/
fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
fun startUnBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback<Pair<Boolean, ThreePid?>>)
/**
* This will perform the actual association of ThreePid and Matrix account
*/
fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* @param code the code sent to the user phone number
*/
fun submitValidationToken(pid: ThreePid, code: String, callback: MatrixCallback<Unit>): Cancelable
/**
* The request will actually be done on the homeserver
*/
fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* Search MatrixId of users providing email and phone numbers
*/
fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable
fun addListener(listener: IdentityServiceListener)

View file

@ -20,4 +20,6 @@ sealed class IdentityServiceError(cause: Throwable? = null) : Throwable(cause =
object NoIdentityServerConfigured : IdentityServiceError(null)
object TermsNotSignedException : IdentityServiceError(null)
object BulkLookupSha256NotSupported : IdentityServiceError(null)
object BindingError : IdentityServiceError(null)
object NoCurrentBindingError : IdentityServiceError(null)
}

View file

@ -16,7 +16,16 @@
package im.vector.matrix.android.api.session.identity
import im.vector.matrix.android.internal.session.profile.ThirdPartyIdentifier
sealed class ThreePid(open val value: String) {
data class Email(val email: String) : ThreePid(email)
data class Msisdn(val msisdn: String, val countryCode: String? = null) : ThreePid(msisdn)
}
internal fun ThreePid.toMedium(): String {
return when (this) {
is ThreePid.Email -> ThirdPartyIdentifier.MEDIUM_EMAIL
is ThreePid.Msisdn -> ThirdPartyIdentifier.MEDIUM_MSISDN
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2020 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.matrix.android.api.session.identity
data class ThreePidStatus(
val threePid: ThreePid,
val shareState: SharedState
)
enum class SharedState {
SHARED,
NOT_SHARED,
BINDING_IN_PROGRESS
}

View file

@ -175,23 +175,6 @@ internal abstract class SessionModule {
.build()
}
@JvmStatic
@Provides
@IdentityDatabase
@SessionScope
fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils,
@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String): RealmConfiguration {
return RealmConfiguration.Builder()
.directory(directory)
.name("matrix-sdk-identity.realm")
.apply {
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
}
.modules(IdentityRealmModule())
.build()
}
@JvmStatic
@Provides
@SessionScope

View file

@ -21,14 +21,15 @@ import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.session.identity.FoundThreePid
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.identity.toMedium
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url
import im.vector.matrix.android.internal.crypto.tools.withOlmUtility
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStore
import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse
import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Params
import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Response
import im.vector.matrix.android.internal.session.profile.ThirdPartyIdentifier
import im.vector.matrix.android.internal.task.Task
import java.util.Locale
import javax.inject.Inject
@ -41,12 +42,13 @@ internal interface BulkLookupTask : Task<BulkLookupTask.Params, List<FoundThreeP
internal class DefaultBulkLookupTask @Inject constructor(
private val identityApiProvider: IdentityApiProvider,
private val identityServiceStore: IdentityServiceStore
private val identityServiceStore: IdentityServiceStore,
@UserId private val userId: String
) : BulkLookupTask {
override suspend fun execute(params: BulkLookupTask.Params): List<FoundThreePid> {
val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured
val entity = identityServiceStore.get() ?: throw IdentityServiceError.NoIdentityServerConfigured
val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId)
val entity = identityServiceStore.getIdentityServerDetails() ?: throw IdentityServiceError.NoIdentityServerConfigured
val pepper = entity.hashLookupPepper
val hashDetailResponse = if (pepper == null) {
// We need to fetch the hash details first
@ -128,11 +130,4 @@ internal class DefaultBulkLookupTask @Inject constructor(
FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpV2Response.mappings[hashedAddress] ?: error(""))
}
}
private fun ThreePid.toMedium(): String {
return when (this) {
is ThreePid.Email -> ThirdPartyIdentifier.MEDIUM_EMAIL
is ThreePid.Msisdn -> ThirdPartyIdentifier.MEDIUM_MSISDN
}
}
}

View file

@ -40,6 +40,8 @@ import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStor
import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource
import im.vector.matrix.android.internal.session.identity.todelete.observeNotNull
import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
import im.vector.matrix.android.internal.session.profile.BindThreePidsTask
import im.vector.matrix.android.internal.session.profile.UnbindThreePidsTask
import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
@ -59,6 +61,7 @@ internal class DefaultIdentityService @Inject constructor(
private val bulkLookupTask: BulkLookupTask,
private val identityRegisterTask: IdentityRegisterTask,
private val identityDisconnectTask: IdentityDisconnectTask,
private val identityRequestTokenForBindingTask: IdentityRequestTokenForBindingTask,
@Unauthenticated
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
@AuthenticatedIdentity
@ -66,6 +69,8 @@ internal class DefaultIdentityService @Inject constructor(
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val bindThreePidsTask: BindThreePidsTask,
private val unbindThreePidsTask: UnbindThreePidsTask,
private val identityApiProvider: IdentityApiProvider,
private val accountDataDataSource: AccountDataDataSource
) : IdentityService {
@ -85,12 +90,12 @@ internal class DefaultIdentityService @Inject constructor(
}
// Init identityApi
updateIdentityAPI(identityServiceStore.get()?.identityServerUrl)
updateIdentityAPI(identityServiceStore.getIdentityServerDetails()?.identityServerUrl)
}
private fun notifyIdentityServerUrlChange(baseUrl: String?) {
// This is maybe not a real change (echo of account data we are just setting)
if (identityServiceStore.get()?.identityServerUrl == baseUrl) {
if (identityServiceStore.getIdentityServerDetails()?.identityServerUrl == baseUrl) {
Timber.d("Echo of local identity server url change, or no change")
} else {
// Url has changed, we have to reset our store, update internal configuration and notify listeners
@ -111,23 +116,29 @@ internal class DefaultIdentityService @Inject constructor(
}
override fun getCurrentIdentityServer(): String? {
return identityServiceStore.get()?.identityServerUrl
return identityServiceStore.getIdentityServerDetails()?.identityServerUrl
}
override fun startBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback<ThreePid>) {
override fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid))
}
}
override fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
bindThreePidsTask.execute(BindThreePidsTask.Params(threePid))
}
}
override fun submitValidationToken(pid: ThreePid, code: String, callback: MatrixCallback<Unit>): Cancelable {
TODO("Not yet implemented")
}
override fun finalizeBindSessionFor3PID(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>) {
TODO("Not yet implemented")
}
override fun submitValidationToken(pid: ThreePid, code: String, matrixCallback: MatrixCallback<Unit>) {
TODO("Not yet implemented")
}
override fun startUnBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback<Pair<Boolean, ThreePid?>>) {
TODO("Not yet implemented")
override fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
unbindThreePidsTask.execute(UnbindThreePidsTask.Params(threePid))
}
}
override fun setNewIdentityServer(url: String?, callback: MatrixCallback<String?>): Cancelable {
@ -148,7 +159,7 @@ internal class DefaultIdentityService @Inject constructor(
Timber.d("Same URL, nothing to do")
null -> {
// Disconnect previous one if any
identityServiceStore.get()?.let {
identityServiceStore.getIdentityServerDetails()?.let {
if (it.identityServerUrl != null && it.token != null) {
// Disconnect, ignoring any error
runCatching {
@ -216,7 +227,7 @@ internal class DefaultIdentityService @Inject constructor(
}
private suspend fun ensureToken() {
val entity = identityServiceStore.get() ?: throw IdentityServiceError.NoIdentityServerConfigured
val entity = identityServiceStore.getIdentityServerDetails() ?: throw IdentityServiceError.NoIdentityServerConfigured
val url = entity.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured
if (entity.token == null) {
@ -246,7 +257,7 @@ internal class DefaultIdentityService @Inject constructor(
private fun updateIdentityAPI(url: String?) {
identityApiProvider.identityApi = url
?.let { retrofitFactory.create(okHttpClient, it) }
?.let { it.create(IdentityAPI::class.java) }
?.create(IdentityAPI::class.java)
}
}

View file

@ -23,6 +23,9 @@ import im.vector.matrix.android.internal.session.identity.model.IdentityHashDeta
import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Params
import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Response
import im.vector.matrix.android.internal.session.identity.model.IdentityRequestOwnershipParams
import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForEmailBody
import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForMsisdnBody
import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
@ -62,12 +65,22 @@ internal interface IdentityAPI {
fun bulkLookupV2(@Body body: IdentityLookUpV2Params): Call<IdentityLookUpV2Response>
/**
* Request the ownership validation of an email address or a phone number previously set
* by [ProfileApi.requestEmailValidation]
* Create a session to change the bind status of an email to an identity server
* The identity server will also send an email
*
* @param medium the medium of the 3pid
* @param body
* @return the sid
*/
@POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken")
fun requestOwnershipValidationV2(@Path("medium") medium: String?,
@Body body: IdentityRequestOwnershipParams): Call<SuccessResult>
@POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/email/requestToken")
fun requestTokenToBindEmail(@Body body: IdentityRequestTokenForEmailBody): Call<IdentityRequestTokenResponse>
/**
* Create a session to change the bind status of an phone number to an identity server
* The identity server will also send an SMS on the ThreePid provided
*
* @param body
* @return the sid
*/
@POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/msisdn/requestToken")
fun requestTokenToBindMsisdn(@Body body: IdentityRequestTokenForMsisdnBody): Call<IdentityRequestTokenResponse>
}

View file

@ -23,5 +23,5 @@ import javax.inject.Inject
internal class IdentityAccessTokenProvider @Inject constructor(
private val identityServiceStore: IdentityServiceStore
) : AccessTokenProvider {
override fun getToken() = identityServiceStore.get()?.token
override fun getToken() = identityServiceStore.getIdentityServerDetails()?.token
}

View file

@ -19,15 +19,23 @@ package im.vector.matrix.android.internal.session.identity
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
import im.vector.matrix.android.internal.di.IdentityDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
import im.vector.matrix.android.internal.session.SessionModule
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.identity.db.IdentityRealmModule
import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStore
import im.vector.matrix.android.internal.session.identity.db.RealmIdentityServerStore
import im.vector.matrix.android.internal.session.identity.db.RealmIdentityServiceStore
import io.realm.RealmConfiguration
import okhttp3.OkHttpClient
import java.io.File
@Module
internal abstract class IdentityModule {
@ -56,6 +64,26 @@ internal abstract class IdentityModule {
}
.build()
}
@JvmStatic
@Provides
@IdentityDatabase
@SessionScope
fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils,
@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String): RealmConfiguration {
return RealmConfiguration.Builder()
.directory(directory)
.name("matrix-sdk-identity.realm")
.apply {
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
}
.modules(IdentityRealmModule())
// TODO Handle migration properly
.deleteRealmIfMigrationNeeded()
.build()
}
}
@Binds
@ -63,11 +91,14 @@ internal abstract class IdentityModule {
abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider
@Binds
abstract fun bindIdentityServiceStore(store: RealmIdentityServerStore): IdentityServiceStore
abstract fun bindIdentityServiceStore(store: RealmIdentityServiceStore): IdentityServiceStore
@Binds
abstract fun bindIdentityRegisterTask(task: DefaultIdentityRegisterTask): IdentityRegisterTask
@Binds
abstract fun bindIdentityRequestTokenForBindingTask(task: DefaultIdentityRequestTokenForBindingTask): IdentityRequestTokenForBindingTask
@Binds
abstract fun bindBulkLookupTask(task: DefaultBulkLookupTask): BulkLookupTask

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.identity
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.identity.db.RealmIdentityServiceStore
import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForEmailBody
import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForMsisdnBody
import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenResponse
import im.vector.matrix.android.internal.task.Task
import java.util.UUID
import javax.inject.Inject
internal interface IdentityRequestTokenForBindingTask : Task<IdentityRequestTokenForBindingTask.Params, Unit> {
data class Params(
val threePid: ThreePid
)
}
internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor(
private val identityApiProvider: IdentityApiProvider,
private val identityServiceStore: RealmIdentityServiceStore,
@UserId private val userId: String
) : IdentityRequestTokenForBindingTask {
override suspend fun execute(params: IdentityRequestTokenForBindingTask.Params) {
val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId)
val clientSecret = UUID.randomUUID().toString()
val tokenResponse = executeRequest<IdentityRequestTokenResponse>(null) {
apiCall = when (params.threePid) {
is ThreePid.Email -> identityAPI.requestTokenToBindEmail(IdentityRequestTokenForEmailBody(
clientSecret = clientSecret,
sendAttempt = 1,
email = params.threePid.email
))
is ThreePid.Msisdn -> identityAPI.requestTokenToBindMsisdn(IdentityRequestTokenForMsisdnBody(
clientSecret = clientSecret,
sendAttempt = 1,
phoneNumber = params.threePid.msisdn,
countryCode = params.threePid.countryCode
))
}
}
if (!tokenResponse.success) {
throw IdentityServiceError.BindingError
}
// Store client secret and sid
identityServiceStore.storePendingBinding(
params.threePid,
clientSecret,
tokenResponse.sid)
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.identity
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.identity.model.IdentityAccountResponse
internal suspend fun getIdentityApiAndEnsureTerms(identityApiProvider: IdentityApiProvider, userId: String): IdentityAPI {
val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured
// Always check that we have access to the service (regarding terms)
val identityAccountResponse = executeRequest<IdentityAccountResponse>(null) {
apiCall = identityAPI.getAccount()
}
assert(userId == identityAccountResponse.userId)
return identityAPI
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.identity.db
import io.realm.RealmObject
internal open class IdentityPendingBindingEntity(
var threePidValue: String = "",
var medium: String = "",
var clientSecret: String = "",
var sid: String = ""
) : RealmObject() {
companion object
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.identity.db
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.identity.toMedium
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
internal fun IdentityPendingBindingEntity.Companion.get(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity? {
return realm.where<IdentityPendingBindingEntity>()
.equalTo(IdentityPendingBindingEntityFields.THREE_PID_VALUE, threePid.value)
.equalTo(IdentityPendingBindingEntityFields.MEDIUM, threePid.toMedium())
.findFirst()
}
internal fun IdentityPendingBindingEntity.Companion.getOrCreate(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity {
return get(realm, threePid) ?: realm.createObject()
}
internal fun IdentityPendingBindingEntity.Companion.delete(realm: Realm, threePid: ThreePid) {
realm.where<IdentityPendingBindingEntity>()
.equalTo(IdentityPendingBindingEntityFields.THREE_PID_VALUE, threePid.value)
.equalTo(IdentityPendingBindingEntityFields.MEDIUM, threePid.toMedium())
.findAll()
.deleteAllFromRealm()
}
internal fun IdentityPendingBindingEntity.Companion.deleteAll(realm: Realm) {
realm.where<IdentityPendingBindingEntity>()
.findAll()
.deleteAllFromRealm()
}

View file

@ -23,6 +23,7 @@ import io.realm.annotations.RealmModule
*/
@RealmModule(library = true,
classes = [
IdentityServerEntity::class
IdentityServerEntity::class,
IdentityPendingBindingEntity::class
])
internal class IdentityRealmModule

View file

@ -35,6 +35,8 @@ private fun IdentityServerEntity.Companion.getOrCreate(realm: Realm): IdentitySe
internal fun IdentityServerEntity.Companion.setUrl(realm: Realm,
url: String?) {
realm.where<IdentityServerEntity>().findAll().deleteAllFromRealm()
// Delete all pending binding if any
IdentityPendingBindingEntity.deleteAll(realm)
if (url != null) {
getOrCreate(realm).apply {

View file

@ -16,15 +16,27 @@
package im.vector.matrix.android.internal.session.identity.db
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse
internal interface IdentityServiceStore {
fun get(): IdentityServerEntity?
fun getIdentityServerDetails(): IdentityServerEntity?
fun setUrl(url: String?)
fun setToken(token: String?)
fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse)
/**
* Store details about a current binding
*/
fun storePendingBinding(threePid: ThreePid,
clientSecret: String,
sid: String)
fun getPendingBinding(threePid: ThreePid): IdentityPendingBindingEntity?
fun deletePendingBinding(threePid: ThreePid)
}

View file

@ -16,6 +16,8 @@
package im.vector.matrix.android.internal.session.identity.db
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.identity.toMedium
import im.vector.matrix.android.internal.di.IdentityDatabase
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse
@ -24,12 +26,12 @@ import io.realm.RealmConfiguration
import javax.inject.Inject
@SessionScope
internal class RealmIdentityServerStore @Inject constructor(
internal class RealmIdentityServiceStore @Inject constructor(
@IdentityDatabase
private val realmConfiguration: RealmConfiguration
) : IdentityServiceStore {
override fun get(): IdentityServerEntity? {
override fun getIdentityServerDetails(): IdentityServerEntity? {
return Realm.getInstance(realmConfiguration).use { realm ->
IdentityServerEntity.get(realm)?.let { realm.copyFromRealm(it) }
}
@ -58,4 +60,31 @@ internal class RealmIdentityServerStore @Inject constructor(
}
}
}
override fun storePendingBinding(threePid: ThreePid, clientSecret: String, sid: String) {
Realm.getInstance(realmConfiguration).use {
it.executeTransaction { realm ->
IdentityPendingBindingEntity.getOrCreate(realm, threePid).let { entity ->
entity.threePidValue = threePid.value
entity.medium = threePid.toMedium()
entity.clientSecret = clientSecret
entity.sid = sid
}
}
}
}
override fun getPendingBinding(threePid: ThreePid): IdentityPendingBindingEntity? {
return Realm.getInstance(realmConfiguration).use { realm ->
IdentityPendingBindingEntity.get(realm, threePid)?.let { realm.copyFromRealm(it) }
}
}
override fun deletePendingBinding(threePid: ThreePid) {
Realm.getInstance(realmConfiguration).use {
it.executeTransaction { realm ->
IdentityPendingBindingEntity.delete(realm, threePid)
}
}
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.identity.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// Just to consider common parameters
private interface IdentityRequestTokenBody {
val clientSecret: String
val sendAttempt: Int
}
@JsonClass(generateAdapter = true)
internal data class IdentityRequestTokenForEmailBody(
@Json(name = "client_secret")
override val clientSecret: String,
@Json(name = "send_attempt")
override val sendAttempt: Int,
@Json(name = "email")
val email: String
) : IdentityRequestTokenBody
@JsonClass(generateAdapter = true)
internal data class IdentityRequestTokenForMsisdnBody(
@Json(name = "client_secret")
override val clientSecret: String,
@Json(name = "send_attempt")
override val sendAttempt: Int,
@Json(name = "phone_number")
val phoneNumber: String,
@Json(name = "country")
val countryCode: String?
) : IdentityRequestTokenBody

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.identity.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class IdentityRequestTokenResponse(
@Json(name = "sid")
val sid: String,
@Json(name = "success")
val success: Boolean
)

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class BindThreePidBody(
/**
* Required. The client secret used in the session with the identity server.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The identity server to use. (without "https://")
*/
@Json(name = "id_server")
var idServer: String,
/**
* Required. An access token previously registered with the identity server.
*/
@Json(name = "id_access_token")
var idAccessToken: String,
/**
* Required. The session identifier given by the identity server.
*/
@Json(name = "sid")
var sid: String
)

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.profile
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStore
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal abstract class BindThreePidsTask : Task<BindThreePidsTask.Params, Unit> {
data class Params(
val threePid: ThreePid
)
}
internal class DefaultBindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI,
private val identityServiceStore: IdentityServiceStore,
private val eventBus: EventBus) : BindThreePidsTask() {
override suspend fun execute(params: Params) {
val idServer = identityServiceStore.getIdentityServerDetails()?.identityServerUrl?.substringAfter("://") ?: throw IdentityServiceError.NoIdentityServerConfigured
val idToken = identityServiceStore.getIdentityServerDetails()?.token ?: throw IdentityServiceError.NoIdentityServerConfigured
val pendingThreePid = identityServiceStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError
executeRequest<Unit>(eventBus) {
apiCall = profileAPI.bindThreePid(
BindThreePidBody(
clientSecret = pendingThreePid.clientSecret,
idServer = idServer,
idAccessToken = idToken,
sid = pendingThreePid.sid
))
}
// Binding is over, cleanup the store
identityServiceStore.deletePendingBinding(params.threePid)
}
}

View file

@ -20,7 +20,9 @@ package im.vector.matrix.android.internal.session.profile
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
internal interface ProfileAPI {
@ -39,4 +41,18 @@ internal interface ProfileAPI {
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid")
fun getThreePIDs(): Call<AccountThreePidsResponse>
/**
* Bind a threePid
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/bind")
fun bindThreePid(@Body body: BindThreePidBody): Call<Unit>
/**
* Unbind a threePid
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-unbind
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind")
fun unbindThreePid(@Body body: UnbindThreePidBody): Call<UnbindThreePidResponse>
}

View file

@ -45,4 +45,10 @@ internal abstract class ProfileModule {
@Binds
abstract fun bindRefreshUserThreePidsTask(task: DefaultRefreshUserThreePidsTask): RefreshUserThreePidsTask
@Binds
abstract fun bindBindThreePidsTask(task: DefaultBindThreePidsTask): BindThreePidsTask
@Binds
abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UnbindThreePidBody(
/**
* The identity server to unbind from. If not provided, the homeserver MUST use the id_server the identifier was added through.
* If the homeserver does not know the original id_server, it MUST return a id_server_unbind_result of no-support.
*/
@Json(name = "id_server")
val idServer: String?,
/**
* Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"]
*/
@Json(name = "medium")
val medium: String,
/**
* Required. The third party address being removed.
*/
@Json(name = "address")
val address: String
)

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UnbindThreePidResponse(
@Json(name = "id_server_unbind_result")
val idServerUnbindResult: String?
) {
fun isSuccess() = idServerUnbindResult == "success"
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.profile
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.identity.toMedium
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStore
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal abstract class UnbindThreePidsTask : Task<UnbindThreePidsTask.Params, Boolean> {
data class Params(
val threePid: ThreePid
)
}
internal class DefaultUnbindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI,
private val identityServiceStore: IdentityServiceStore,
private val eventBus: EventBus) : UnbindThreePidsTask() {
override suspend fun execute(params: Params): Boolean {
val idServer = identityServiceStore.getIdentityServerDetails()?.identityServerUrl?.substringAfter("://") ?: throw IdentityServiceError.NoIdentityServerConfigured
return executeRequest<UnbindThreePidResponse>(eventBus) {
apiCall = profileAPI.unbindThreePid(
UnbindThreePidBody(
idServer,
params.threePid.toMedium(),
params.threePid.value
))
}.isSuccess()
}
}

View file

@ -22,6 +22,8 @@ import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.google.i18n.phonenumbers.PhoneNumberUtil
import im.vector.matrix.android.api.session.identity.SharedState
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.resources.ColorProvider
@ -116,20 +118,19 @@ class DiscoverySettingsController @Inject constructor(
})
}
is Success -> when (piState.isShared()) {
PidInfo.SharedState.SHARED,
PidInfo.SharedState.NOT_SHARED -> {
checked(piState.isShared() == PidInfo.SharedState.SHARED)
SharedState.SHARED,
SharedState.NOT_SHARED -> {
checked(piState.isShared() == SharedState.SHARED)
buttonType(SettingsTextButtonItem.ButtonType.SWITCH)
switchChangeListener { _, checked ->
if (checked) {
listener?.onTapShareMsisdn(piState.threePid.value)
listener?.onTapShare(piState.threePid)
} else {
listener?.onTapRevokeMsisdn(piState.threePid.value)
listener?.onTapRevoke(piState.threePid)
}
}
}
PidInfo.SharedState.NOT_VERIFIED_FOR_BIND,
PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> {
SharedState.BINDING_IN_PROGRESS -> {
buttonType(SettingsTextButtonItem.ButtonType.NORMAL)
buttonTitle("")
}
@ -137,21 +138,20 @@ class DiscoverySettingsController @Inject constructor(
}
}
when (piState.isShared()) {
PidInfo.SharedState.NOT_VERIFIED_FOR_BIND,
PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> {
SharedState.BINDING_IN_PROGRESS -> {
settingsItemText {
id("tverif" + piState.threePid.value)
descriptionText(stringProvider.getString(R.string.settings_text_message_sent, phoneNumber))
interactionListener(object : SettingsItemText.Listener {
override fun onValidate(code: String) {
val bind = piState.isShared() == PidInfo.SharedState.NOT_VERIFIED_FOR_BIND
listener?.checkMsisdnVerification(piState.threePid.value, code, bind)
if (piState.threePid is ThreePid.Msisdn) {
listener?.checkMsisdnVerification(piState.threePid, code)
}
}
})
}
}
else -> {
}
else -> Unit
}
}
}
@ -202,27 +202,27 @@ class DiscoverySettingsController @Inject constructor(
})
}
is Success -> when (piState.isShared()) {
PidInfo.SharedState.SHARED,
PidInfo.SharedState.NOT_SHARED -> {
checked(piState.isShared() == PidInfo.SharedState.SHARED)
SharedState.SHARED,
SharedState.NOT_SHARED -> {
checked(piState.isShared() == SharedState.SHARED)
buttonType(SettingsTextButtonItem.ButtonType.SWITCH)
switchChangeListener { _, checked ->
if (checked) {
listener?.onTapShareEmail(piState.threePid.value)
listener?.onTapShare(piState.threePid)
} else {
listener?.onTapRevokeEmail(piState.threePid.value)
listener?.onTapRevoke(piState.threePid)
}
}
}
PidInfo.SharedState.NOT_VERIFIED_FOR_BIND,
PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> {
SharedState.BINDING_IN_PROGRESS -> {
buttonType(SettingsTextButtonItem.ButtonType.NORMAL)
buttonTitleId(R.string._continue)
infoMessageTintColorId(R.color.vector_info_color)
infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail, piState.threePid.value))
buttonClickListener(View.OnClickListener {
val bind = piState.isShared() == PidInfo.SharedState.NOT_VERIFIED_FOR_BIND
listener?.checkEmailVerification(piState.threePid.value, bind)
if (piState.threePid is ThreePid.Email) {
listener?.checkEmailVerification(piState.threePid)
}
})
}
}
@ -296,12 +296,10 @@ class DiscoverySettingsController @Inject constructor(
interface Listener {
fun onSelectIdentityServer()
fun onTapRevokeEmail(email: String)
fun onTapShareEmail(email: String)
fun checkEmailVerification(email: String, bind: Boolean)
fun checkMsisdnVerification(msisdn: String, code: String, bind: Boolean)
fun onTapRevokeMsisdn(msisdn: String)
fun onTapShareMsisdn(msisdn: String)
fun onTapRevoke(threePid: ThreePid)
fun onTapShare(threePid: ThreePid)
fun checkEmailVerification(threePid: ThreePid.Email)
fun checkMsisdnVerification(threePid: ThreePid.Msisdn, code: String)
fun onTapChangeIdentityServer()
fun onTapDisconnectIdentityServer()
fun onTapRetryToRetrieveBindings()

View file

@ -22,6 +22,7 @@ import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.identity.SharedState
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.riotx.R
@ -113,34 +114,26 @@ class DiscoverySettingsFragment @Inject constructor(
}
}
override fun onTapRevokeEmail(email: String) {
viewModel.handle(DiscoverySettingsAction.RevokeThreePid(ThreePid.Email(email)))
override fun onTapRevoke(threePid: ThreePid) {
viewModel.handle(DiscoverySettingsAction.RevokeThreePid(threePid))
}
override fun onTapShareEmail(email: String) {
viewModel.handle(DiscoverySettingsAction.ShareThreePid(ThreePid.Email(email)))
override fun onTapShare(threePid: ThreePid) {
viewModel.handle(DiscoverySettingsAction.ShareThreePid(threePid))
}
override fun checkEmailVerification(email: String, bind: Boolean) {
viewModel.handle(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Email(email), bind))
override fun checkEmailVerification(threePid: ThreePid.Email) {
viewModel.handle(DiscoverySettingsAction.FinalizeBind3pid(threePid))
}
override fun checkMsisdnVerification(msisdn: String, code: String, bind: Boolean) {
viewModel.handle(DiscoverySettingsAction.SubmitMsisdnToken(msisdn, code, bind))
}
override fun onTapRevokeMsisdn(msisdn: String) {
viewModel.handle(DiscoverySettingsAction.RevokeThreePid(ThreePid.Msisdn(msisdn)))
}
override fun onTapShareMsisdn(msisdn: String) {
viewModel.handle(DiscoverySettingsAction.ShareThreePid(ThreePid.Msisdn(msisdn)))
override fun checkMsisdnVerification(threePid: ThreePid.Msisdn, code: String) {
viewModel.handle(DiscoverySettingsAction.SubmitMsisdnToken(threePid, code))
}
override fun onTapChangeIdentityServer() = withState(viewModel) { state ->
//we should prompt if there are bound items with current is
val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty()
val hasBoundIds = pidList.any { it.isShared() == PidInfo.SharedState.SHARED }
val hasBoundIds = pidList.any { it.isShared() == SharedState.SHARED }
if (hasBoundIds) {
//we should prompt
@ -160,7 +153,7 @@ class DiscoverySettingsFragment @Inject constructor(
//we should prompt if there are bound items with current is
withState(viewModel) { state ->
val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty()
val hasBoundIds = pidList.any { it.isShared() == PidInfo.SharedState.SHARED }
val hasBoundIds = pidList.any { it.isShared() == SharedState.SHARED }
if (hasBoundIds) {
//we should prompt

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.identity.FoundThreePid
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.IdentityServiceListener
import im.vector.matrix.android.api.session.identity.SharedState
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.exhaustive
@ -44,14 +45,7 @@ data class PidInfo(
val threePid: ThreePid,
// Retrieved from IdentityServer, or transient state
val isShared: Async<SharedState>
) {
enum class SharedState {
SHARED,
NOT_SHARED,
NOT_VERIFIED_FOR_BIND,
NOT_VERIFIED_FOR_UNBIND
}
}
)
data class DiscoverySettingsState(
val identityServer: Async<String?> = Uninitialized,
@ -68,8 +62,8 @@ sealed class DiscoverySettingsAction : VectorViewModelAction {
data class ChangeIdentityServer(val url: String?) : DiscoverySettingsAction()
data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class FinalizeBind3pid(val threePid: ThreePid, val bind: Boolean) : DiscoverySettingsAction()
data class SubmitMsisdnToken(val msisdn: String, val code: String, val bind: Boolean) : DiscoverySettingsAction()
data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction()
data class SubmitMsisdnToken(val threePid: ThreePid.Msisdn, val code: String) : DiscoverySettingsAction()
}
sealed class DiscoverySettingsViewEvents : VectorViewEvents {
@ -169,136 +163,96 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
})
}
private fun shareThreePid(action: DiscoverySettingsAction.ShareThreePid) {
when (action.threePid) {
is ThreePid.Email -> shareEmail(action.threePid.email)
is ThreePid.Msisdn -> shareMsisdn(action.threePid.msisdn)
}.exhaustive
}
private fun shareEmail(email: String) = withState { state ->
private fun shareThreePid(action: DiscoverySettingsAction.ShareThreePid) = withState { state ->
if (state.identityServer() == null) return@withState
changeMailState(email, Loading())
changeThreePidState(action.threePid, Loading())
identityService.startBindSession(ThreePid.Email(email), null,
object : MatrixCallback<ThreePid> {
override fun onSuccess(data: ThreePid) {
changeMailState(email, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_BIND)/* TODO , data*/)
}
val threePid = if (action.threePid is ThreePid.Msisdn && action.threePid.countryCode == null) {
// Ensure we have a country code
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
val phoneNumber = PhoneNumberUtil.getInstance()
.parse("+${action.threePid.msisdn}", null)
action.threePid.copy(countryCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
)
} else {
action.threePid
}
changeMailState(email, Fail(failure))
}
})
identityService.startBindThreePid(threePid, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
changeThreePidState(action.threePid, Success(SharedState.BINDING_IN_PROGRESS))
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeThreePidState(action.threePid, Fail(failure))
}
})
}
private fun changeMailState(address: String, state: Async<PidInfo.SharedState>) {
private fun changeThreePidState(threePid: ThreePid, state: Async<SharedState>) {
setState {
val currentMails = emailList() ?: emptyList()
val phones = phoneNumbersList() ?: emptyList()
copy(emailList = Success(
currentMails.map {
if (it.threePid.value == address) {
if (it.threePid == threePid) {
it.copy(isShared = state)
} else {
it
}
}
))
}
}
private fun changeMsisdnState(address: String, state: Async<PidInfo.SharedState>) {
setState {
val phones = phoneNumbersList() ?: emptyList()
copy(phoneNumbersList = Success(
phones.map {
if (it.threePid.value == address) {
it.copy(isShared = state)
} else {
it
}
}
))
),
phoneNumbersList = Success(
phones.map {
if (it.threePid == threePid) {
it.copy(isShared = state)
} else {
it
}
}
)
)
}
}
private fun revokeThreePid(action: DiscoverySettingsAction.RevokeThreePid) {
when (action.threePid) {
is ThreePid.Email -> revokeEmail(action.threePid.email)
is ThreePid.Msisdn -> revokeMsisdn(action.threePid.msisdn)
is ThreePid.Email -> revokeEmail(action.threePid)
is ThreePid.Msisdn -> revokeMsisdn(action.threePid)
}.exhaustive
}
private fun revokeEmail(email: String) = withState { state ->
private fun revokeEmail(threePid: ThreePid.Email) = withState { state ->
if (state.identityServer() == null) return@withState
if (state.emailList() == null) return@withState
changeMailState(email, Loading())
changeThreePidState(threePid, Loading())
identityService.startUnBindSession(ThreePid.Email(email), null, object : MatrixCallback<Pair<Boolean, ThreePid?>> {
override fun onSuccess(data: Pair<Boolean, ThreePid?>) {
if (data.first) {
// requires mail validation
changeMailState(email, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND) /* TODO , data.second */)
} else {
changeMailState(email, Success(PidInfo.SharedState.NOT_SHARED))
}
identityService.unbindThreePid(threePid, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
changeThreePidState(threePid, Success(SharedState.NOT_SHARED))
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeMailState(email, Fail(failure))
changeThreePidState(threePid, Fail(failure))
}
})
}
private fun revokeMsisdn(msisdn: String) = withState { state ->
private fun revokeMsisdn(threePid: ThreePid.Msisdn) = withState { state ->
if (state.identityServer() == null) return@withState
if (state.emailList() == null) return@withState
changeMsisdnState(msisdn, Loading())
if (state.phoneNumbersList() == null) return@withState
changeThreePidState(threePid, Loading())
val phoneNumber = PhoneNumberUtil.getInstance()
.parse("+$msisdn", null)
val countryCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
identityService.startUnBindSession(ThreePid.Msisdn(msisdn, countryCode), null, object : MatrixCallback<Pair<Boolean, ThreePid?>> {
override fun onSuccess(data: Pair<Boolean, ThreePid?>) {
if (data.first /*requires mail validation */) {
changeMsisdnState(msisdn, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND) /* TODO , data.second */)
} else {
changeMsisdnState(msisdn, Success(PidInfo.SharedState.NOT_SHARED))
}
identityService.unbindThreePid(threePid, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
changeThreePidState(threePid, Success(SharedState.NOT_SHARED))
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeMsisdnState(msisdn, Fail(failure))
}
})
}
private fun shareMsisdn(msisdn: String) = withState { state ->
if (state.identityServer() == null) return@withState
changeMsisdnState(msisdn, Loading())
val phoneNumber = PhoneNumberUtil.getInstance()
.parse("+$msisdn", null)
val countryCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
identityService.startBindSession(ThreePid.Msisdn(msisdn, countryCode), null, object : MatrixCallback<ThreePid> {
override fun onSuccess(data: ThreePid) {
changeMsisdnState(msisdn, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_BIND) /* TODO , data */)
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeMsisdnState(msisdn, Fail(failure))
changeThreePidState(threePid, Fail(failure))
}
})
}
@ -364,59 +318,52 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
val hasMatrixId = foundThreePids.any { it.threePid == threePid }
PidInfo(
threePid = threePid,
isShared = Success(PidInfo.SharedState.SHARED.takeIf { hasMatrixId } ?: PidInfo.SharedState.NOT_SHARED)
isShared = Success(SharedState.SHARED.takeIf { hasMatrixId } ?: SharedState.NOT_SHARED)
)
}
}
private fun submitMsisdnToken(action: DiscoverySettingsAction.SubmitMsisdnToken) = withState { state ->
val pid = state.phoneNumbersList()?.find { it.threePid.value == action.msisdn }?.threePid ?: return@withState
if (state.identityServer().isNullOrBlank()) return@withState
identityService.submitValidationToken(pid,
identityService.submitValidationToken(action.threePid,
action.code,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Msisdn(action.msisdn), action.bind))
// TODO This should be done in the task
finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(action.threePid))
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
changeMsisdnState(action.msisdn, Fail(failure))
changeThreePidState(action.threePid, Fail(failure))
}
}
)
}
private fun finalizeBind3pid(action: DiscoverySettingsAction.FinalizeBind3pid) = withState { state ->
val _3pid = when (action.threePid) {
val threePid = when (action.threePid) {
is ThreePid.Email -> {
changeMailState(action.threePid.email, Loading())
changeThreePidState(action.threePid, Loading())
state.emailList()?.find { it.threePid.value == action.threePid.email }?.threePid ?: return@withState
}
is ThreePid.Msisdn -> {
changeMsisdnState(action.threePid.msisdn, Loading())
changeThreePidState(action.threePid, Loading())
state.phoneNumbersList()?.find { it.threePid.value == action.threePid.msisdn }?.threePid ?: return@withState
}
}
identityService.finalizeBindSessionFor3PID(_3pid, object : MatrixCallback<Unit> {
identityService.finalizeBindThreePid(threePid, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
val sharedState = Success(if (action.bind) PidInfo.SharedState.SHARED else PidInfo.SharedState.NOT_SHARED)
when (action.threePid) {
is ThreePid.Email -> changeMailState(action.threePid.email, sharedState)
is ThreePid.Msisdn -> changeMsisdnState(action.threePid.msisdn, sharedState)
}
changeThreePidState(action.threePid, Success(SharedState.SHARED))
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
// Restore previous state after an error
val sharedState = Success(if (action.bind) PidInfo.SharedState.NOT_VERIFIED_FOR_BIND else PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND)
when (action.threePid) {
is ThreePid.Email -> changeMailState(action.threePid.email, sharedState)
is ThreePid.Msisdn -> changeMsisdnState(action.threePid.msisdn, sharedState)
}
changeThreePidState(action.threePid, Success(SharedState.BINDING_IN_PROGRESS))
}
})
@ -425,9 +372,8 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
private fun refreshPendingEmailBindings() = withState { state ->
state.emailList()?.forEach { info ->
when (info.isShared()) {
PidInfo.SharedState.NOT_VERIFIED_FOR_BIND -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(info.threePid, true))
PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(info.threePid, false))
else -> Unit
SharedState.BINDING_IN_PROGRESS -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(info.threePid))
else -> Unit
}
}
}