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"