mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Merge pull request #2020 from vector-im/feature/add_email
Add email and phone numbers
This commit is contained in:
commit
05ec5bde93
67 changed files with 2468 additions and 463 deletions
|
@ -2,7 +2,7 @@ Changes in Element 1.0.6 (2020-XX-XX)
|
|||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- List phone numbers and emails added to the Matrix account, and add emails and phone numbers to account (#44, #45)
|
||||
|
||||
Improvements 🙌:
|
||||
- You can now join room through permalink and within room directory search
|
||||
|
|
285
docs/add_threePids.md
Normal file
285
docs/add_threePids.md
Normal file
|
@ -0,0 +1,285 @@
|
|||
# Adding and removing ThreePids to an account
|
||||
|
||||
## Add email
|
||||
|
||||
### User enter the email
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/r0/account/3pid/email/requestToken
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "alice@email-provider.org",
|
||||
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh",
|
||||
"send_attempt": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### The email is already added to an account
|
||||
|
||||
400
|
||||
|
||||
```json
|
||||
{
|
||||
"errcode": "M_THREEPID_IN_USE",
|
||||
"error": "Email is already in use"
|
||||
}
|
||||
```
|
||||
|
||||
#### The email is free
|
||||
|
||||
Wording: "We've sent you an email to verify your address. Please follow the instructions there and then click the button below."
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "bxyDHuJKsdkjMlTJ"
|
||||
}
|
||||
```
|
||||
|
||||
## User receive an e-mail
|
||||
|
||||
> [homeserver.org] Validate your email
|
||||
>
|
||||
> A request to add an email address to your Matrix account has been received. If this was you, please click the link below to confirm adding this email:
|
||||
https://homeserver.org/_matrix/client/unstable/add_threepid/email/submit_token?token=WUnEhQAmJrXupdEbXgdWvnVIKaGYZFsU&client_secret=TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh&sid=bxyDHuJKsdkjMlTJ
|
||||
>
|
||||
> If this was not you, you can safely ignore this email. Thank you.
|
||||
|
||||
### User clicks on the link
|
||||
|
||||
The browser displays the following message:
|
||||
|
||||
> Your email has now been validated, please return to your client. You may now close this window.
|
||||
|
||||
### User returns on Element
|
||||
|
||||
User clicks on CONTINUE
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/r0/account/3pid/add
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "bxyDHuJKsdkjMlTJ",
|
||||
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh"
|
||||
}
|
||||
```
|
||||
|
||||
401 User Interactive Authentication
|
||||
|
||||
```json
|
||||
{
|
||||
"session": "ppvvnozXCQZFaggUBlHJYPjA",
|
||||
"flows": [
|
||||
{
|
||||
"stages": [
|
||||
"m.login.password"
|
||||
]
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### User enters his password
|
||||
|
||||
POST https://homeserver.org/_matrix/client/r0/account/3pid/add
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "bxyDHuJKsdkjMlTJ",
|
||||
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh",
|
||||
"auth": {
|
||||
"session": "ppvvnozXCQZFaggUBlHJYPjA",
|
||||
"type": "m.login.password",
|
||||
"user": "@benoitx:matrix.org",
|
||||
"password": "weak_password"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### The link has not been clicked
|
||||
|
||||
400
|
||||
|
||||
```json
|
||||
{
|
||||
"errcode": "M_THREEPID_AUTH_FAILED",
|
||||
"error": "No validated 3pid session found"
|
||||
}
|
||||
```
|
||||
|
||||
#### Wrong password
|
||||
|
||||
401
|
||||
|
||||
```json
|
||||
{
|
||||
"session": "fXHOvoQsPMhEebVqTnIrzZJN",
|
||||
"flows": [
|
||||
{
|
||||
"stages": [
|
||||
"m.login.password"
|
||||
]
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
},
|
||||
"completed":[
|
||||
],
|
||||
"error": "Invalid password",
|
||||
"errcode": "M_FORBIDDEN"
|
||||
}
|
||||
```
|
||||
|
||||
#### The link has been clicked and the account password is correct
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
## Remove email
|
||||
|
||||
### User want to remove an email from his account
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/r0/account/3pid/delete
|
||||
|
||||
```json
|
||||
{
|
||||
"medium": "email",
|
||||
"address": "alice@email-provider.org"
|
||||
}
|
||||
```
|
||||
|
||||
#### Email was not bound to an identity server
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{
|
||||
"id_server_unbind_result": "no-support"
|
||||
}
|
||||
```
|
||||
|
||||
#### Email was bound to an identity server
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{
|
||||
"id_server_unbind_result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
## Add phone number
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/r0/account/3pid/msisdn/requestToken
|
||||
|
||||
```json
|
||||
{
|
||||
"country": "FR",
|
||||
"phone_number": "611223344",
|
||||
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
|
||||
"send_attempt": 1
|
||||
}
|
||||
```
|
||||
|
||||
Note that the phone number is sent without `+` and without the country code
|
||||
|
||||
#### The phone number is already added to an account
|
||||
|
||||
400
|
||||
|
||||
```json
|
||||
{
|
||||
"errcode": "M_THREEPID_IN_USE",
|
||||
"error": "MSISDN is already in use"
|
||||
}
|
||||
```
|
||||
|
||||
#### The phone number is free
|
||||
|
||||
Wording: "A text message has been sent to +33611223344. Please enter the verification code it contains."
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{
|
||||
"msisdn": "33651547677",
|
||||
"intl_fmt": "+33 6 51 54 76 77",
|
||||
"success": true,
|
||||
"sid": "253299954",
|
||||
"submit_url": "https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token"
|
||||
}
|
||||
```
|
||||
|
||||
## User receive a text message
|
||||
|
||||
> Riot
|
||||
|
||||
> Your Riot validation code is 892541, please enter this into the app
|
||||
|
||||
### User enter the code to the app
|
||||
|
||||
#### Wrong code
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "253299954",
|
||||
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
|
||||
"token": "111111"
|
||||
}
|
||||
```
|
||||
|
||||
400
|
||||
|
||||
```json
|
||||
{
|
||||
"errcode": "M_UNKNOWN",
|
||||
"error": "Error contacting the identity server"
|
||||
}
|
||||
```
|
||||
|
||||
This is not an ideal, but the client will display a hint to check the entered code to the user.
|
||||
|
||||
#### Correct code
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "253299954",
|
||||
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
|
||||
"token": "892541"
|
||||
}
|
||||
```
|
||||
|
||||
200
|
||||
|
||||
````json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
````
|
||||
|
||||
Then the app call `https://homeserver.org/_matrix/client/r0/account/3pid/add` as per adding an email and follow the same UIS flow
|
||||
|
||||
## Remove phone number
|
||||
|
||||
### User wants to remove a phone number from his account
|
||||
|
||||
This is the same request and response than to remove email, but with this body:
|
||||
|
||||
```json
|
||||
{
|
||||
"medium": "msisdn",
|
||||
"address": "33611223344"
|
||||
}
|
||||
```
|
||||
|
||||
Note that the phone number is provided without `+`, but with the country code.
|
|
@ -18,9 +18,13 @@
|
|||
package org.matrix.android.sdk.rx
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function3
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
|
@ -43,10 +47,6 @@ import org.matrix.android.sdk.api.util.toOptional
|
|||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function3
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
|
@ -110,6 +110,11 @@ class RxSession(private val session: Session) {
|
|||
.startWithCallable { session.getThreePids() }
|
||||
}
|
||||
|
||||
fun livePendingThreePIds(): Observable<List<ThreePid>> {
|
||||
return session.getPendingThreePidsLive().asObservable()
|
||||
.startWithCallable { session.getPendingThreePids() }
|
||||
}
|
||||
|
||||
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
|
||||
session.createRoom(roomParams, it)
|
||||
}
|
||||
|
|
|
@ -83,4 +83,43 @@ interface ProfileService {
|
|||
* @param refreshData set to true to fetch data from the homeserver
|
||||
*/
|
||||
fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>>
|
||||
|
||||
/**
|
||||
* Get the pending 3Pids, i.e. ThreePids that have requested a token, but not yet validated by the user.
|
||||
*/
|
||||
fun getPendingThreePids(): List<ThreePid>
|
||||
|
||||
/**
|
||||
* Get the pending 3Pids Live
|
||||
*/
|
||||
fun getPendingThreePidsLive(): LiveData<List<ThreePid>>
|
||||
|
||||
/**
|
||||
* Add a 3Pids. This is the first step to add a ThreePid to an account. Then the threePid will be added to the pending threePid list.
|
||||
*/
|
||||
fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Validate a code received by text message
|
||||
*/
|
||||
fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid
|
||||
*/
|
||||
fun finalizeAddingThreePid(threePid: ThreePid,
|
||||
uiaSession: String?,
|
||||
accountPassword: String?,
|
||||
matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Cancel adding a threepid. It will remove locally stored data about this ThreePid
|
||||
*/
|
||||
fun cancelAddingThreePid(threePid: ThreePid,
|
||||
matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Remove a 3Pid from the Matrix account.
|
||||
*/
|
||||
fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.auth.registration
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
|
@ -33,9 +36,6 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
|
|||
import org.matrix.android.sdk.internal.network.RetrofitFactory
|
||||
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
/**
|
||||
* This class execute the registration request and is responsible to keep the session of interactive authentication
|
||||
|
@ -193,7 +193,7 @@ internal class DefaultRegistrationWizard(
|
|||
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code")
|
||||
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code")
|
||||
val validationBody = ValidationCodeBody(
|
||||
clientSecret = pendingSessionData.clientSecret,
|
||||
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
|
||||
|
|
|
@ -20,18 +20,24 @@ package org.matrix.android.sdk.internal.database
|
|||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||
|
||||
companion object {
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 4L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
|
||||
|
||||
if (oldVersion <= 0) migrateTo1(realm)
|
||||
if (oldVersion <= 1) migrateTo2(realm)
|
||||
if (oldVersion <= 2) migrateTo3(realm)
|
||||
if (oldVersion <= 3) migrateTo4(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
|
@ -63,4 +69,17 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateTo4(realm: DynamicRealm) {
|
||||
Timber.d("Step 3 -> 4")
|
||||
realm.schema.create("PendingThreePidEntity")
|
||||
.addField(PendingThreePidEntityFields.CLIENT_SECRET, String::class.java)
|
||||
.setRequired(PendingThreePidEntityFields.CLIENT_SECRET, true)
|
||||
.addField(PendingThreePidEntityFields.EMAIL, String::class.java)
|
||||
.addField(PendingThreePidEntityFields.MSISDN, String::class.java)
|
||||
.addField(PendingThreePidEntityFields.SEND_ATTEMPT, Int::class.java)
|
||||
.addField(PendingThreePidEntityFields.SID, String::class.java)
|
||||
.setRequired(PendingThreePidEntityFields.SID, true)
|
||||
.addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,14 @@ package org.matrix.android.sdk.internal.database
|
|||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
|
||||
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.UserMd5
|
||||
import org.matrix.android.sdk.internal.session.SessionModule
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
@ -46,20 +47,16 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
|
|||
val migration: RealmSessionStoreMigration,
|
||||
context: Context) {
|
||||
|
||||
companion object {
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 3L
|
||||
}
|
||||
|
||||
// Keep legacy preferences name for compatibility reason
|
||||
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
|
||||
|
||||
fun create(): RealmConfiguration {
|
||||
val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
|
||||
if (shouldClearRealm) {
|
||||
Timber.v("************************************************************")
|
||||
Timber.v("The realm file session was corrupted and couldn't be loaded.")
|
||||
Timber.v("The file has been deleted to recover.")
|
||||
Timber.v("************************************************************")
|
||||
Timber.e("************************************************************")
|
||||
Timber.e("The realm file session was corrupted and couldn't be loaded.")
|
||||
Timber.e("The file has been deleted to recover.")
|
||||
Timber.e("************************************************************")
|
||||
deleteRealmFiles()
|
||||
}
|
||||
sharedPreferences.edit {
|
||||
|
@ -74,7 +71,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
|
|||
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
|
||||
}
|
||||
.modules(SessionRealmModule())
|
||||
.schemaVersion(SESSION_STORE_SCHEMA_VERSION)
|
||||
.schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION)
|
||||
.migration(migration)
|
||||
.build()
|
||||
|
||||
|
@ -90,6 +87,11 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
|
|||
|
||||
// Delete all the realm files of the session
|
||||
private fun deleteRealmFiles() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.e("No op because it is a debug build")
|
||||
return
|
||||
}
|
||||
|
||||
listOf(REALM_NAME, "$REALM_NAME.lock", "$REALM_NAME.note", "$REALM_NAME.management").forEach { file ->
|
||||
try {
|
||||
File(directory, file).deleteRecursively()
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.database.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
|
||||
/**
|
||||
* This class is used to store pending threePid data, when user wants to add a threePid to his account
|
||||
*/
|
||||
internal open class PendingThreePidEntity(
|
||||
var email: String? = null,
|
||||
var msisdn: String? = null,
|
||||
var clientSecret: String = "",
|
||||
var sendAttempt: Int = 0,
|
||||
var sid: String = "",
|
||||
var submitUrl: String? = null
|
||||
) : RealmObject()
|
|
@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule
|
|||
RoomSummaryEntity::class,
|
||||
RoomTagEntity::class,
|
||||
SyncEntity::class,
|
||||
PendingThreePidEntity::class,
|
||||
UserEntity::class,
|
||||
IgnoredUserEntity::class,
|
||||
BreadcrumbsEntity::class,
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AddEmailBody(
|
||||
/**
|
||||
* Required. A unique string generated by the client, and used to identify the validation attempt.
|
||||
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed
|
||||
* 255 characters and it must not be empty.
|
||||
*/
|
||||
@Json(name = "client_secret")
|
||||
val clientSecret: String,
|
||||
|
||||
/**
|
||||
* Required. The email address to validate.
|
||||
*/
|
||||
@Json(name = "email")
|
||||
val email: String,
|
||||
|
||||
/**
|
||||
* Required. The server will only send an email if the send_attempt is a number greater than the most
|
||||
* recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly
|
||||
* sending the same email in the case of request retries between the POSTing user and the identity server.
|
||||
* The client should increment this value if they desire a new email (e.g. a reminder) to be sent.
|
||||
* If they do not, the server should respond with success but not resend the email.
|
||||
*/
|
||||
@Json(name = "send_attempt")
|
||||
val sendAttempt: Int
|
||||
)
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AddEmailResponse(
|
||||
/**
|
||||
* Required. The session ID. Session IDs are opaque strings that must consist entirely
|
||||
* of the characters [0-9a-zA-Z.=_-]. Their length must not exceed 255 characters and they must not be empty.
|
||||
*/
|
||||
@Json(name = "sid")
|
||||
val sid: String
|
||||
)
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AddMsisdnBody(
|
||||
/**
|
||||
* Required. A unique string generated by the client, and used to identify the validation attempt.
|
||||
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed
|
||||
* 255 characters and it must not be empty.
|
||||
*/
|
||||
@Json(name = "client_secret")
|
||||
val clientSecret: String,
|
||||
|
||||
/**
|
||||
* Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in
|
||||
* phone_number should be parsed as if it were dialled from.
|
||||
*/
|
||||
@Json(name = "country")
|
||||
val country: String,
|
||||
|
||||
/**
|
||||
* Required. The phone number to validate.
|
||||
*/
|
||||
@Json(name = "phone_number")
|
||||
val phoneNumber: String,
|
||||
|
||||
/**
|
||||
* Required. The server will only send an SMS if the send_attempt is a number greater than the most
|
||||
* recent one which it has seen, scoped to that country + phone_number + client_secret triple. This
|
||||
* is to avoid repeatedly sending the same SMS in the case of request retries between the POSTing user
|
||||
* and the identity server. The client should increment this value if they desire a new SMS (e.g. a
|
||||
* reminder) to be sent.
|
||||
*/
|
||||
@Json(name = "send_attempt")
|
||||
val sendAttempt: Int
|
||||
)
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AddMsisdnResponse(
|
||||
/**
|
||||
* Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-].
|
||||
* Their length must not exceed 255 characters and they must not be empty.
|
||||
*/
|
||||
@Json(name = "sid")
|
||||
val sid: String,
|
||||
|
||||
/**
|
||||
* An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity
|
||||
* Service API's POST /validate/email/submitToken endpoint (without the requirement for an access token).
|
||||
* The homeserver must send this token to the user (if applicable), who should then be prompted to provide it to the client.
|
||||
*
|
||||
* If this field is not present, the client can assume that verification will happen without the client's involvement provided
|
||||
* the homeserver advertises this specification version in the /versions response (ie: r0.5.0).
|
||||
*/
|
||||
@Json(name = "submit_url")
|
||||
val submitUrl: String? = null,
|
||||
|
||||
/* ==========================================================================================
|
||||
* It seems that the homeserver is sending more data, we may need it
|
||||
* ========================================================================================== */
|
||||
|
||||
@Json(name = "msisdn")
|
||||
val msisdn: String? = null,
|
||||
|
||||
@Json(name = "intl_fmt")
|
||||
val formattedMsisdn: String? = null,
|
||||
|
||||
@Json(name = "success")
|
||||
val success: Boolean? = null
|
||||
)
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
internal abstract class AddThreePidTask : Task<AddThreePidTask.Params, Unit> {
|
||||
data class Params(
|
||||
val threePid: ThreePid
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultAddThreePidTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val pendingThreePidMapper: PendingThreePidMapper,
|
||||
private val eventBus: EventBus) : AddThreePidTask() {
|
||||
|
||||
override suspend fun execute(params: Params) {
|
||||
when (params.threePid) {
|
||||
is ThreePid.Email -> addEmail(params.threePid)
|
||||
is ThreePid.Msisdn -> addMsisdn(params.threePid)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addEmail(threePid: ThreePid.Email) {
|
||||
val clientSecret = UUID.randomUUID().toString()
|
||||
val sendAttempt = 1
|
||||
|
||||
val result = executeRequest<AddEmailResponse>(eventBus) {
|
||||
val body = AddEmailBody(
|
||||
clientSecret = clientSecret,
|
||||
email = threePid.email,
|
||||
sendAttempt = sendAttempt
|
||||
)
|
||||
apiCall = profileAPI.addEmail(body)
|
||||
}
|
||||
|
||||
// Store as a pending three pid
|
||||
monarchy.awaitTransaction { realm ->
|
||||
PendingThreePid(
|
||||
threePid = threePid,
|
||||
clientSecret = clientSecret,
|
||||
sendAttempt = sendAttempt,
|
||||
sid = result.sid,
|
||||
submitUrl = null
|
||||
)
|
||||
.let { pendingThreePidMapper.map(it) }
|
||||
.let { realm.copyToRealm(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addMsisdn(threePid: ThreePid.Msisdn) {
|
||||
val clientSecret = UUID.randomUUID().toString()
|
||||
val sendAttempt = 1
|
||||
|
||||
// Get country code and national number from the phone number
|
||||
val phoneNumber = threePid.msisdn
|
||||
val phoneNumberUtil = PhoneNumberUtil.getInstance()
|
||||
val parsedNumber = phoneNumberUtil.parse(phoneNumber, null)
|
||||
val countryCode = parsedNumber.countryCode
|
||||
val country = phoneNumberUtil.getRegionCodeForCountryCode(countryCode)
|
||||
|
||||
val result = executeRequest<AddMsisdnResponse>(eventBus) {
|
||||
val body = AddMsisdnBody(
|
||||
clientSecret = clientSecret,
|
||||
country = country,
|
||||
phoneNumber = parsedNumber.nationalNumber.toString(),
|
||||
sendAttempt = sendAttempt
|
||||
)
|
||||
apiCall = profileAPI.addMsisdn(body)
|
||||
}
|
||||
|
||||
// Store as a pending three pid
|
||||
monarchy.awaitTransaction { realm ->
|
||||
PendingThreePid(
|
||||
threePid = threePid,
|
||||
clientSecret = clientSecret,
|
||||
sendAttempt = sendAttempt,
|
||||
sid = result.sid,
|
||||
submitUrl = result.submitUrl
|
||||
)
|
||||
.let { pendingThreePidMapper.map(it) }
|
||||
.let { realm.copyToRealm(it) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService
|
|||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||
import org.matrix.android.sdk.internal.database.model.UserThreePidEntity
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.content.FileUploader
|
||||
|
@ -44,6 +45,11 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||
private val getProfileInfoTask: GetProfileInfoTask,
|
||||
private val setDisplayNameTask: SetDisplayNameTask,
|
||||
private val setAvatarUrlTask: SetAvatarUrlTask,
|
||||
private val addThreePidTask: AddThreePidTask,
|
||||
private val validateSmsCodeTask: ValidateSmsCodeTask,
|
||||
private val finalizeAddingThreePidTask: FinalizeAddingThreePidTask,
|
||||
private val deleteThreePidTask: DeleteThreePidTask,
|
||||
private val pendingThreePidMapper: PendingThreePidMapper,
|
||||
private val fileUploader: FileUploader) : ProfileService {
|
||||
|
||||
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||
|
@ -116,9 +122,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||
override fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> {
|
||||
if (refreshData) {
|
||||
// Force a refresh of the values
|
||||
refreshUserThreePidsTask
|
||||
.configureWith()
|
||||
.executeBy(taskExecutor)
|
||||
refreshThreePids()
|
||||
}
|
||||
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
|
@ -126,6 +130,95 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||
{ it.asDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
private fun refreshThreePids() {
|
||||
refreshUserThreePidsTask
|
||||
.configureWith()
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getPendingThreePids(): List<ThreePid> {
|
||||
return monarchy.fetchAllMappedSync(
|
||||
{ it.where<PendingThreePidEntity>() },
|
||||
{ pendingThreePidMapper.map(it).threePid }
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPendingThreePidsLive(): LiveData<List<ThreePid>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ it.where<PendingThreePidEntity>() },
|
||||
{ pendingThreePidMapper.map(it).threePid }
|
||||
)
|
||||
}
|
||||
|
||||
override fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return addThreePidTask
|
||||
.configureWith(AddThreePidTask.Params(threePid)) {
|
||||
callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return validateSmsCodeTask
|
||||
.configureWith(ValidateSmsCodeTask.Params(threePid, code)) {
|
||||
callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun finalizeAddingThreePid(threePid: ThreePid,
|
||||
uiaSession: String?,
|
||||
accountPassword: String?,
|
||||
matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return finalizeAddingThreePidTask
|
||||
.configureWith(FinalizeAddingThreePidTask.Params(
|
||||
threePid = threePid,
|
||||
session = uiaSession,
|
||||
accountPassword = accountPassword,
|
||||
userWantsToCancel = false
|
||||
)) {
|
||||
callback = alsoRefresh(matrixCallback)
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun cancelAddingThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return finalizeAddingThreePidTask
|
||||
.configureWith(FinalizeAddingThreePidTask.Params(
|
||||
threePid = threePid,
|
||||
session = null,
|
||||
accountPassword = null,
|
||||
userWantsToCancel = true
|
||||
)) {
|
||||
callback = alsoRefresh(matrixCallback)
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the callback to fetch 3Pids from the server in case of success
|
||||
*/
|
||||
private fun alsoRefresh(callback: MatrixCallback<Unit>): MatrixCallback<Unit> {
|
||||
return object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
refreshThreePids()
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return deleteThreePidTask
|
||||
.configureWith(DeleteThreePidTask.Params(threePid)) {
|
||||
callback = alsoRefresh(matrixCallback)
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserThreePidEntity.asDomain(): ThreePid {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class DeleteThreePidBody(
|
||||
/**
|
||||
* 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
|
||||
)
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class DeleteThreePidResponse(
|
||||
/**
|
||||
* Required. An indicator as to whether or not the homeserver was able to unbind the 3PID from
|
||||
* the identity server. success indicates that the identity server has unbound the identifier
|
||||
* whereas no-support indicates that the identity server refuses to support the request or the
|
||||
* homeserver was not able to determine an identity server to unbind from. One of: ["no-support", "success"]
|
||||
*/
|
||||
@Json(name = "id_server_unbind_result")
|
||||
val idServerUnbindResult: String? = null
|
||||
)
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.identity.toMedium
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal abstract class DeleteThreePidTask : Task<DeleteThreePidTask.Params, Unit> {
|
||||
data class Params(
|
||||
val threePid: ThreePid
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultDeleteThreePidTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
private val eventBus: EventBus) : DeleteThreePidTask() {
|
||||
|
||||
override suspend fun execute(params: Params) {
|
||||
executeRequest<DeleteThreePidResponse>(eventBus) {
|
||||
val body = DeleteThreePidBody(
|
||||
medium = params.threePid.toMedium(),
|
||||
address = params.threePid.value
|
||||
)
|
||||
apiCall = profileAPI.deleteThreePid(body)
|
||||
}
|
||||
|
||||
// We do not really care about the result for the moment
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class FinalizeAddThreePidBody(
|
||||
/**
|
||||
* Required. The client secret used in the session with the homeserver.
|
||||
*/
|
||||
@Json(name = "client_secret")
|
||||
val clientSecret: String,
|
||||
|
||||
/**
|
||||
* Required. The session identifier given by the homeserver.
|
||||
*/
|
||||
@Json(name = "sid")
|
||||
val sid: String,
|
||||
|
||||
/**
|
||||
* Additional authentication information for the user-interactive authentication API.
|
||||
*/
|
||||
@Json(name = "auth")
|
||||
val auth: UserPasswordAuth?
|
||||
)
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import javax.inject.Inject
|
||||
|
||||
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
|
||||
data class Params(
|
||||
val threePid: ThreePid,
|
||||
val session: String?,
|
||||
val accountPassword: String?,
|
||||
val userWantsToCancel: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val pendingThreePidMapper: PendingThreePidMapper,
|
||||
@UserId private val userId: String,
|
||||
private val eventBus: EventBus) : FinalizeAddingThreePidTask() {
|
||||
|
||||
override suspend fun execute(params: Params) {
|
||||
if (params.userWantsToCancel.not()) {
|
||||
// Get the required pending data
|
||||
val pendingThreePids = monarchy.fetchAllMappedSync(
|
||||
{ it.where(PendingThreePidEntity::class.java) },
|
||||
{ pendingThreePidMapper.map(it) }
|
||||
)
|
||||
.firstOrNull { it.threePid == params.threePid }
|
||||
?: throw IllegalArgumentException("unknown threepid")
|
||||
|
||||
try {
|
||||
executeRequest<Unit>(eventBus) {
|
||||
val body = FinalizeAddThreePidBody(
|
||||
clientSecret = pendingThreePids.clientSecret,
|
||||
sid = pendingThreePids.sid,
|
||||
auth = if (params.session != null && params.accountPassword != null) {
|
||||
UserPasswordAuth(
|
||||
session = params.session,
|
||||
user = userId,
|
||||
password = params.accountPassword
|
||||
)
|
||||
} else null
|
||||
)
|
||||
apiCall = profileAPI.finalizeAddThreePid(body)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
throw throwable.toRegistrationFlowResponse()
|
||||
?.let { Failure.RegistrationFlowError(it) }
|
||||
?: throwable
|
||||
}
|
||||
}
|
||||
|
||||
cleanupDatabase(params)
|
||||
}
|
||||
|
||||
private suspend fun cleanupDatabase(params: Params) {
|
||||
// Delete the pending three pid
|
||||
monarchy.awaitTransaction { realm ->
|
||||
realm.where(PendingThreePidEntity::class.java)
|
||||
.equalTo(PendingThreePidEntityFields.EMAIL, params.threePid.value)
|
||||
.or()
|
||||
.equalTo(PendingThreePidEntityFields.MSISDN, params.threePid.value)
|
||||
.findAll()
|
||||
.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
|
||||
internal data class PendingThreePid(
|
||||
val threePid: ThreePid,
|
||||
val clientSecret: String,
|
||||
val sendAttempt: Int,
|
||||
// For Msisdn and Email
|
||||
val sid: String,
|
||||
// For Msisdn only
|
||||
val submitUrl: String?
|
||||
)
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class PendingThreePidMapper @Inject constructor() {
|
||||
|
||||
fun map(entity: PendingThreePidEntity): PendingThreePid {
|
||||
return PendingThreePid(
|
||||
threePid = entity.email?.let { ThreePid.Email(it) }
|
||||
?: entity.msisdn?.let { ThreePid.Msisdn(it) }
|
||||
?: error("Invalid data"),
|
||||
clientSecret = entity.clientSecret,
|
||||
sendAttempt = entity.sendAttempt,
|
||||
sid = entity.sid,
|
||||
submitUrl = entity.submitUrl
|
||||
)
|
||||
}
|
||||
|
||||
fun map(domain: PendingThreePid): PendingThreePidEntity {
|
||||
return PendingThreePidEntity(
|
||||
email = domain.threePid.takeIf { it is ThreePid.Email }?.value,
|
||||
msisdn = domain.threePid.takeIf { it is ThreePid.Msisdn }?.value,
|
||||
clientSecret = domain.clientSecret,
|
||||
sendAttempt = domain.sendAttempt,
|
||||
sid = domain.sid,
|
||||
submitUrl = domain.submitUrl
|
||||
)
|
||||
}
|
||||
}
|
|
@ -19,6 +19,8 @@
|
|||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.auth.registration.SuccessResult
|
||||
import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
|
@ -26,9 +28,9 @@ import retrofit2.http.GET
|
|||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Url
|
||||
|
||||
internal interface ProfileAPI {
|
||||
|
||||
/**
|
||||
* Get the combined profile information for this user.
|
||||
* This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers.
|
||||
|
@ -71,4 +73,35 @@ internal interface ProfileAPI {
|
|||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind")
|
||||
fun unbindThreePid(@Body body: UnbindThreePidBody): Call<UnbindThreePidResponse>
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-email-requesttoken
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken")
|
||||
fun addEmail(@Body body: AddEmailBody): Call<AddEmailResponse>
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-msisdn-requesttoken
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/msisdn/requestToken")
|
||||
fun addMsisdn(@Body body: AddMsisdnBody): Call<AddMsisdnResponse>
|
||||
|
||||
/**
|
||||
* Validate Msisdn code (same model than for Identity server API)
|
||||
*/
|
||||
@POST
|
||||
fun validateMsisdn(@Url url: String,
|
||||
@Body params: ValidationCodeBody): Call<SuccessResult>
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-add
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/add")
|
||||
fun finalizeAddThreePid(@Body body: FinalizeAddThreePidBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-delete
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/delete")
|
||||
fun deleteThreePid(@Body body: DeleteThreePidBody): Call<DeleteThreePidResponse>
|
||||
}
|
||||
|
|
|
@ -58,4 +58,16 @@ internal abstract class ProfileModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindAddThreePidTask(task: DefaultAddThreePidTask): AddThreePidTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindValidateSmsCodeTask(task: DefaultValidateSmsCodeTask): ValidateSmsCodeTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindFinalizeAddingThreePidTask(task: DefaultFinalizeAddingThreePidTask): FinalizeAddingThreePidTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteThreePidTask(task: DefaultDeleteThreePidTask): DeleteThreePidTask
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.internal.auth.registration.SuccessResult
|
||||
import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface ValidateSmsCodeTask : Task<ValidateSmsCodeTask.Params, Unit> {
|
||||
data class Params(
|
||||
val threePid: ThreePid.Msisdn,
|
||||
val code: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultValidateSmsCodeTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
@SessionDatabase
|
||||
private val monarchy: Monarchy,
|
||||
private val pendingThreePidMapper: PendingThreePidMapper,
|
||||
private val eventBus: EventBus
|
||||
) : ValidateSmsCodeTask {
|
||||
|
||||
override suspend fun execute(params: ValidateSmsCodeTask.Params) {
|
||||
// Search the pending ThreePid
|
||||
val pendingThreePids = monarchy.fetchAllMappedSync(
|
||||
{ it.where(PendingThreePidEntity::class.java) },
|
||||
{ pendingThreePidMapper.map(it) }
|
||||
)
|
||||
.firstOrNull { it.threePid == params.threePid }
|
||||
?: throw IllegalArgumentException("unknown threepid")
|
||||
|
||||
val url = pendingThreePids.submitUrl ?: throw IllegalArgumentException("invalid threepid")
|
||||
val body = ValidationCodeBody(
|
||||
clientSecret = pendingThreePids.clientSecret,
|
||||
sid = pendingThreePids.sid,
|
||||
code = params.code
|
||||
)
|
||||
val result = executeRequest<SuccessResult>(eventBus) {
|
||||
apiCall = profileAPI.validateMsisdn(url, body)
|
||||
}
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
throw Failure.SuccessError
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<template
|
||||
format="5"
|
||||
revision="1"
|
||||
name="RiotX Feature"
|
||||
name="Element Feature"
|
||||
minApi="19"
|
||||
minBuildApi="19"
|
||||
description="Creates a new activity and a fragment with view model, view state and actions">
|
|
@ -16,10 +16,10 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
echo "Configure RiotX Template..."
|
||||
echo "Configure Element Template..."
|
||||
if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi
|
||||
{
|
||||
ln -s $(pwd)/RiotXFeature "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
|
||||
ln -s $(pwd)/ElementFeature "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
|
||||
} && {
|
||||
echo "Please restart Android Studio."
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ import im.vector.app.features.settings.ignored.VectorSettingsIgnoredUsersFragmen
|
|||
import im.vector.app.features.settings.locale.LocalePickerFragment
|
||||
import im.vector.app.features.settings.push.PushGatewaysFragment
|
||||
import im.vector.app.features.settings.push.PushRulesFragment
|
||||
import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment
|
||||
import im.vector.app.features.share.IncomingShareFragment
|
||||
import im.vector.app.features.signout.soft.SoftLogoutFragment
|
||||
import im.vector.app.features.terms.ReviewTermsFragment
|
||||
|
@ -313,6 +314,11 @@ interface FragmentModule {
|
|||
@FragmentKey(VectorSettingsDevicesFragment::class)
|
||||
fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(ThreePidsSettingsFragment::class)
|
||||
fun bindThreePidsSettingsFragment(fragment: ThreePidsSettingsFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(PublicRoomsFragment::class)
|
||||
|
|
|
@ -59,35 +59,46 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
}
|
||||
is Failure.ServerError -> {
|
||||
when {
|
||||
throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> {
|
||||
throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> {
|
||||
// Special case for terms and conditions
|
||||
stringProvider.getString(R.string.error_terms_not_accepted)
|
||||
}
|
||||
throwable.isInvalidPassword() -> {
|
||||
throwable.isInvalidPassword() -> {
|
||||
stringProvider.getString(R.string.auth_invalid_login_param)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_USER_IN_USE -> {
|
||||
throwable.error.code == MatrixError.M_USER_IN_USE -> {
|
||||
stringProvider.getString(R.string.login_signup_error_user_in_use)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_BAD_JSON -> {
|
||||
throwable.error.code == MatrixError.M_BAD_JSON -> {
|
||||
stringProvider.getString(R.string.login_error_bad_json)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_NOT_JSON -> {
|
||||
throwable.error.code == MatrixError.M_NOT_JSON -> {
|
||||
stringProvider.getString(R.string.login_error_not_json)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_THREEPID_DENIED -> {
|
||||
throwable.error.code == MatrixError.M_THREEPID_DENIED -> {
|
||||
stringProvider.getString(R.string.login_error_threepid_denied)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
|
||||
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
|
||||
limitExceededError(throwable.error)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
|
||||
throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
|
||||
stringProvider.getString(R.string.login_reset_password_error_not_found)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_USER_DEACTIVATED -> {
|
||||
throwable.error.code == MatrixError.M_USER_DEACTIVATED -> {
|
||||
stringProvider.getString(R.string.auth_invalid_login_deactivated_account)
|
||||
}
|
||||
else -> {
|
||||
throwable.error.code == MatrixError.M_THREEPID_IN_USE
|
||||
&& throwable.error.message == "Email is already in use" -> {
|
||||
stringProvider.getString(R.string.account_email_already_used_error)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_THREEPID_IN_USE
|
||||
&& throwable.error.message == "MSISDN is already in use" -> {
|
||||
stringProvider.getString(R.string.account_phone_number_already_used_error)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> {
|
||||
stringProvider.getString(R.string.error_threepid_auth_failed)
|
||||
}
|
||||
else -> {
|
||||
throwable.error.message.takeIf { it.isNotEmpty() }
|
||||
?: throwable.error.code.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
@ -102,6 +113,7 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
throwable.localizedMessage
|
||||
}
|
||||
}
|
||||
is SsoFlowNotSupportedYet -> stringProvider.getString(R.string.error_sso_flow_not_supported_yet)
|
||||
else -> throwable.localizedMessage
|
||||
}
|
||||
?: stringProvider.getString(R.string.unknown_error)
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.app.core.error
|
||||
|
||||
class SsoFlowNotSupportedYet : Throwable()
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.app.core.extensions
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import org.matrix.android.sdk.api.extensions.ensurePrefix
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
|
||||
fun ThreePid.getFormattedValue(): String {
|
||||
return when (this) {
|
||||
is ThreePid.Email -> email
|
||||
is ThreePid.Msisdn -> {
|
||||
tryThis(message = "Unable to parse the phone number") {
|
||||
PhoneNumberUtil.getInstance().parse(msisdn.ensurePrefix("+"), null)
|
||||
}
|
||||
?.let {
|
||||
PhoneNumberUtil.getInstance().format(it, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
|
||||
}
|
||||
?: msisdn
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import im.vector.app.R
|
|||
import im.vector.app.core.extensions.vectorComponent
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import org.matrix.android.sdk.api.session.user.model.User
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
|
||||
class UserAvatarPreference : Preference {
|
||||
|
@ -34,6 +35,8 @@ class UserAvatarPreference : Preference {
|
|||
|
||||
private var avatarRenderer: AvatarRenderer = context.vectorComponent().avatarRenderer()
|
||||
|
||||
private var userItem: MatrixItem.UserItem? = null
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
@ -50,9 +53,16 @@ class UserAvatarPreference : Preference {
|
|||
super.onBindViewHolder(holder)
|
||||
mAvatarView = holder.itemView.findViewById(R.id.settings_avatar)
|
||||
mLoadingProgressBar = holder.itemView.findViewById(R.id.avatar_update_progress_bar)
|
||||
refreshUi()
|
||||
}
|
||||
|
||||
fun refreshAvatar(user: User) {
|
||||
mAvatarView?.let { avatarRenderer.render(user.toMatrixItem(), it) }
|
||||
userItem = user.toMatrixItem()
|
||||
refreshUi()
|
||||
}
|
||||
|
||||
private fun refreshUi() {
|
||||
val safeUserItem = userItem ?: return
|
||||
mAvatarView?.let { avatarRenderer.render(safeUserItem, it) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,9 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
|
|||
@EpoxyAttribute
|
||||
var buttonAction: Action? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var destructiveButtonAction: Action? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var itemClickAction: Action? = null
|
||||
|
||||
|
@ -109,6 +112,11 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
|
|||
buttonAction?.perform?.run()
|
||||
}
|
||||
|
||||
holder.destructiveButton.setTextOrHide(destructiveButtonAction?.title)
|
||||
holder.destructiveButton.setOnClickListener {
|
||||
destructiveButtonAction?.perform?.run()
|
||||
}
|
||||
|
||||
holder.root.setOnClickListener {
|
||||
itemClickAction?.perform?.run()
|
||||
}
|
||||
|
@ -122,5 +130,6 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
|
|||
val accessoryImage by bind<ImageView>(R.id.item_generic_accessory_image)
|
||||
val progressBar by bind<ProgressBar>(R.id.item_generic_progress_bar)
|
||||
val actionButton by bind<Button>(R.id.item_generic_action_button)
|
||||
val destructiveButton by bind<Button>(R.id.item_generic_destructive_action_button)
|
||||
}
|
||||
}
|
||||
|
|
45
vector/src/main/java/im/vector/app/core/utils/ReadOnce.kt
Normal file
45
vector/src/main/java/im/vector/app/core/utils/ReadOnce.kt
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.app.core.utils
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Use this container to read a value only once
|
||||
*/
|
||||
class ReadOnce<T>(
|
||||
private val value: T
|
||||
) {
|
||||
private val valueHasBeenRead = AtomicBoolean(false)
|
||||
|
||||
fun get(): T? {
|
||||
return if (valueHasBeenRead.getAndSet(true)) {
|
||||
null
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the first call to isTrue() will return true
|
||||
*/
|
||||
class ReadOnceTrue {
|
||||
private val readOnce = ReadOnce(true)
|
||||
|
||||
fun isTrue() = readOnce.get() == true
|
||||
}
|
|
@ -23,19 +23,18 @@ import com.airbnb.mvrx.Incomplete
|
|||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.attributes.ButtonStyle
|
||||
import im.vector.app.core.epoxy.attributes.ButtonType
|
||||
import im.vector.app.core.epoxy.attributes.IconMode
|
||||
import im.vector.app.core.epoxy.loadingItem
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.extensions.getFormattedValue
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.identity.SharedState
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
|
@ -235,16 +234,7 @@ class DiscoverySettingsController @Inject constructor(
|
|||
}
|
||||
|
||||
private fun buildMsisdn(pidInfo: PidInfo) {
|
||||
val phoneNumber = try {
|
||||
PhoneNumberUtil.getInstance().parse("+${pidInfo.threePid.value}", null)
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "Unable to parse the phone number")
|
||||
null
|
||||
}
|
||||
?.let {
|
||||
PhoneNumberUtil.getInstance().format(it, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
|
||||
}
|
||||
?: pidInfo.threePid.value
|
||||
val phoneNumber = pidInfo.threePid.getFormattedValue()
|
||||
|
||||
buildThreePid(pidInfo, phoneNumber)
|
||||
|
||||
|
@ -277,8 +267,8 @@ class DiscoverySettingsController @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCodeChange(code: String) {
|
||||
codes[pidInfo.threePid] = code
|
||||
override fun onTextChange(text: String) {
|
||||
codes[pidInfo.threePid] = text
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -341,25 +331,22 @@ class DiscoverySettingsController @Inject constructor(
|
|||
private fun buildContinueCancel(threePid: ThreePid) {
|
||||
settingsContinueCancelItem {
|
||||
id("bottom${threePid.value}")
|
||||
interactionListener(object : SettingsContinueCancelItem.Listener {
|
||||
override fun onContinue() {
|
||||
when (threePid) {
|
||||
is ThreePid.Email -> {
|
||||
listener?.checkEmailVerification(threePid)
|
||||
}
|
||||
is ThreePid.Msisdn -> {
|
||||
val code = codes[threePid]
|
||||
if (code != null) {
|
||||
listener?.sendMsisdnVerificationCode(threePid, code)
|
||||
}
|
||||
continueOnClick {
|
||||
when (threePid) {
|
||||
is ThreePid.Email -> {
|
||||
listener?.checkEmailVerification(threePid)
|
||||
}
|
||||
is ThreePid.Msisdn -> {
|
||||
val code = codes[threePid]
|
||||
if (code != null) {
|
||||
listener?.sendMsisdnVerificationCode(threePid, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
listener?.cancelBinding(threePid)
|
||||
}
|
||||
})
|
||||
}
|
||||
cancelOnClick {
|
||||
listener?.cancelBinding(threePid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,33 +20,28 @@ import com.airbnb.epoxy.EpoxyAttribute
|
|||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_settings_continue_cancel)
|
||||
abstract class SettingsContinueCancelItem : EpoxyModelWithHolder<SettingsContinueCancelItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var interactionListener: Listener? = null
|
||||
var continueOnClick: ClickListener? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var cancelOnClick: ClickListener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
holder.cancelButton.setOnClickListener {
|
||||
interactionListener?.onCancel()
|
||||
}
|
||||
|
||||
holder.continueButton.setOnClickListener {
|
||||
interactionListener?.onContinue()
|
||||
}
|
||||
holder.cancelButton.onClick(cancelOnClick)
|
||||
holder.continueButton.onClick(continueOnClick)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val cancelButton by bind<Button>(R.id.settings_item_cancel_button)
|
||||
val continueButton by bind<Button>(R.id.settings_item_continue_button)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onContinue()
|
||||
fun onCancel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,19 +27,24 @@ import com.google.android.material.textfield.TextInputLayout
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.core.extensions.showKeyboard
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_settings_edit_text)
|
||||
abstract class SettingsEditTextItem : EpoxyModelWithHolder<SettingsEditTextItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute var hint: String? = null
|
||||
@EpoxyAttribute var value: String? = null
|
||||
@EpoxyAttribute var requestFocus = false
|
||||
@EpoxyAttribute var descriptionText: String? = null
|
||||
@EpoxyAttribute var errorText: String? = null
|
||||
@EpoxyAttribute var inProgress: Boolean = false
|
||||
@EpoxyAttribute var inputType: Int? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var interactionListener: Listener? = null
|
||||
|
||||
private val textChangeListener: (text: CharSequence?, start: Int, count: Int, after: Int) -> Unit = { code, _, _, _ ->
|
||||
code?.let { interactionListener?.onCodeChange(it.toString()) }
|
||||
private val textChangeListener: (text: CharSequence?, start: Int, count: Int, after: Int) -> Unit = { text, _, _, _ ->
|
||||
text?.let { interactionListener?.onTextChange(it.toString()) }
|
||||
}
|
||||
|
||||
private val editorActionListener = object : TextView.OnEditorActionListener {
|
||||
|
@ -63,9 +68,17 @@ abstract class SettingsEditTextItem : EpoxyModelWithHolder<SettingsEditTextItem.
|
|||
} else {
|
||||
holder.textInputLayout.error = errorText
|
||||
}
|
||||
holder.textInputLayout.hint = hint
|
||||
inputType?.let { holder.editText.inputType = it }
|
||||
|
||||
holder.editText.doOnTextChanged(textChangeListener)
|
||||
holder.editText.setOnEditorActionListener(editorActionListener)
|
||||
if (value != null) {
|
||||
holder.editText.setText(value)
|
||||
}
|
||||
if (requestFocus) {
|
||||
holder.editText.showKeyboard(andRequestFocus = true)
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
|
@ -76,6 +89,6 @@ abstract class SettingsEditTextItem : EpoxyModelWithHolder<SettingsEditTextItem.
|
|||
|
||||
interface Listener {
|
||||
fun onValidate()
|
||||
fun onCodeChange(code: String)
|
||||
fun onTextChange(text: String)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
package im.vector.app.features.discovery
|
||||
|
||||
import android.view.View
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
|
@ -69,6 +69,6 @@ abstract class SettingsItem : EpoxyModelWithHolder<SettingsItem.Holder>() {
|
|||
class Holder : VectorEpoxyHolder() {
|
||||
val titleText by bind<TextView>(R.id.settings_item_title)
|
||||
val descriptionText by bind<TextView>(R.id.settings_item_description)
|
||||
val switchButton by bind<Switch>(R.id.settings_item_switch)
|
||||
val switchButton by bind<SwitchMaterial>(R.id.settings_item_switch)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.app.features.discovery
|
|||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -27,6 +26,7 @@ import androidx.core.view.isVisible
|
|||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
|
@ -160,7 +160,7 @@ abstract class SettingsTextButtonSingleLineItem : EpoxyModelWithHolder<SettingsT
|
|||
class Holder : VectorEpoxyHolder() {
|
||||
val textView by bind<TextView>(R.id.settings_item_text)
|
||||
val mainButton by bind<Button>(R.id.settings_item_button)
|
||||
val switchButton by bind<Switch>(R.id.settings_item_switch)
|
||||
val switchButton by bind<SwitchMaterial>(R.id.settings_item_switch)
|
||||
val progress by bind<ProgressBar>(R.id.settings_item_progress)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
package im.vector.app.features.form
|
||||
|
||||
import android.text.Editable
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
|
@ -35,9 +37,18 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
|
|||
@EpoxyAttribute
|
||||
var value: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var showBottomSeparator: Boolean = true
|
||||
|
||||
@EpoxyAttribute
|
||||
var errorMessage: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var enabled: Boolean = true
|
||||
|
||||
@EpoxyAttribute
|
||||
var inputType: Int? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var onTextChange: ((String) -> Unit)? = null
|
||||
|
||||
|
@ -51,14 +62,17 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
|
|||
super.bind(holder)
|
||||
holder.textInputLayout.isEnabled = enabled
|
||||
holder.textInputLayout.hint = hint
|
||||
holder.textInputLayout.error = errorMessage
|
||||
|
||||
// Update only if text is different
|
||||
if (holder.textInputEditText.text.toString() != value) {
|
||||
// Update only if text is different and value is not null
|
||||
if (value != null && holder.textInputEditText.text.toString() != value) {
|
||||
holder.textInputEditText.setText(value)
|
||||
}
|
||||
holder.textInputEditText.isEnabled = enabled
|
||||
inputType?.let { holder.textInputEditText.inputType = it }
|
||||
|
||||
holder.textInputEditText.addTextChangedListener(onTextChangeListener)
|
||||
holder.bottomSeparator.isVisible = showBottomSeparator
|
||||
}
|
||||
|
||||
override fun shouldSaveViewState(): Boolean {
|
||||
|
@ -73,5 +87,6 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
|
|||
class Holder : VectorEpoxyHolder() {
|
||||
val textInputLayout by bind<TextInputLayout>(R.id.formTextInputTextInputLayout)
|
||||
val textInputEditText by bind<TextInputEditText>(R.id.formTextInputTextInputEditText)
|
||||
val bottomSeparator by bind<View>(R.id.formTextInputDivider)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,13 +23,11 @@ import android.content.Intent
|
|||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.util.Patterns
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.EditTextPreference
|
||||
|
@ -54,13 +52,11 @@ import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
|
|||
import im.vector.app.core.utils.TextUtils
|
||||
import im.vector.app.core.utils.allGranted
|
||||
import im.vector.app.core.utils.checkPermissions
|
||||
import im.vector.app.core.utils.copyToClipboard
|
||||
import im.vector.app.core.utils.getSizeOfFiles
|
||||
import im.vector.app.core.utils.toast
|
||||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.MainActivityArgs
|
||||
import im.vector.app.features.media.createUCropWithDefaultSettings
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.app.features.workers.signout.SignOutUiWorker
|
||||
import im.vector.lib.multipicker.MultiPicker
|
||||
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
||||
|
@ -187,44 +183,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||
mPasswordPreference.isVisible = false
|
||||
}
|
||||
|
||||
// Add Email
|
||||
findPreference<EditTextPreference>(ADD_EMAIL_PREFERENCE_KEY)!!.let {
|
||||
// It does not work on XML, do it here
|
||||
it.icon = activity?.let {
|
||||
ThemeUtils.tintDrawable(it,
|
||||
ContextCompat.getDrawable(it, R.drawable.ic_material_add)!!, R.attr.colorAccent)
|
||||
}
|
||||
|
||||
// Unfortunately, this is not supported in lib v7
|
||||
// it.editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
||||
it.setOnPreferenceClickListener {
|
||||
notImplemented()
|
||||
true
|
||||
}
|
||||
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
notImplemented()
|
||||
// addEmail((newValue as String).trim())
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Add phone number
|
||||
findPreference<VectorPreference>(ADD_PHONE_NUMBER_PREFERENCE_KEY)!!.let {
|
||||
// It does not work on XML, do it here
|
||||
it.icon = activity?.let {
|
||||
ThemeUtils.tintDrawable(it,
|
||||
ContextCompat.getDrawable(it, R.drawable.ic_material_add)!!, R.attr.colorAccent)
|
||||
}
|
||||
|
||||
it.setOnPreferenceClickListener {
|
||||
notImplemented()
|
||||
// TODO val intent = PhoneNumberAdditionActivity.getIntent(activity, session.credentials.userId)
|
||||
// startActivityForResult(intent, REQUEST_NEW_PHONE_NUMBER)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// Advanced settings
|
||||
|
||||
// user account
|
||||
|
@ -235,8 +193,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_HOME_SERVER_PREFERENCE_KEY)!!
|
||||
.summary = session.sessionParams.homeServerUrl
|
||||
|
||||
refreshEmailsList()
|
||||
refreshPhoneNumbersList()
|
||||
// Contacts
|
||||
setContactsPreferences()
|
||||
|
||||
|
@ -533,295 +489,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||
* Refresh phone number list
|
||||
*/
|
||||
private fun refreshPhoneNumbersList() {
|
||||
/* TODO
|
||||
val currentPhoneNumber3PID = ArrayList(session.myUser.getlinkedPhoneNumbers())
|
||||
|
||||
val phoneNumberList = ArrayList<String>()
|
||||
for (identifier in currentPhoneNumber3PID) {
|
||||
phoneNumberList.add(identifier.address)
|
||||
}
|
||||
|
||||
// check first if there is an update
|
||||
var isNewList = true
|
||||
if (phoneNumberList.size == mDisplayedPhoneNumber.size) {
|
||||
isNewList = !mDisplayedPhoneNumber.containsAll(phoneNumberList)
|
||||
}
|
||||
|
||||
if (isNewList) {
|
||||
// remove the displayed one
|
||||
run {
|
||||
var index = 0
|
||||
while (true) {
|
||||
val preference = mUserSettingsCategory.findPreference(PHONE_NUMBER_PREFERENCE_KEY_BASE + index)
|
||||
|
||||
if (null != preference) {
|
||||
mUserSettingsCategory.removePreference(preference)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
// add new phone number list
|
||||
mDisplayedPhoneNumber = phoneNumberList
|
||||
|
||||
val addPhoneBtn = mUserSettingsCategory.findPreference(ADD_PHONE_NUMBER_PREFERENCE_KEY)
|
||||
?: return
|
||||
|
||||
var order = addPhoneBtn.order
|
||||
|
||||
for ((index, phoneNumber3PID) in currentPhoneNumber3PID.withIndex()) {
|
||||
val preference = VectorPreference(activity!!)
|
||||
|
||||
preference.title = getString(R.string.settings_phone_number)
|
||||
var phoneNumberFormatted = phoneNumber3PID.address
|
||||
try {
|
||||
// Attempt to format phone number
|
||||
val phoneNumber = PhoneNumberUtil.getInstance().parse("+$phoneNumberFormatted", null)
|
||||
phoneNumberFormatted = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
|
||||
} catch (e: NumberParseException) {
|
||||
// Do nothing, we will display raw version
|
||||
}
|
||||
|
||||
preference.summary = phoneNumberFormatted
|
||||
preference.key = PHONE_NUMBER_PREFERENCE_KEY_BASE + index
|
||||
preference.order = order
|
||||
|
||||
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
displayDelete3PIDConfirmationDialog(phoneNumber3PID, preference.summary)
|
||||
true
|
||||
}
|
||||
|
||||
preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
|
||||
override fun onPreferenceLongClick(preference: Preference): Boolean {
|
||||
activity?.let { copyToClipboard(it, phoneNumber3PID.address) }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
order++
|
||||
mUserSettingsCategory.addPreference(preference)
|
||||
}
|
||||
|
||||
addPhoneBtn.order = order
|
||||
} */
|
||||
}
|
||||
|
||||
// ==============================================================================================================
|
||||
// Email management
|
||||
// ==============================================================================================================
|
||||
|
||||
/**
|
||||
* Refresh the emails list
|
||||
*/
|
||||
private fun refreshEmailsList() {
|
||||
val currentEmail3PID = emptyList<String>() // TODO ArrayList(session.myUser.getlinkedEmails())
|
||||
|
||||
val newEmailsList = ArrayList<String>()
|
||||
for (identifier in currentEmail3PID) {
|
||||
// TODO newEmailsList.add(identifier.address)
|
||||
}
|
||||
|
||||
// check first if there is an update
|
||||
var isNewList = true
|
||||
if (newEmailsList.size == mDisplayedEmails.size) {
|
||||
isNewList = !mDisplayedEmails.containsAll(newEmailsList)
|
||||
}
|
||||
|
||||
if (isNewList) {
|
||||
// remove the displayed one
|
||||
run {
|
||||
var index = 0
|
||||
while (true) {
|
||||
val preference = mUserSettingsCategory.findPreference<VectorPreference>(EMAIL_PREFERENCE_KEY_BASE + index)
|
||||
|
||||
if (null != preference) {
|
||||
mUserSettingsCategory.removePreference(preference)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
// add new emails list
|
||||
mDisplayedEmails = newEmailsList
|
||||
|
||||
val addEmailBtn = mUserSettingsCategory.findPreference<VectorPreference>(ADD_EMAIL_PREFERENCE_KEY) ?: return
|
||||
|
||||
var order = addEmailBtn.order
|
||||
|
||||
for ((index, email3PID) in currentEmail3PID.withIndex()) {
|
||||
val preference = VectorPreference(requireActivity())
|
||||
|
||||
preference.title = getString(R.string.settings_email_address)
|
||||
preference.summary = "TODO" // email3PID.address
|
||||
preference.key = EMAIL_PREFERENCE_KEY_BASE + index
|
||||
preference.order = order
|
||||
|
||||
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { pref ->
|
||||
displayDelete3PIDConfirmationDialog(/* TODO email3PID, */ pref.summary)
|
||||
true
|
||||
}
|
||||
|
||||
preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
|
||||
override fun onPreferenceLongClick(preference: Preference): Boolean {
|
||||
activity?.let { copyToClipboard(it, "TODO") } // email3PID.address) }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
mUserSettingsCategory.addPreference(preference)
|
||||
|
||||
order++
|
||||
}
|
||||
|
||||
addEmailBtn.order = order
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to add a new email to the account
|
||||
*
|
||||
* @param email the email to add.
|
||||
*/
|
||||
private fun addEmail(email: String) {
|
||||
// check first if the email syntax is valid
|
||||
// if email is null , then also its invalid email
|
||||
if (email.isBlank() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
|
||||
activity?.toast(R.string.auth_invalid_email)
|
||||
return
|
||||
}
|
||||
|
||||
// check first if the email syntax is valid
|
||||
if (mDisplayedEmails.indexOf(email) >= 0) {
|
||||
activity?.toast(R.string.auth_email_already_defined)
|
||||
return
|
||||
}
|
||||
|
||||
notImplemented()
|
||||
/* TODO
|
||||
val pid = ThreePid(email, ThreePid.MEDIUM_EMAIL)
|
||||
|
||||
displayLoadingView()
|
||||
|
||||
session.myUser.requestEmailValidationToken(pid, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(info: Void?) {
|
||||
activity?.runOnUiThread { showEmailValidationDialog(pid) }
|
||||
}
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
|
||||
override fun onMatrixError(e: MatrixError) {
|
||||
if (TextUtils.equals(MatrixError.THREEPID_IN_USE, e.errcode)) {
|
||||
onCommonDone(getString(R.string.account_email_already_used_error))
|
||||
} else {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUnexpectedError(e: Exception) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an email validation dialog to warn the user tho valid his email link.
|
||||
*
|
||||
* @param pid the used pid.
|
||||
*/
|
||||
/* TODO
|
||||
private fun showEmailValidationDialog(pid: ThreePid) {
|
||||
activity?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setTitle(R.string.account_email_validation_title)
|
||||
.setMessage(R.string.account_email_validation_message)
|
||||
.setPositiveButton(R.string._continue) { _, _ ->
|
||||
session.myUser.add3Pid(pid, true, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(info: Void?) {
|
||||
it.runOnUiThread {
|
||||
hideLoadingView()
|
||||
refreshEmailsList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
|
||||
override fun onMatrixError(e: MatrixError) {
|
||||
if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) {
|
||||
it.runOnUiThread {
|
||||
hideLoadingView()
|
||||
it.toast(R.string.account_email_validation_error)
|
||||
}
|
||||
} else {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUnexpectedError(e: Exception) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
hideLoadingView()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
} */
|
||||
|
||||
/**
|
||||
* Display a dialog which asks confirmation for the deletion of a 3pid
|
||||
*
|
||||
* @param pid the 3pid to delete
|
||||
* @param preferenceSummary the displayed 3pid
|
||||
*/
|
||||
private fun displayDelete3PIDConfirmationDialog(/* TODO pid: ThirdPartyIdentifier,*/ preferenceSummary: CharSequence) {
|
||||
val mediumFriendlyName = "TODO" // ThreePid.getMediumFriendlyName(pid.medium, activity).toLowerCase(VectorLocale.applicationLocale)
|
||||
val dialogMessage = getString(R.string.settings_delete_threepid_confirmation, mediumFriendlyName, preferenceSummary)
|
||||
|
||||
activity?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setTitle(R.string.dialog_title_confirmation)
|
||||
.setMessage(dialogMessage)
|
||||
.setPositiveButton(R.string.remove) { _, _ ->
|
||||
notImplemented()
|
||||
/* TODO
|
||||
displayLoadingView()
|
||||
|
||||
session.myUser.delete3Pid(pid, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(info: Void?) {
|
||||
when (pid.medium) {
|
||||
ThreePid.MEDIUM_EMAIL -> refreshEmailsList()
|
||||
ThreePid.MEDIUM_MSISDN -> refreshPhoneNumbersList()
|
||||
}
|
||||
onCommonDone(null)
|
||||
}
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
|
||||
override fun onMatrixError(e: MatrixError) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
|
||||
override fun onUnexpectedError(e: Exception) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -985,12 +652,6 @@ private fun showEmailValidationDialog(pid: ThreePid) {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val ADD_EMAIL_PREFERENCE_KEY = "ADD_EMAIL_PREFERENCE_KEY"
|
||||
private const val ADD_PHONE_NUMBER_PREFERENCE_KEY = "ADD_PHONE_NUMBER_PREFERENCE_KEY"
|
||||
|
||||
private const val EMAIL_PREFERENCE_KEY_BASE = "EMAIL_PREFERENCE_KEY_BASE"
|
||||
private const val PHONE_NUMBER_PREFERENCE_KEY_BASE = "PHONE_NUMBER_PREFERENCE_KEY_BASE"
|
||||
|
||||
private const val REQUEST_NEW_PHONE_NUMBER = 456
|
||||
private const val REQUEST_PHONEBOOK_COUNTRY = 789
|
||||
}
|
||||
|
|
|
@ -28,6 +28,12 @@ import com.airbnb.mvrx.Uninitialized
|
|||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.error.SsoFlowNotSupportedYet
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
|
@ -41,11 +47,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
|
|||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -309,14 +310,14 @@ class DevicesViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
if (!isPasswordRequestFound) {
|
||||
// LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far...
|
||||
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
|
||||
setState {
|
||||
copy(
|
||||
request = Fail(failure)
|
||||
)
|
||||
}
|
||||
|
||||
_viewEvents.post(DevicesViewEvents.Failure(failure))
|
||||
_viewEvents.post(DevicesViewEvents.Failure(SsoFlowNotSupportedYet()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.app.features.settings.threepids
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_settings_three_pid)
|
||||
abstract class ThreePidItem : EpoxyModelWithHolder<ThreePidItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var title: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
@DrawableRes
|
||||
var iconResId: Int? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var deleteClickListener: ClickListener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
val safeIconResId = iconResId
|
||||
if (safeIconResId != null) {
|
||||
holder.icon.isVisible = true
|
||||
holder.icon.setImageResource(safeIconResId)
|
||||
} else {
|
||||
holder.icon.isVisible = false
|
||||
}
|
||||
|
||||
holder.title.text = title
|
||||
holder.delete.onClick { deleteClickListener?.invoke() }
|
||||
holder.delete.isVisible = deleteClickListener != null
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val icon by bind<ImageView>(R.id.item_settings_three_pid_icon)
|
||||
val title by bind<TextView>(R.id.item_settings_three_pid_title)
|
||||
val delete by bind<View>(R.id.item_settings_three_pid_delete)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.app.features.settings.threepids
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
|
||||
sealed class ThreePidsSettingsAction : VectorViewModelAction {
|
||||
data class ChangeUiState(val newUiState: ThreePidsSettingsUiState) : ThreePidsSettingsAction()
|
||||
data class AddThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
||||
data class SubmitCode(val threePid: ThreePid.Msisdn, val code: String) : ThreePidsSettingsAction()
|
||||
data class ContinueThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
||||
data class CancelThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
||||
data class AccountPassword(val password: String) : ThreePidsSettingsAction()
|
||||
data class DeleteThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
* 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.app.features.settings.threepids
|
||||
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.loadingItem
|
||||
import im.vector.app.core.epoxy.noResultItem
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.extensions.getFormattedValue
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.ui.list.genericButtonItem
|
||||
import im.vector.app.core.ui.list.genericFooterItem
|
||||
import im.vector.app.features.discovery.SettingsEditTextItem
|
||||
import im.vector.app.features.discovery.settingsContinueCancelItem
|
||||
import im.vector.app.features.discovery.settingsEditTextItem
|
||||
import im.vector.app.features.discovery.settingsInfoItem
|
||||
import im.vector.app.features.discovery.settingsInformationItem
|
||||
import im.vector.app.features.discovery.settingsSectionTitleItem
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import javax.inject.Inject
|
||||
|
||||
class ThreePidsSettingsController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val errorFormatter: ErrorFormatter
|
||||
) : TypedEpoxyController<ThreePidsSettingsViewState>() {
|
||||
|
||||
interface InteractionListener {
|
||||
fun addEmail()
|
||||
fun addMsisdn()
|
||||
fun cancelAdding()
|
||||
fun doAddEmail(email: String)
|
||||
fun doAddMsisdn(msisdn: String)
|
||||
fun submitCode(threePid: ThreePid.Msisdn, code: String)
|
||||
fun continueThreePid(threePid: ThreePid)
|
||||
fun cancelThreePid(threePid: ThreePid)
|
||||
fun deleteThreePid(threePid: ThreePid)
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
// For phone number or email (exclusive)
|
||||
private var currentInputValue = ""
|
||||
|
||||
// For validation code
|
||||
private val currentCodes = mutableMapOf<ThreePid, String>()
|
||||
|
||||
override fun buildModels(data: ThreePidsSettingsViewState?) {
|
||||
if (data == null) return
|
||||
|
||||
if (data.uiState is ThreePidsSettingsUiState.Idle) {
|
||||
currentInputValue = ""
|
||||
}
|
||||
|
||||
when (data.threePids) {
|
||||
is Loading -> {
|
||||
loadingItem {
|
||||
id("loading")
|
||||
loadingText(stringProvider.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
genericFooterItem {
|
||||
id("fail")
|
||||
text(data.threePids.error.localizedMessage)
|
||||
}
|
||||
}
|
||||
is Success -> {
|
||||
val dataList = data.threePids.invoke()
|
||||
buildThreePids(dataList, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildThreePids(list: List<ThreePid>, data: ThreePidsSettingsViewState) {
|
||||
val splited = list.groupBy { it is ThreePid.Email }
|
||||
val emails = splited[true].orEmpty()
|
||||
val msisdn = splited[false].orEmpty()
|
||||
|
||||
settingsSectionTitleItem {
|
||||
id("email")
|
||||
title(stringProvider.getString(R.string.settings_emails))
|
||||
}
|
||||
|
||||
emails.forEach { buildThreePid("email ", it) }
|
||||
|
||||
// Pending emails
|
||||
data.pendingThreePids.invoke()
|
||||
?.filterIsInstance(ThreePid.Email::class.java)
|
||||
.orEmpty()
|
||||
.let { pendingList ->
|
||||
if (pendingList.isEmpty() && emails.isEmpty()) {
|
||||
noResultItem {
|
||||
id("noEmail")
|
||||
text(stringProvider.getString(R.string.settings_emails_empty))
|
||||
}
|
||||
}
|
||||
|
||||
pendingList.forEach { buildPendingThreePid(data, "p_email ", it) }
|
||||
}
|
||||
|
||||
when (data.uiState) {
|
||||
ThreePidsSettingsUiState.Idle ->
|
||||
genericButtonItem {
|
||||
id("addEmail")
|
||||
text(stringProvider.getString(R.string.settings_add_email_address))
|
||||
textColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
buttonClickAction(View.OnClickListener { interactionListener?.addEmail() })
|
||||
}
|
||||
is ThreePidsSettingsUiState.AddingEmail -> {
|
||||
settingsEditTextItem {
|
||||
id("addingEmail")
|
||||
inputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS)
|
||||
hint(stringProvider.getString(R.string.medium_email))
|
||||
if (data.editTextReinitiator.isTrue()) {
|
||||
value("")
|
||||
requestFocus(true)
|
||||
}
|
||||
errorText(data.uiState.error)
|
||||
interactionListener(object : SettingsEditTextItem.Listener {
|
||||
override fun onValidate() {
|
||||
interactionListener?.doAddEmail(currentInputValue)
|
||||
}
|
||||
|
||||
override fun onTextChange(text: String) {
|
||||
currentInputValue = text
|
||||
}
|
||||
})
|
||||
}
|
||||
settingsContinueCancelItem {
|
||||
id("contAddingEmail")
|
||||
continueOnClick { interactionListener?.doAddEmail(currentInputValue) }
|
||||
cancelOnClick { interactionListener?.cancelAdding() }
|
||||
}
|
||||
}
|
||||
is ThreePidsSettingsUiState.AddingPhoneNumber -> Unit
|
||||
}.exhaustive
|
||||
|
||||
settingsSectionTitleItem {
|
||||
id("msisdn")
|
||||
title(stringProvider.getString(R.string.settings_phone_numbers))
|
||||
}
|
||||
|
||||
msisdn.forEach { buildThreePid("msisdn ", it) }
|
||||
|
||||
// Pending msisdn
|
||||
data.pendingThreePids.invoke()
|
||||
?.filterIsInstance(ThreePid.Msisdn::class.java)
|
||||
.orEmpty()
|
||||
.let { pendingList ->
|
||||
if (pendingList.isEmpty() && msisdn.isEmpty()) {
|
||||
noResultItem {
|
||||
id("noMsisdn")
|
||||
text(stringProvider.getString(R.string.settings_phone_number_empty))
|
||||
}
|
||||
}
|
||||
|
||||
pendingList.forEach { buildPendingThreePid(data, "p_msisdn ", it) }
|
||||
}
|
||||
|
||||
when (data.uiState) {
|
||||
ThreePidsSettingsUiState.Idle ->
|
||||
genericButtonItem {
|
||||
id("addMsisdn")
|
||||
text(stringProvider.getString(R.string.settings_add_phone_number))
|
||||
textColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
buttonClickAction(View.OnClickListener { interactionListener?.addMsisdn() })
|
||||
}
|
||||
is ThreePidsSettingsUiState.AddingEmail -> Unit
|
||||
is ThreePidsSettingsUiState.AddingPhoneNumber -> {
|
||||
settingsInfoItem {
|
||||
id("addingMsisdnInfo")
|
||||
helperText(stringProvider.getString(R.string.login_msisdn_notice))
|
||||
}
|
||||
settingsEditTextItem {
|
||||
id("addingMsisdn")
|
||||
inputType(InputType.TYPE_CLASS_PHONE)
|
||||
hint(stringProvider.getString(R.string.medium_phone_number))
|
||||
if (data.editTextReinitiator.isTrue()) {
|
||||
value("")
|
||||
requestFocus(true)
|
||||
}
|
||||
errorText(data.uiState.error)
|
||||
interactionListener(object : SettingsEditTextItem.Listener {
|
||||
override fun onValidate() {
|
||||
interactionListener?.doAddMsisdn(currentInputValue)
|
||||
}
|
||||
|
||||
override fun onTextChange(text: String) {
|
||||
currentInputValue = text
|
||||
}
|
||||
})
|
||||
}
|
||||
settingsContinueCancelItem {
|
||||
id("contAddingMsisdn")
|
||||
continueOnClick { interactionListener?.doAddMsisdn(currentInputValue) }
|
||||
cancelOnClick { interactionListener?.cancelAdding() }
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun buildThreePid(idPrefix: String, threePid: ThreePid) {
|
||||
threePidItem {
|
||||
id(idPrefix + threePid.value)
|
||||
// TODO Add an icon for emails
|
||||
// iconResId(if (threePid is ThreePid.Msisdn) R.drawable.ic_phone else null)
|
||||
title(threePid.getFormattedValue())
|
||||
deleteClickListener { interactionListener?.deleteThreePid(threePid) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildPendingThreePid(data: ThreePidsSettingsViewState, idPrefix: String, threePid: ThreePid) {
|
||||
threePidItem {
|
||||
id(idPrefix + threePid.value)
|
||||
// TODO Add an icon for emails
|
||||
// iconResId(if (threePid is ThreePid.Msisdn) R.drawable.ic_phone else null)
|
||||
title(threePid.getFormattedValue())
|
||||
}
|
||||
|
||||
when (threePid) {
|
||||
is ThreePid.Email -> {
|
||||
settingsInformationItem {
|
||||
id("info" + idPrefix + threePid.value)
|
||||
message(stringProvider.getString(R.string.account_email_validation_message))
|
||||
colorProvider(colorProvider)
|
||||
}
|
||||
settingsContinueCancelItem {
|
||||
id("cont" + idPrefix + threePid.value)
|
||||
continueOnClick { interactionListener?.continueThreePid(threePid) }
|
||||
cancelOnClick { interactionListener?.cancelThreePid(threePid) }
|
||||
}
|
||||
}
|
||||
is ThreePid.Msisdn -> {
|
||||
settingsInformationItem {
|
||||
id("info" + idPrefix + threePid.value)
|
||||
message(stringProvider.getString(R.string.settings_text_message_sent, threePid.getFormattedValue()))
|
||||
colorProvider(colorProvider)
|
||||
}
|
||||
settingsEditTextItem {
|
||||
id("msisdnVerification${threePid.value}")
|
||||
inputType(InputType.TYPE_CLASS_NUMBER)
|
||||
hint(stringProvider.getString(R.string.settings_text_message_sent_hint))
|
||||
if (data.msisdnValidationReinitiator[threePid]?.isTrue() == true) {
|
||||
value("")
|
||||
}
|
||||
errorText(getCodeError(data, threePid))
|
||||
interactionListener(object : SettingsEditTextItem.Listener {
|
||||
override fun onValidate() {
|
||||
interactionListener?.submitCode(threePid, currentCodes[threePid] ?: "")
|
||||
}
|
||||
|
||||
override fun onTextChange(text: String) {
|
||||
currentCodes[threePid] = text
|
||||
}
|
||||
})
|
||||
}
|
||||
settingsContinueCancelItem {
|
||||
id("cont" + idPrefix + threePid.value)
|
||||
continueOnClick { interactionListener?.submitCode(threePid, currentCodes[threePid] ?: "") }
|
||||
cancelOnClick { interactionListener?.cancelThreePid(threePid) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCodeError(data: ThreePidsSettingsViewState, threePid: ThreePid.Msisdn): String? {
|
||||
val failure = (data.msisdnValidationRequests[threePid.value] as? Fail)?.error ?: return null
|
||||
// Wrong code?
|
||||
// See https://github.com/matrix-org/synapse/issues/8218
|
||||
return if (failure is Failure.ServerError
|
||||
&& failure.httpCode == 400
|
||||
&& failure.error.code == MatrixError.M_UNKNOWN) {
|
||||
stringProvider.getString(R.string.settings_text_message_sent_wrong_code)
|
||||
} else {
|
||||
errorFormatter.toHumanReadable(failure)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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.app.features.settings.threepids
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.dialogs.PromptPasswordDialog
|
||||
import im.vector.app.core.dialogs.withColoredButton
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.extensions.getFormattedValue
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
import im.vector.app.core.extensions.isMsisdn
|
||||
import im.vector.app.core.platform.OnBackPressed
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import javax.inject.Inject
|
||||
|
||||
class ThreePidsSettingsFragment @Inject constructor(
|
||||
private val viewModelFactory: ThreePidsSettingsViewModel.Factory,
|
||||
private val epoxyController: ThreePidsSettingsController
|
||||
) :
|
||||
VectorBaseFragment(),
|
||||
OnBackPressed,
|
||||
ThreePidsSettingsViewModel.Factory by viewModelFactory,
|
||||
ThreePidsSettingsController.InteractionListener {
|
||||
|
||||
private val viewModel: ThreePidsSettingsViewModel by fragmentViewModel()
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_generic_recycler
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
recyclerView.configureWith(epoxyController)
|
||||
epoxyController.interactionListener = this
|
||||
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable)
|
||||
ThreePidsSettingsViewEvents.RequestPassword -> askUserPassword()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun askUserPassword() {
|
||||
PromptPasswordDialog().show(requireActivity()) { password ->
|
||||
viewModel.handle(ThreePidsSettingsAction.AccountPassword(password))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
recyclerView.cleanup()
|
||||
epoxyController.interactionListener = null
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_emails_and_phone_numbers_title)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
if (state.isLoading) {
|
||||
showLoadingDialog()
|
||||
} else {
|
||||
dismissLoadingDialog()
|
||||
}
|
||||
epoxyController.setData(state)
|
||||
}
|
||||
|
||||
override fun addEmail() {
|
||||
viewModel.handle(ThreePidsSettingsAction.ChangeUiState(ThreePidsSettingsUiState.AddingEmail(null)))
|
||||
}
|
||||
|
||||
override fun doAddEmail(email: String) {
|
||||
// Sanity
|
||||
val safeEmail = email.trim().replace(" ", "")
|
||||
viewModel.handle(ThreePidsSettingsAction.ChangeUiState(ThreePidsSettingsUiState.AddingEmail(null)))
|
||||
|
||||
// Check that email is valid
|
||||
if (!safeEmail.isEmail()) {
|
||||
viewModel.handle(ThreePidsSettingsAction.ChangeUiState(ThreePidsSettingsUiState.AddingEmail(getString(R.string.auth_invalid_email))))
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.handle(ThreePidsSettingsAction.AddThreePid(ThreePid.Email(safeEmail)))
|
||||
}
|
||||
|
||||
override fun addMsisdn() {
|
||||
viewModel.handle(ThreePidsSettingsAction.ChangeUiState(ThreePidsSettingsUiState.AddingPhoneNumber(null)))
|
||||
}
|
||||
|
||||
override fun doAddMsisdn(msisdn: String) {
|
||||
// Sanity
|
||||
val safeMsisdn = msisdn.trim().replace(" ", "")
|
||||
|
||||
viewModel.handle(ThreePidsSettingsAction.ChangeUiState(ThreePidsSettingsUiState.AddingPhoneNumber(null)))
|
||||
|
||||
// Check that phone number is valid
|
||||
if (!msisdn.startsWith("+")) {
|
||||
viewModel.handle(
|
||||
ThreePidsSettingsAction.ChangeUiState(ThreePidsSettingsUiState.AddingPhoneNumber(getString(R.string.login_msisdn_error_not_international)))
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!msisdn.isMsisdn()) {
|
||||
viewModel.handle(
|
||||
ThreePidsSettingsAction.ChangeUiState(ThreePidsSettingsUiState.AddingPhoneNumber(getString(R.string.login_msisdn_error_other)))
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.handle(ThreePidsSettingsAction.AddThreePid(ThreePid.Msisdn(safeMsisdn)))
|
||||
}
|
||||
|
||||
override fun submitCode(threePid: ThreePid.Msisdn, code: String) {
|
||||
viewModel.handle(ThreePidsSettingsAction.SubmitCode(threePid, code))
|
||||
// Hide the keyboard
|
||||
view?.hideKeyboard()
|
||||
}
|
||||
|
||||
override fun cancelAdding() {
|
||||
viewModel.handle(ThreePidsSettingsAction.ChangeUiState(ThreePidsSettingsUiState.Idle))
|
||||
// Hide the keyboard
|
||||
view?.hideKeyboard()
|
||||
}
|
||||
|
||||
override fun continueThreePid(threePid: ThreePid) {
|
||||
viewModel.handle(ThreePidsSettingsAction.ContinueThreePid(threePid))
|
||||
}
|
||||
|
||||
override fun cancelThreePid(threePid: ThreePid) {
|
||||
viewModel.handle(ThreePidsSettingsAction.CancelThreePid(threePid))
|
||||
}
|
||||
|
||||
override fun deleteThreePid(threePid: ThreePid) {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setMessage(getString(R.string.settings_remove_three_pid_confirmation_content, threePid.getFormattedValue()))
|
||||
.setPositiveButton(R.string.remove) { _, _ ->
|
||||
viewModel.handle(ThreePidsSettingsAction.DeleteThreePid(threePid))
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||
return withState(viewModel) {
|
||||
if (it.uiState is ThreePidsSettingsUiState.Idle) {
|
||||
false
|
||||
} else {
|
||||
cancelAdding()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.app.features.settings.threepids
|
||||
|
||||
sealed class ThreePidsSettingsUiState {
|
||||
object Idle : ThreePidsSettingsUiState()
|
||||
data class AddingEmail(val error: String?) : ThreePidsSettingsUiState()
|
||||
data class AddingPhoneNumber(val error: String?) : ThreePidsSettingsUiState()
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.app.features.settings.threepids
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
|
||||
sealed class ThreePidsSettingsViewEvents : VectorViewEvents {
|
||||
data class Failure(val throwable: Throwable) : ThreePidsSettingsViewEvents()
|
||||
object RequestPassword : ThreePidsSettingsViewEvents()
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* 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.app.features.settings.threepids
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.ActivityViewModelContext
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.error.SsoFlowNotSupportedYet
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.utils.ReadOnceTrue
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
|
||||
class ThreePidsSettingsViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: ThreePidsSettingsViewState,
|
||||
private val session: Session,
|
||||
private val stringProvider: StringProvider
|
||||
) : VectorViewModel<ThreePidsSettingsViewState, ThreePidsSettingsAction, ThreePidsSettingsViewEvents>(initialState) {
|
||||
|
||||
// UIA session
|
||||
private var pendingThreePid: ThreePid? = null
|
||||
private var pendingSession: String? = null
|
||||
|
||||
private val loadingCallback: MatrixCallback<Unit> = object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
isLoading(false)
|
||||
|
||||
if (failure is Failure.RegistrationFlowError) {
|
||||
var isPasswordRequestFound = false
|
||||
|
||||
// We only support LoginFlowTypes.PASSWORD
|
||||
// Check if we can provide the user password
|
||||
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
|
||||
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
|
||||
}
|
||||
|
||||
if (isPasswordRequestFound) {
|
||||
pendingSession = failure.registrationFlowResponse.session
|
||||
_viewEvents.post(ThreePidsSettingsViewEvents.RequestPassword)
|
||||
} else {
|
||||
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
|
||||
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(SsoFlowNotSupportedYet()))
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
pendingThreePid = null
|
||||
pendingSession = null
|
||||
isLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLoading(isLoading: Boolean) {
|
||||
setState {
|
||||
copy(
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: ThreePidsSettingsViewState): ThreePidsSettingsViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<ThreePidsSettingsViewModel, ThreePidsSettingsViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: ThreePidsSettingsViewState): ThreePidsSettingsViewModel? {
|
||||
val factory = when (viewModelContext) {
|
||||
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
|
||||
is ActivityViewModelContext -> viewModelContext.activity as? Factory
|
||||
}
|
||||
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeThreePids()
|
||||
observePendingThreePids()
|
||||
}
|
||||
|
||||
private fun observeThreePids() {
|
||||
session.rx()
|
||||
.liveThreePIds(true)
|
||||
.execute {
|
||||
copy(
|
||||
threePids = it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observePendingThreePids() {
|
||||
session.rx()
|
||||
.livePendingThreePIds()
|
||||
.execute {
|
||||
copy(
|
||||
pendingThreePids = it,
|
||||
// Ensure the editText for code will be reset
|
||||
msisdnValidationReinitiator = msisdnValidationReinitiator.toMutableMap().apply {
|
||||
it.invoke()
|
||||
?.filterIsInstance(ThreePid.Msisdn::class.java)
|
||||
?.forEach { threePid ->
|
||||
getOrPut(threePid) { ReadOnceTrue() }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: ThreePidsSettingsAction) {
|
||||
when (action) {
|
||||
is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action)
|
||||
is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action)
|
||||
is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action)
|
||||
is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action)
|
||||
is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action)
|
||||
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action)
|
||||
is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleSubmitCode(action: ThreePidsSettingsAction.SubmitCode) {
|
||||
isLoading(true)
|
||||
setState {
|
||||
copy(
|
||||
msisdnValidationRequests = msisdnValidationRequests.toMutableMap().apply {
|
||||
put(action.threePid.value, Loading())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
// First submit the code
|
||||
session.submitSmsCode(action.threePid, action.code, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
// then finalize
|
||||
pendingThreePid = action.threePid
|
||||
session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
isLoading(false)
|
||||
setState {
|
||||
copy(
|
||||
msisdnValidationRequests = msisdnValidationRequests.toMutableMap().apply {
|
||||
put(action.threePid.value, Fail(failure))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChangeUiState(action: ThreePidsSettingsAction.ChangeUiState) {
|
||||
setState {
|
||||
copy(
|
||||
uiState = action.newUiState,
|
||||
editTextReinitiator = ReadOnceTrue()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddThreePid(action: ThreePidsSettingsAction.AddThreePid) {
|
||||
isLoading(true)
|
||||
|
||||
withState { state ->
|
||||
val allThreePids = state.threePids.invoke().orEmpty() + state.pendingThreePids.invoke().orEmpty()
|
||||
if (allThreePids.any { it.value == action.threePid.value }) {
|
||||
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalArgumentException(stringProvider.getString(
|
||||
when (action.threePid) {
|
||||
is ThreePid.Email -> R.string.auth_email_already_defined
|
||||
is ThreePid.Msisdn -> R.string.auth_msisdn_already_defined
|
||||
}
|
||||
))))
|
||||
} else {
|
||||
viewModelScope.launch {
|
||||
session.addThreePid(action.threePid, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
// Also reset the state
|
||||
setState {
|
||||
copy(
|
||||
uiState = ThreePidsSettingsUiState.Idle
|
||||
)
|
||||
}
|
||||
loadingCallback.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
loadingCallback.onFailure(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleContinueThreePid(action: ThreePidsSettingsAction.ContinueThreePid) {
|
||||
isLoading(true)
|
||||
pendingThreePid = action.threePid
|
||||
viewModelScope.launch {
|
||||
session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCancelThreePid(action: ThreePidsSettingsAction.CancelThreePid) {
|
||||
isLoading(true)
|
||||
viewModelScope.launch {
|
||||
session.cancelAddingThreePid(action.threePid, loadingCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAccountPassword(action: ThreePidsSettingsAction.AccountPassword) {
|
||||
val safeSession = pendingSession ?: return Unit
|
||||
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending session"))) }
|
||||
val safeThreePid = pendingThreePid ?: return Unit
|
||||
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) }
|
||||
isLoading(true)
|
||||
viewModelScope.launch {
|
||||
session.finalizeAddingThreePid(safeThreePid, safeSession, action.password, loadingCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeleteThreePid(action: ThreePidsSettingsAction.DeleteThreePid) {
|
||||
isLoading(true)
|
||||
viewModelScope.launch {
|
||||
session.deleteThreePid(action.threePid, loadingCallback)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.app.features.settings.threepids
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.core.utils.ReadOnceTrue
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
|
||||
data class ThreePidsSettingsViewState(
|
||||
val uiState: ThreePidsSettingsUiState = ThreePidsSettingsUiState.Idle,
|
||||
val isLoading: Boolean = false,
|
||||
val threePids: Async<List<ThreePid>> = Uninitialized,
|
||||
val pendingThreePids: Async<List<ThreePid>> = Uninitialized,
|
||||
val msisdnValidationRequests: Map<String, Async<Unit>> = emptyMap(),
|
||||
val editTextReinitiator: ReadOnceTrue = ReadOnceTrue(),
|
||||
val msisdnValidationReinitiator: Map<ThreePid, ReadOnceTrue> = emptyMap()
|
||||
) : MvRxState
|
|
@ -14,6 +14,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/formTextInputDivider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
app:layout_constraintTop_toTopOf="@+id/item_generic_title_text"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Set a maw width because the text can be long -->
|
||||
<!-- Set a max width because the text can be long -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/item_generic_action_button"
|
||||
style="@style/VectorButtonStyle"
|
||||
|
@ -102,10 +102,26 @@
|
|||
android:layout_marginBottom="16dp"
|
||||
android:maxWidth="@dimen/button_max_width"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/item_generic_destructive_action_button"
|
||||
app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier"
|
||||
app:layout_constraintTop_toBottomOf="@+id/item_generic_description_text"
|
||||
tools:text="@string/settings_troubleshoot_test_device_settings_quickfix"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/item_generic_destructive_action_button"
|
||||
style="@style/VectorButtonStyleDestructive"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:maxWidth="@dimen/button_max_width"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier"
|
||||
app:layout_constraintTop_toBottomOf="@+id/item_generic_action_button"
|
||||
tools:text="@string/delete"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<Button
|
||||
android:id="@+id/settings_item_button"
|
||||
style="@style/VectorButtonStyleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
tools:text="@string/action_change" />
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Switch
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/settings_item_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:orientation="vertical"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
|
@ -38,7 +37,7 @@
|
|||
tools:text="Description / Value" />
|
||||
</LinearLayout>
|
||||
|
||||
<Switch
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/settings_item_switch"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
|
|
47
vector/src/main/res/layout/item_settings_three_pid.xml
Normal file
47
vector/src/main/res/layout/item_settings_three_pid.xml
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="64dp"
|
||||
android:paddingStart="@dimen/layout_horizontal_margin"
|
||||
android:paddingEnd="@dimen/layout_horizontal_margin">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/item_settings_three_pid_icon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:scaleType="center"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_phone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/item_settings_three_pid_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/item_settings_three_pid_delete"
|
||||
app:layout_constraintStart_toEndOf="@+id/item_settings_three_pid_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="alice@email-provider.org" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/item_settings_three_pid_delete"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="@dimen/layout_touch_size"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_trash_24"
|
||||
android:tint="@color/riotx_destructive_accent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -281,6 +281,7 @@
|
|||
<string name="auth_invalid_email">"This doesn’t look like a valid email address"</string>
|
||||
<string name="auth_invalid_phone">"This doesn’t look like a valid phone number"</string>
|
||||
<string name="auth_email_already_defined">This email address is already defined.</string>
|
||||
<string name="auth_msisdn_already_defined">This phone number is already defined.</string>
|
||||
<string name="auth_missing_email">Missing email address</string>
|
||||
<string name="auth_missing_phone">Missing phone number</string>
|
||||
<string name="auth_missing_email_or_phone">Missing email address or phone number</string>
|
||||
|
@ -675,6 +676,7 @@
|
|||
<string name="settings_email_address">Email</string>
|
||||
<string name="settings_add_email_address">Add email address</string>
|
||||
<string name="settings_phone_number">Phone</string>
|
||||
<string name="settings_phone_number_empty">No phone number has been added to your account</string>
|
||||
<string name="settings_add_phone_number">Add phone number</string>
|
||||
<string name="settings_app_info_link_title">Application info</string>
|
||||
<string name="settings_app_info_link_summary">Show the application info in the system settings.</string>
|
||||
|
@ -682,6 +684,11 @@
|
|||
<string name="settings_add_3pid_flow_not_supported">You can\'t do this from Element mobile</string>
|
||||
<string name="settings_add_3pid_authentication_needed">Authentication is required</string>
|
||||
|
||||
<string name="settings_emails">Email addresses</string>
|
||||
<string name="settings_emails_empty">No email has been added to your account</string>
|
||||
<string name="settings_phone_numbers">Phone numbers</string>
|
||||
<string name="settings_remove_three_pid_confirmation_content">Remove %s?</string>
|
||||
<string name="error_threepid_auth_failed">Ensure that you have clicked on the link in the email we have sent to you.</string>
|
||||
|
||||
<string name="settings_notification_advanced">Advanced Notification Settings</string>
|
||||
<string name="settings_notification_by_event">Notification importance by event</string>
|
||||
|
@ -927,6 +934,9 @@
|
|||
<string name="settings_unignore_user">Show all messages from %s?\n\nNote that this action will restart the app and it may take some time.</string>
|
||||
<string name="passwords_do_not_match">Passwords do not match</string>
|
||||
|
||||
<string name="settings_emails_and_phone_numbers_title">Emails and phone numbers</string>
|
||||
<string name="settings_emails_and_phone_numbers_summary">Manage emails and phone numbers linked to your Matrix account</string>
|
||||
|
||||
<string name="settings_delete_notification_targets_confirmation">Are you sure you want to remove this notification target?</string>
|
||||
|
||||
<string name="settings_delete_threepid_confirmation">Are you sure you want to remove the %1$s %2$s?</string>
|
||||
|
@ -1756,6 +1766,7 @@
|
|||
<string name="settings_discovery_no_terms_title">Identity server has no terms of services</string>
|
||||
<string name="settings_discovery_no_terms">The identity server you have chosen does not have any terms of services. Only continue if you trust the owner of the service</string>
|
||||
<string name="settings_text_message_sent">A text message has been sent to %s. Please enter the verification code it contains.</string>
|
||||
<string name="settings_text_message_sent_hint">Code</string>
|
||||
<string name="settings_text_message_sent_wrong_code">The verification code is not correct.</string>
|
||||
|
||||
<string name="settings_discovery_disconnect_with_bound_pid">You are currently sharing email addresses or phone numbers on the identity server %1$s. You will need to reconnect to %2$s to stop sharing them.</string>
|
||||
|
@ -1944,6 +1955,7 @@
|
|||
<string name="login_msisdn_confirm_send_again">Send again</string>
|
||||
<string name="login_msisdn_confirm_submit">Next</string>
|
||||
|
||||
<string name="login_msisdn_notice">"Please use the international format (phone number must start with '+')"</string>
|
||||
<string name="login_msisdn_error_not_international">"International phone numbers must start with '+'"</string>
|
||||
<string name="login_msisdn_error_other">"Phone number seems invalid. Please check it"</string>
|
||||
|
||||
|
@ -2416,6 +2428,8 @@
|
|||
<string name="confirm_your_identity_quad_s">Confirm your identity by verifying this login, granting it access to encrypted messages.</string>
|
||||
<string name="mark_as_verified">Mark as Trusted</string>
|
||||
|
||||
<string name="error_sso_flow_not_supported_yet">Sorry, this operation is not possible yet for accounts connected using Single Sign-On.</string>
|
||||
|
||||
<string name="error_empty_field_choose_user_name">Please choose a username.</string>
|
||||
<string name="error_empty_field_choose_password">Please choose a password.</string>
|
||||
<string name="external_link_confirmation_title">Double-check this link</string>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
|
||||
<im.vector.app.core.preference.VectorPreferenceCategory
|
||||
android:key="SETTINGS_USER_SETTINGS_PREFERENCE_KEY"
|
||||
android:title="@string/settings_user_settings">
|
||||
|
@ -22,29 +21,13 @@
|
|||
android:summary="@string/change_password_summary"
|
||||
android:title="@string/settings_password" />
|
||||
|
||||
<!-- Email will be added here -->
|
||||
|
||||
<!-- Note: inputType does not work, it is set also in code, as well as iconTint -->
|
||||
<im.vector.app.core.preference.VectorEditTextPreference
|
||||
android:icon="@drawable/ic_material_add"
|
||||
android:inputType="textEmailAddress"
|
||||
android:key="ADD_EMAIL_PREFERENCE_KEY"
|
||||
android:order="100"
|
||||
android:title="@string/settings_add_email_address"
|
||||
app:iconTint="@color/riotx_accent" />
|
||||
|
||||
<!-- Phone will be added here -->
|
||||
|
||||
<!-- Note: iconTint does not work, it is also done in code -->
|
||||
<im.vector.app.core.preference.VectorPreference
|
||||
android:icon="@drawable/ic_material_add"
|
||||
android:key="ADD_PHONE_NUMBER_PREFERENCE_KEY"
|
||||
android:order="200"
|
||||
android:title="@string/settings_add_phone_number"
|
||||
app:iconTint="@color/riotx_accent" />
|
||||
android:key="SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY"
|
||||
android:summary="@string/settings_emails_and_phone_numbers_summary"
|
||||
android:title="@string/settings_emails_and_phone_numbers_title"
|
||||
app:fragment="im.vector.app.features.settings.threepids.ThreePidsSettingsFragment" />
|
||||
|
||||
<im.vector.app.core.preference.VectorPreference
|
||||
android:order="1000"
|
||||
android:persistent="false"
|
||||
android:summary="@string/settings_discovery_manage"
|
||||
android:title="@string/settings_discovery_category"
|
||||
|
|
Loading…
Reference in a new issue