diff --git a/CHANGES.md b/CHANGES.md index 0c400c8962..3a2cbcf336 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/docs/add_threePids.md b/docs/add_threePids.md new file mode 100644 index 0000000000..98fcbbda6a --- /dev/null +++ b/docs/add_threePids.md @@ -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. diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 45efd125ee..55ede52c0c 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt index 449c670983..15066cc4a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index 79b71b208e..676f40a918 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 7d2a4ea581..ad05406aa0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -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) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt index 456eecc54a..d5c259050f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt @@ -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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PendingThreePidEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PendingThreePidEntity.kt new file mode 100644 index 0000000000..2f5643d7bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PendingThreePidEntity.kt @@ -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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index ea466db352..2c45cfcdbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule RoomSummaryEntity::class, RoomTagEntity::class, SyncEntity::class, + PendingThreePidEntity::class, UserEntity::class, IgnoredUserEntity::class, BreadcrumbsEntity::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailBody.kt new file mode 100644 index 0000000000..ff81ad6a5c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailBody.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailResponse.kt new file mode 100644 index 0000000000..8654d7c5ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailResponse.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnBody.kt new file mode 100644 index 0000000000..64c53f6729 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnBody.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnResponse.kt new file mode 100644 index 0000000000..b4c137b3a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnResponse.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt new file mode 100644 index 0000000000..c844c8ca6f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt @@ -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) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index 633b047994..97212a8687 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidBody.kt new file mode 100644 index 0000000000..e7d4568f8b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidBody.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidResponse.kt new file mode 100644 index 0000000000..3817277a9d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidResponse.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt new file mode 100644 index 0000000000..69ff7d82da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt @@ -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 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt new file mode 100644 index 0000000000..73e9b39cea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt @@ -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? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt new file mode 100644 index 0000000000..3886b926ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt @@ -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() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePid.kt new file mode 100644 index 0000000000..af7e217d47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePid.kt @@ -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? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePidMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePidMapper.kt new file mode 100644 index 0000000000..b1877027ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePidMapper.kt @@ -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 + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt index 31e1f09bbd..4e2f518c5a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt @@ -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> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt index 57a86d03e0..ae7ae7a6f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt new file mode 100644 index 0000000000..b11955b96a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt @@ -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 + } + } +} diff --git a/tools/templates/RiotXFeature/globals.xml.ftl b/tools/templates/ElementFeature/globals.xml.ftl similarity index 100% rename from tools/templates/RiotXFeature/globals.xml.ftl rename to tools/templates/ElementFeature/globals.xml.ftl diff --git a/tools/templates/RiotXFeature/recipe.xml.ftl b/tools/templates/ElementFeature/recipe.xml.ftl similarity index 100% rename from tools/templates/RiotXFeature/recipe.xml.ftl rename to tools/templates/ElementFeature/recipe.xml.ftl diff --git a/tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl b/tools/templates/ElementFeature/root/res/layout/fragment.xml.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl rename to tools/templates/ElementFeature/root/res/layout/fragment.xml.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/Action.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/Action.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/Activity.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/Activity.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewEvents.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/ViewEvents.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewState.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/ViewState.kt.ftl diff --git a/tools/templates/RiotXFeature/template.xml b/tools/templates/ElementFeature/template.xml similarity index 99% rename from tools/templates/RiotXFeature/template.xml rename to tools/templates/ElementFeature/template.xml index 33d2edfc70..14c718c993 100644 --- a/tools/templates/RiotXFeature/template.xml +++ b/tools/templates/ElementFeature/template.xml @@ -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"> diff --git a/tools/templates/configure.sh b/tools/templates/configure.sh index de7fe7da81..0669ab1312 100755 --- a/tools/templates/configure.sh +++ b/tools/templates/configure.sh @@ -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." } diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 591d1c0474..d0e4c938cd 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 051847321a..43395b97f7 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/core/error/SsoFlowNotSupportedYet.kt b/vector/src/main/java/im/vector/app/core/error/SsoFlowNotSupportedYet.kt new file mode 100644 index 0000000000..7b22072c34 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/error/SsoFlowNotSupportedYet.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/core/extensions/ThreePid.kt b/vector/src/main/java/im/vector/app/core/extensions/ThreePid.kt new file mode 100644 index 0000000000..5d91370963 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/ThreePid.kt @@ -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 + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/preference/UserAvatarPreference.kt b/vector/src/main/java/im/vector/app/core/preference/UserAvatarPreference.kt index e3b4430fe0..3bb50c6284 100755 --- a/vector/src/main/java/im/vector/app/core/preference/UserAvatarPreference.kt +++ b/vector/src/main/java/im/vector/app/core/preference/UserAvatarPreference.kt @@ -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) } } } diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt index 832250fab4..492df9eb00 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/core/utils/ReadOnce.kt b/vector/src/main/java/im/vector/app/core/utils/ReadOnce.kt new file mode 100644 index 0000000000..4283ecefab --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/ReadOnce.kt @@ -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 +} diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt index 6e1b5a5f57..306d9bffd1 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt @@ -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) + } } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt index bb225f4ef4..c9ad23f1a9 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt @@ -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() - } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsEditTextItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsEditTextItem.kt index 81d46373a2..ad139309ac 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsEditTextItem.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsItem.kt index 53bf7e64d6..3471a3ab56 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsItem.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt index b7d9997d46..9f64a68d4f 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt index f0c43228c1..12538d314a 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index e31b81b162..c4df3a8d6e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index 237c8c218d..8c5762afce 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -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())) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidItem.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidItem.kt new file mode 100644 index 0000000000..6b05c9f96e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidItem.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt new file mode 100644 index 0000000000..0be3c6a198 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt new file mode 100644 index 0000000000..f07e31bb2e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt new file mode 100644 index 0000000000..519b7132bf --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt @@ -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 + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsUiState.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsUiState.kt new file mode 100644 index 0000000000..d7e427acf3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsUiState.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt new file mode 100644 index 0000000000..1ac2d10458 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt new file mode 100644 index 0000000000..2001c85a2e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt new file mode 100644 index 0000000000..b080c06cbd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt @@ -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 diff --git a/vector/src/main/res/layout/item_form_text_input.xml b/vector/src/main/res/layout/item_form_text_input.xml index 775489c5d9..594bfc1788 100644 --- a/vector/src/main/res/layout/item_form_text_input.xml +++ b/vector/src/main/res/layout/item_form_text_input.xml @@ -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" diff --git a/vector/src/main/res/layout/item_generic_list.xml b/vector/src/main/res/layout/item_generic_list.xml index 4b206c352d..f89413f15f 100644 --- a/vector/src/main/res/layout/item_generic_list.xml +++ b/vector/src/main/res/layout/item_generic_list.xml @@ -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> diff --git a/vector/src/main/res/layout/item_settings_button.xml b/vector/src/main/res/layout/item_settings_button.xml index d994a7fe99..9070eb37b7 100644 --- a/vector/src/main/res/layout/item_settings_button.xml +++ b/vector/src/main/res/layout/item_settings_button.xml @@ -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" /> diff --git a/vector/src/main/res/layout/item_settings_button_single_line.xml b/vector/src/main/res/layout/item_settings_button_single_line.xml index 592eb6dd96..583ed541ca 100644 --- a/vector/src/main/res/layout/item_settings_button_single_line.xml +++ b/vector/src/main/res/layout/item_settings_button_single_line.xml @@ -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" diff --git a/vector/src/main/res/layout/item_settings_simple_item.xml b/vector/src/main/res/layout/item_settings_simple_item.xml index 2aeda8c295..7fe7937da6 100644 --- a/vector/src/main/res/layout/item_settings_simple_item.xml +++ b/vector/src/main/res/layout/item_settings_simple_item.xml @@ -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" diff --git a/vector/src/main/res/layout/item_settings_three_pid.xml b/vector/src/main/res/layout/item_settings_three_pid.xml new file mode 100644 index 0000000000..fd3443ac17 --- /dev/null +++ b/vector/src/main/res/layout/item_settings_three_pid.xml @@ -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> \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 2e031cfd4f..eaa4469069 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -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> diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index 042ea5c77c..c1fe82e4c2 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -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"