From 8b63f78d7647b62fd2aa85db02b0773db34b3164 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 12 Nov 2019 15:31:46 +0100 Subject: [PATCH 001/132] Add documentation on the sign up flow --- docs/signup.md | 395 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 docs/signup.md diff --git a/docs/signup.md b/docs/signup.md new file mode 100644 index 0000000000..7d35f6b19b --- /dev/null +++ b/docs/signup.md @@ -0,0 +1,395 @@ +# Sign up to a homeserver + +This document describes the flow of registration to a homeserver. Examples come from the matrix.org homeserver, and the logs come from Riot-Android. + +Note that it contains bugs: + - "password" and "initial_device_display_name" values are sent a bit too much + - the first received "sessionId" is not reused + - The order of stages returned by the homeserver is not strictly followed + +Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management + +## Sign up flows + +### First step + +Client request the sign-up flows, once the homeserver is chosen by the user and its url is knwon (in the example it's https://matrix.org) + +> curl -X POST --data $'{"initial_device_display_name":"Mobile device","x_show_msisdn":true}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "initial_device_display_name": "Mobile device", + "x_show_msisdn": true +} +``` + +401 + +```json +{ + "session": "vwehdKMtkRedactedAMwgCACZ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + } +} +``` + +### Step 1: entering user name and password + +The app is displaying a form with login and password. Only the login is sent for the first request + +> curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "initial_device_display_name": "Mobile device", + "username": "alice" +} +``` + +401 + +```json +{ + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + } +} +``` + +#### If username already exists + +```json +{ + "errcode": "M_USER_IN_USE", + "error": "User ID already taken." +} +``` + +### Step 2: entering email + +User is proposed to enter an email. We skip this step. + +> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"},"initial_device_display_name":"Mobile device","password":"azerty","username":"alice"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "type": "m.login.dummy" + }, + "initial_device_display_name": "Mobile device", + "password": "password_REDACTED", + "username":"alice" +} +``` + +401 + +```json +{ + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + }, + "completed": [ + "m.login.dummy" + ] +} +``` + +### Step 2 bis: we enter an email + +> curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@yopmail.com","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken' + +```json +{ + "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", + "email": "alice@yopmail.com", + "send_attempt": 0 +} +``` + +200 + +```json +{ + "sid": "qlBCREDACTEDEtgxD" +} +``` + +And + +> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"},"initial_device_display_name":"Mobile device","password":"password_REDACTED","username":"alice"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "threepid_creds": { + "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", + "sid": "qlBCREDACTEDEtgxD" + }, + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "type": "m.login.email.identity" + }, + "initial_device_display_name": "Mobile device", + "password": "password_REDACTED", + "username": "alice" +} +``` + +401 + +```json +{ + "errcode": "M_UNAUTHORIZED", + "error": "" +} +``` + +The app is now polling on + +> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"},"initial_device_display_name":"Mobile device","password":"password_REDACTED","username":"alice"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "threepid_creds": { + "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", + "sid": "qlBCREDACTEDEtgxD" + }, + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "type": "m.login.email.identity" + }, + "initial_device_display_name": "Mobile device", + "password": "password_REDACTED", + "username": "alice" +} +``` + +We click on the link received by email https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD which contains: +- A token vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ +- a client secret: 53e679ea-oRED-ACTED-92b8-3012c49c6cfa +- A sid: qlBCREDACTEDEtgxD + +Once the link is clicked, the registration request (polling) returns a 401 with the following content: + +```json +{ + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + }, + "completed": [ + "m.login.email.identity" + ] +} +``` + +### Step 3: Accepting T&C + +User is proposed to accept T&C and he accepts them + +> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"},"initial_device_display_name":"Mobile device"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "type": "m.login.terms" + }, + "initial_device_display_name": "Mobile device" +} +``` + +401 + +```json +{ + "session": "xptUYoREDACTEDogOWAGVnbJQ", + "flows": [ + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.dummy" + ] + }, + { + "stages": [ + "m.login.recaptcha", + "m.login.terms", + "m.login.email.identity" + ] + } + ], + "params": { + "m.login.recaptcha": { + "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" + }, + "m.login.terms": { + "policies": { + "privacy_policy": { + "version": "1.0", + "en": { + "name": "Terms and Conditions", + "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" + } + } + } + } + }, + "completed": [ + "m.login.dummy", + "m.login.terms" + ] +} +``` + +### Step 4: Captcha + +User is proposed to prove he is not a robot and he does it: + +> curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"iLHmdwNlXZoREDACTEDoouwMi","type":"m.login.recaptcha"},"initial_device_display_name":"Mobile device"}' 'https://matrix.org/_matrix/client/r0/register' + +```json +{ + "auth": { + "response": "03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q", + "session": "iLHmdwNlXZoREDACTEDoouwMi", + "type": "m.login.recaptcha" + }, + "initial_device_display_name": "Mobile device" +} +``` + +200 + +```json +{ + "user_id": "@alice:matrix.org", + "home_server": "matrix.org", + "access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmcKMoREDACTEDo50aWZpZXIga2V5CjAwMTBjaWQgZ2VuID0gMQowMDI5Y2lkIHVzZXJfaWQgPSBAYmVub2l0eHh4eDptYXRoREDACTEDoCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNHVSVm00aVFDaWlKdoREDACTEDoJmc2lnbmF0dXJlIOmHnTLRfxiPjhrWhS-dThUX-qAzZktfRThzH1YyAsxaCg", + "device_id": "FLBAREDAJZ" +} +``` + +The account is created! From 4485d1c68544845f0bcf3e8dbaba1f5cef0d7150 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 13 Nov 2019 18:40:17 +0100 Subject: [PATCH 002/132] Registration flow: SDK side --- .idea/dictionaries/bmarty.xml | 2 + .../android/api/auth/data/Credentials.kt | 3 +- .../auth/registration/RegistrationService.kt | 25 +++++ .../auth/registration/RegistrationWizard.kt | 31 +++++ .../android/api/auth/registration/Stage.kt | 51 +++++++++ .../matrix/android/api/util/Cancelable.kt | 3 + .../matrix/android/internal/auth/AuthAPI.kt | 8 ++ .../internal/auth/registration/AuthParams.kt | 32 ++++++ .../auth/registration/AuthParamsCaptcha.kt | 30 +++++ .../registration/AuthParamsEmailIdentity.kt | 40 +++++++ .../DefaultRegistrationService.kt | 46 ++++++++ .../registration/DefaultRegistrationWizard.kt | 106 ++++++++++++++++++ .../LocalizedFlowDataLoginTerms.kt | 31 +++++ .../auth/registration/RegisterTask.kt | 63 +++++++++++ .../auth/registration/RegistrationParams.kt | 47 ++++++++ 15 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 01981ada12..784e5c1182 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -4,6 +4,7 @@ backstack bytearray ciphertext + coroutine decryptor emoji emojis @@ -12,6 +13,7 @@ linkified linkify megolm + msisdn pbkdf pkcs diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index d5962e261b..089129967b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -30,4 +30,5 @@ data class Credentials( @Json(name = "home_server") val homeServer: String, @Json(name = "access_token") val accessToken: String, @Json(name = "refresh_token") val refreshToken: String?, - @Json(name = "device_id") val deviceId: String?) + @Json(name = "device_id") val deviceId: String? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt new file mode 100644 index 0000000000..7b131b922d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.registration + +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig + +interface RegistrationService { + + fun getOrCreateRegistrationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): RegistrationWizard + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt new file mode 100644 index 0000000000..879bd5d74b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.registration + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.Cancelable + +interface RegistrationWizard { + + fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback): Cancelable + + fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable + + // TODO Add other method here + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt new file mode 100644 index 0000000000..f302b953c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.registration + +import im.vector.matrix.android.api.util.JsonDict + + +sealed class Stage(open val mandatory: Boolean) { + + // m.login.password + data class Password(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) + + // m.login.recaptcha + data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) + + // m.login.oauth2 + // m.login.email.identity + data class Email(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + + // m.login.msisdn + data class Msisdn(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + // m.login.token + // m.login.dummy + + // Undocumented yet: m.login.terms + data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + + // TODO SSO + + // For unknown stages + data class Other(override val mandatory: Boolean, val type: String, val params: JsonDict?) : Stage(mandatory) +} + + +class TermPolicies { + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt index 7f3543dec2..7ec01cca10 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt @@ -29,3 +29,6 @@ interface Cancelable { // no-op } } + + +object NoOpCancellable : Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index bfc2b76db7..8316589ad4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.auth import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.auth.registration.RegistrationParams import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body @@ -31,6 +32,13 @@ import retrofit2.http.POST */ internal interface AuthAPI { + /** + * Register to the homeserver + * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") + fun register(registrationParams: RegistrationParams): Call + /** * Get the supported login flow * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt new file mode 100644 index 0000000000..69ef4e2238 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Open class, parent to all possible authentication parameters + */ +@JsonClass(generateAdapter = true) +open class AuthParams( + @Json(name = "type") + val type: String, + + @Json(name = "session") + val session: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt new file mode 100644 index 0000000000..daf9f911c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes + +/** + * Class to define the authentication parameters for "m.login.recaptcha" type + */ +@JsonClass(generateAdapter = true) +class AuthParamsCaptcha(session: String, + + @Json(name = "response") + val response: String) + : AuthParams(LoginFlowTypes.RECAPTCHA, session) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt new file mode 100644 index 0000000000..981b8682f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes + +/** + * Class to define the authentication parameters for "m.login.email.identity" type + */ +@JsonClass(generateAdapter = true) +class AuthParamsEmailIdentity(session: String, + + @Json(name = "threepid_creds") + val threePidCredentials: ThreePidCredentials) + : AuthParams(LoginFlowTypes.EMAIL_IDENTITY, session) + +data class ThreePidCredentials( + @Json(name = "client_secret") + val clientSecret: String? = null, + + @Json(name = "id_server") + val idServer: String? = null, + + val sid: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt new file mode 100644 index 0000000000..89dbda077b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.registration + +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.auth.registration.RegistrationService +import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.internal.SessionManager +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import okhttp3.OkHttpClient +import javax.inject.Provider + +internal class DefaultRegistrationService(@Unauthenticated + private val okHttpClient: Provider, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager) : RegistrationService { + + override fun getOrCreateRegistrationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): RegistrationWizard { + // TODO Persist the wizard? + return DefaultRegistrationWizard(homeServerConnectionConfig, + okHttpClient, + retrofitFactory, + coroutineDispatchers, + sessionParamsStore, + sessionManager) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt new file mode 100644 index 0000000000..d856d9211a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.registration + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable +import im.vector.matrix.android.internal.SessionManager +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.util.CancelableCoroutine +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import javax.inject.Provider + +internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: HomeServerConnectionConfig, + @Unauthenticated + private val okHttpClient: Provider, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager) : RegistrationWizard { + + private var currentSession: String? = null + + private val authAPI = buildAuthAPI() + private val registerTask = DefaultRegisterTask(authAPI) + + override fun createAccount(userName: String, + password: String, + initialDeviceDisplayName: String?, + callback: MatrixCallback): Cancelable { + return performRegistrationRequest(RegistrationParams( + username = userName, + password = password, + initialDeviceDisplayName = initialDeviceDisplayName + ), callback) + } + + override fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + return performRegistrationRequest( + RegistrationParams( + auth = AuthParamsCaptcha( + session = safeSession, + response = response) + ), callback) + } + + + private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { + val job = GlobalScope.launch(coroutineDispatchers.main) { + val result = runCatching { + registerTask.execute(RegisterTask.Params(registrationParams)) + } + result.fold( + { + val sessionParams = SessionParams(it, homeServerConnectionConfig) + sessionParamsStore.save(sessionParams) + val session = sessionManager.getOrCreateSession(sessionParams) + + callback.onSuccess(session) + }, + { + if (it is Failure.RegistrationFlowError) { + currentSession = it.registrationFlowResponse.session + } + callback.onFailure(it) + } + ) + } + return CancelableCoroutine(job) + } + + private fun buildAuthAPI(): AuthAPI { + val retrofit = retrofitFactory.create(okHttpClient.get(), homeServerConnectionConfig.homeServerUri.toString()) + return retrofit.create(AuthAPI::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt new file mode 100644 index 0000000000..dd125e3c74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 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.androidsdk.rest.model.login + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +/** + * This class represent a localized privacy policy for registration Flow. + */ +@Parcelize +data class LocalizedFlowDataLoginTerms( + var policyName: String? = null, + var version: String? = null, + var localizedUrl: String? = null, + var localizedName: String? = null +) : Parcelable \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt new file mode 100644 index 0000000000..4c3cc59e7a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.registration + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface RegisterTask : Task { + data class Params( + val registrationParams: RegistrationParams + ) +} + +internal class DefaultRegisterTask @Inject constructor(private val authAPI: AuthAPI) + : RegisterTask { + + override suspend fun execute(params: RegisterTask.Params): Credentials { + try { + return executeRequest { + apiCall = authAPI.register(params.registrationParams) + } + } catch (throwable: Throwable) { + if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { + // Parse to get a RegistrationFlowResponse + val registrationFlowResponse = try { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(throwable.errorBody) + } catch (e: Exception) { + null + } + // check if the server response can be cast + if (registrationFlowResponse != null) { + throw Failure.RegistrationFlowError(registrationFlowResponse) + } else { + throw throwable + } + } else { + // Other error + throw throwable + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt new file mode 100644 index 0000000000..db8475e06c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to pass parameters to the different registration types for /register. + */ +@JsonClass(generateAdapter = true) +data class RegistrationParams( + // authentication parameters + @Json(name = "auth") + val auth: AuthParams? = null, + + // the account username + @Json(name = "username") + val username: String? = null, + + // the account password + @Json(name = "password") + val password: String? = null, + + // device name + @Json(name = "initial_device_display_name") + val initialDeviceDisplayName: String? = null, + + // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app + // versions to end up in fallback because the HS returns the msisdn flow which they don't support + val x_show_msisdn: Boolean? = null +) \ No newline at end of file From 6ab7209e4d8327d2505eab5d1dac668f7cc78460 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 13 Nov 2019 19:00:59 +0100 Subject: [PATCH 003/132] Handle navigation with VectorSharedAction --- .../vector/riotx/core/di/ViewModelModule.kt | 6 +++++ .../riotx/features/login/LoginAction.kt | 1 - .../riotx/features/login/LoginActivity.kt | 23 ++++++++--------- .../riotx/features/login/LoginFragment.kt | 5 +++- .../riotx/features/login/LoginNavigation.kt | 25 +++++++++++++++++++ .../login/LoginSharedActionViewModel.kt | 22 ++++++++++++++++ .../login/LoginSsoFallbackFragment.kt | 4 ++- .../riotx/features/login/LoginViewModel.kt | 12 --------- 8 files changed, 70 insertions(+), 28 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index cc1e4dabc7..0876701504 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -31,6 +31,7 @@ import im.vector.riotx.features.home.HomeSharedActionViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import im.vector.riotx.features.login.LoginSharedActionViewModel import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel @@ -112,4 +113,9 @@ interface ViewModelModule { @IntoMap @ViewModelKey(RoomDirectorySharedActionViewModel::class) fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(LoginSharedActionViewModel::class) + fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index bb42bc8e0c..63d19ea148 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -23,6 +23,5 @@ sealed class LoginAction : VectorViewModelAction { data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() data class Login(val login: String, val password: String) : LoginAction() data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() - data class NavigateTo(val target: LoginActivity.Navigation) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index abed22cb5e..4e5b5621ce 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -25,7 +25,6 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack -import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity @@ -33,13 +32,8 @@ import javax.inject.Inject class LoginActivity : VectorBaseActivity() { - // Supported navigation actions for this Activity - sealed class Navigation { - object OpenSsoLoginFallback : Navigation() - object GoBack : Navigation() - } - private val loginViewModel: LoginViewModel by viewModel() + private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory @@ -60,12 +54,15 @@ class LoginActivity : VectorBaseActivity() { loginViewModel.handle(LoginAction.InitWith(loginConfig)) } - loginViewModel.navigationLiveData.observeEvent(this) { - when (it) { - is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) - is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - } - } + loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java) + loginSharedActionViewModel.observe() + .subscribe { + when (it) { + is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) + is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + } + .disposeOnDestroy() loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) { if (it is Success) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 456e4b2bb3..aa9aabe5dd 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -44,6 +44,7 @@ import javax.inject.Inject class LoginFragment @Inject constructor() : VectorBaseFragment() { private val viewModel: LoginViewModel by activityViewModel() + private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel private var passwordShown = false @@ -52,6 +53,8 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) + setupNotice() setupAuthButton() setupPasswordReveal() @@ -114,7 +117,7 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() { } private fun openSso() { - viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback)) + loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) } private fun setupPasswordReveal() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt new file mode 100644 index 0000000000..c9de4695f9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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.riotx.features.login + +import im.vector.riotx.core.platform.VectorSharedAction + +// Supported navigation actions for this Activity +sealed class LoginNavigation : VectorSharedAction { + object OpenSsoLoginFallback : LoginNavigation() + object GoBack : LoginNavigation() +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt new file mode 100644 index 0000000000..625208b682 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 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.riotx.features.login + +import im.vector.riotx.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class LoginSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index 38deccccaf..f77b36b1d9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -47,6 +47,7 @@ import javax.inject.Inject */ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnBackPressed { + private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel private val viewModel: LoginViewModel by activityViewModel() var homeServerUrl: String = "" @@ -69,6 +70,7 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB login_sso_fallback_toolbar.title = getString(R.string.login) setupWebview() + loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) } @SuppressLint("SetJavaScriptEnabled") @@ -143,7 +145,7 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB super.onReceivedError(view, errorCode, description, failingUrl) // on error case, close this fragment - viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.GoBack)) + loginSharedActionViewModel.post(LoginNavigation.GoBack) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index a0a7258e2a..b3d7e56029 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -16,8 +16,6 @@ package im.vector.riotx.features.login -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import arrow.core.Try import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted @@ -32,7 +30,6 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import timber.log.Timber @@ -60,10 +57,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private var loginConfig: LoginConfig? = null - private val _navigationLiveData = MutableLiveData>() - val navigationLiveData: LiveData> - get() = _navigationLiveData - private var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null @@ -73,7 +66,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) is LoginAction.SsoLoginSuccess -> handleSsoLoginSuccess(action) - is LoginAction.NavigateTo -> handleNavigation(action) } } @@ -202,10 +194,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } - private fun handleNavigation(action: LoginAction.NavigateTo) { - _navigationLiveData.postValue(LiveEvent(action.target)) - } - override fun onCleared() { super.onCleared() From bdfc4ad8a7d70a0f0853d149b932d5858ef9e1d8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 10:23:48 +0100 Subject: [PATCH 004/132] Login screens: splash screen --- .../features/login/AbstractLoginFragment.kt | 40 ++++++ .../riotx/features/login/LoginActivity.kt | 3 +- .../riotx/features/login/LoginFragment.kt | 8 +- .../riotx/features/login/LoginNavigation.kt | 3 +- .../features/login/LoginSplashFragment.kt | 34 +++++ .../main/res/layout/fragment_login_splash.xml | 126 ++++++++++++++++++ vector/src/main/res/values/strings_riotX.xml | 5 + 7 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_splash.xml diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt new file mode 100644 index 0000000000..25472fa9a1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 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.riotx.features.login + +import android.os.Bundle +import android.view.View +import androidx.annotation.CallSuper +import com.airbnb.mvrx.activityViewModel +import im.vector.riotx.core.platform.VectorBaseFragment + +/** + * Parent Fragment for all the login/registration screens + */ +abstract class AbstractLoginFragment() : VectorBaseFragment() { + + protected val viewModel: LoginViewModel by activityViewModel() + protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel + + @CallSuper + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 4e5b5621ce..d371b459eb 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -45,7 +45,7 @@ class LoginActivity : VectorBaseActivity() { override fun initUiAndData() { if (isFirstCreation()) { - addFragment(R.id.simpleFragmentContainer, LoginFragment::class.java) + addFragment(R.id.simpleFragmentContainer, LoginSplashFragment::class.java) } // Get config extra @@ -58,6 +58,7 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index aa9aabe5dd..4f918baa8a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -28,7 +28,6 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R import im.vector.riotx.core.extensions.setTextWithColoredPart import im.vector.riotx.core.extensions.showPassword -import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.features.homeserver.ServerUrlsRepository import io.reactivex.Observable @@ -41,10 +40,7 @@ import javax.inject.Inject * What can be improved: * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect */ -class LoginFragment @Inject constructor() : VectorBaseFragment() { - - private val viewModel: LoginViewModel by activityViewModel() - private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel +class LoginFragment @Inject constructor() : AbstractLoginFragment() { private var passwordShown = false @@ -53,8 +49,6 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) - setupNotice() setupAuthButton() setupPasswordReveal() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index c9de4695f9..28d583a749 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -18,8 +18,9 @@ package im.vector.riotx.features.login import im.vector.riotx.core.platform.VectorSharedAction -// Supported navigation actions for this Activity +// Supported navigation actions for LoginActivity sealed class LoginNavigation : VectorSharedAction { + object OpenServerSelection : LoginNavigation() object OpenSsoLoginFallback : LoginNavigation() object GoBack : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt new file mode 100644 index 0000000000..672502a167 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 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.riotx.features.login + +import butterknife.OnClick +import im.vector.riotx.R +import javax.inject.Inject + +/** + * + */ +class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_splash + + @OnClick(R.id.loginSplashSubmit) + fun getStarted() { + loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection) + } +} diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml new file mode 100644 index 0000000000..fd40e0bca3 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8d8be693e1..4e7143fa56 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -23,5 +23,10 @@ %1$s made the room public to whoever knows the link. %1$s made the room invite only. + Liberate your communication + Chat with people directly or in groups + Keep conversations private with encryption + Extend & customise your experience + Get started From fa6a9cab7ee2db3fe7d158b143b44b6fdb2fc936 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 11:56:19 +0100 Subject: [PATCH 005/132] Login screens: server selection --- .idea/dictionaries/bmarty.xml | 1 + .../riotx/features/login/LoginAction.kt | 1 + .../riotx/features/login/LoginActivity.kt | 16 +- .../riotx/features/login/LoginNavigation.kt | 1 + .../login/LoginServerSelectionFragment.kt | 81 +++++++ .../riotx/features/login/LoginViewModel.kt | 9 + .../riotx/features/login/LoginViewState.kt | 1 + .../vector/riotx/features/login/ServerType.kt | 23 ++ .../src/main/res/drawable/bg_login_server.xml | 12 ++ .../res/drawable/bg_login_server_checked.xml | 12 ++ .../res/drawable/bg_login_server_selector.xml | 8 + .../src/main/res/drawable/ic_logo_modular.xml | 34 +++ .../fragment_login_server_selection.xml | 197 ++++++++++++++++++ .../main/res/layout/fragment_login_splash.xml | 17 +- vector/src/main/res/values/strings_riotX.xml | 10 + vector/src/main/res/values/styles_login.xml | 14 ++ .../src/main/res/values/text_appearances.xml | 19 ++ 17 files changed, 442 insertions(+), 14 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/ServerType.kt create mode 100644 vector/src/main/res/drawable/bg_login_server.xml create mode 100644 vector/src/main/res/drawable/bg_login_server_checked.xml create mode 100644 vector/src/main/res/drawable/bg_login_server_selector.xml create mode 100644 vector/src/main/res/drawable/ic_logo_modular.xml create mode 100644 vector/src/main/res/layout/fragment_login_server_selection.xml create mode 100644 vector/src/main/res/values/styles_login.xml diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 784e5c1182..7c9f6489ee 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -3,6 +3,7 @@ backstack bytearray + checkables ciphertext coroutine decryptor diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 63d19ea148..8192d26a30 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -24,4 +24,5 @@ sealed class LoginAction : VectorViewModelAction { data class Login(val login: String, val password: String) : LoginAction() data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() + data class UpdateServerType(val serverType: ServerType) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index d371b459eb..ecf3fa45e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment @@ -58,9 +59,10 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) - is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) - is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) + is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } } .disposeOnDestroy() @@ -74,6 +76,14 @@ class LoginActivity : VectorBaseActivity() { } } + private fun onServerSelectionDone() = withState(loginViewModel) { + when (it.serverType) { + ServerType.MatrixOrg -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) + ServerType.Modular, + ServerType.Other -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginEnterHomeServerFragment::class.java) + } + } + override fun onResume() { super.onResume() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index 28d583a749..46a6c213d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -21,6 +21,7 @@ import im.vector.riotx.core.platform.VectorSharedAction // Supported navigation actions for LoginActivity sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() + object OnServerSelectionDone : LoginNavigation() object OpenSsoLoginFallback : LoginNavigation() object GoBack : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt new file mode 100644 index 0000000000..011100a6f5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 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.riotx.features.login + +import android.os.Bundle +import android.view.View +import butterknife.OnClick +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import kotlinx.android.synthetic.main.fragment_login_server_selection.* +import me.gujun.android.span.span +import javax.inject.Inject + +/** + * + */ +class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_server_selection + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initTextViews() + } + + private fun updateSelectedChoice(serverType: ServerType) { + loginServerChoiceMatrixOrg.isChecked = serverType == ServerType.MatrixOrg + loginServerChoiceModular.isChecked = serverType == ServerType.Modular + loginServerChoiceOther.isChecked = serverType == ServerType.Other + } + + private fun initTextViews() { + loginServerChoiceModularLearnMore.text = span { + text = getString(R.string.login_server_modular_learn_more) + textDecorationLine = "underline" + onClick = { + // TODO + } + } + + } + + @OnClick(R.id.loginServerChoiceMatrixOrg) + fun selectMatrixOrg() { + viewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + } + + @OnClick(R.id.loginServerChoiceModular) + fun selectModular() { + viewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + } + + @OnClick(R.id.loginServerChoiceOther) + fun selectOther() { + viewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + } + + @OnClick(R.id.loginServerSubmit) + fun submit() { + loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) + } + + override fun invalidate() = withState(viewModel) { + updateSelectedChoice(it.serverType) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index b3d7e56029..a596603bc6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -62,6 +62,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi override fun handle(action: LoginAction) { when (action) { + is LoginAction.UpdateServerType -> handleUpdateServerType(action) is LoginAction.InitWith -> handleInitWith(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) @@ -69,6 +70,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { + setState { + copy( + serverType = action.serverType + ) + } + } + private fun handleInitWith(action: LoginAction.InitWith) { loginConfig = action.loginConfig } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 0cc0476254..5c00614b09 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized data class LoginViewState( + val serverType: ServerType = ServerType.MatrixOrg, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/login/ServerType.kt b/vector/src/main/java/im/vector/riotx/features/login/ServerType.kt new file mode 100644 index 0000000000..4c7007c137 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/ServerType.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 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.riotx.features.login + +enum class ServerType { + MatrixOrg, + Modular, + Other +} diff --git a/vector/src/main/res/drawable/bg_login_server.xml b/vector/src/main/res/drawable/bg_login_server.xml new file mode 100644 index 0000000000..5aecd26292 --- /dev/null +++ b/vector/src/main/res/drawable/bg_login_server.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_login_server_checked.xml b/vector/src/main/res/drawable/bg_login_server_checked.xml new file mode 100644 index 0000000000..1aea622462 --- /dev/null +++ b/vector/src/main/res/drawable/bg_login_server_checked.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_login_server_selector.xml b/vector/src/main/res/drawable/bg_login_server_selector.xml new file mode 100644 index 0000000000..57be1e5d54 --- /dev/null +++ b/vector/src/main/res/drawable/bg_login_server_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_logo_modular.xml b/vector/src/main/res/drawable/ic_logo_modular.xml new file mode 100644 index 0000000000..c95ee66b86 --- /dev/null +++ b/vector/src/main/res/drawable/ic_logo_modular.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml new file mode 100644 index 0000000000..58bc682d2a --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index fd40e0bca3..5c025ef57b 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -25,9 +25,7 @@ android:layout_height="wrap_content" android:layout_marginTop="48dp" android:text="@string/login_splash_title" - android:textColor="?riotx_text_primary" - android:textSize="20sp" - android:textStyle="bold" + android:textAppearance="@style/TextAppearance.Vector.Login.Title" app:layout_constraintBottom_toTopOf="@+id/loginSplashText1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashLogo" /> @@ -51,8 +49,7 @@ android:layout_marginTop="32dp" android:gravity="start" android:text="@string/login_splash_text1" - android:textColor="?vctr_notice_secondary" - android:textSize="16sp" + android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@+id/loginSplashText2" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/loginSplashPicto1" @@ -76,8 +73,7 @@ android:layout_marginTop="16dp" android:gravity="start" android:text="@string/login_splash_text2" - android:textColor="?vctr_notice_secondary" - android:textSize="16sp" + android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@id/loginSplashText3" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/loginSplashPicto2" @@ -100,9 +96,8 @@ android:layout_marginStart="12dp" android:layout_marginTop="16dp" android:gravity="start" - android:text="@string/login_splash_text1" - android:textColor="?vctr_notice_secondary" - android:textSize="16sp" + android:text="@string/login_splash_text3" + android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/loginSplashPicto3" @@ -110,7 +105,7 @@ Extend & customise your experience Get started + Select a server + Just like email, accounts have one home, although you can talk to anyone + Join millions free on the largest public server + Premium hosting for organisations + Learn more + Other + Custom & advanced settings + Continue + + diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml new file mode 100644 index 0000000000..ffcd5bc29e --- /dev/null +++ b/vector/src/main/res/values/styles_login.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/text_appearances.xml b/vector/src/main/res/values/text_appearances.xml index 606aef6511..6c2a71631d 100644 --- a/vector/src/main/res/values/text_appearances.xml +++ b/vector/src/main/res/values/text_appearances.xml @@ -37,4 +37,23 @@ ?riotx_text_secondary + + + + + + \ No newline at end of file From da8d6fb4f437e313dee7747dbae1e2699d319d4e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 13:16:01 +0100 Subject: [PATCH 006/132] Login screens: signup signin selection --- .idea/dictionaries/bmarty.xml | 2 + .../riotx/features/login/LoginAction.kt | 1 + .../riotx/features/login/LoginActivity.kt | 7 +- .../riotx/features/login/LoginNavigation.kt | 1 + .../LoginSignUpSignInSelectionFragment.kt | 70 +++++++++++++++ .../riotx/features/login/LoginViewModel.kt | 9 ++ .../riotx/features/login/LoginViewState.kt | 1 + .../vector/riotx/features/login/SignMode.kt | 24 ++++++ .../main/res/drawable/ic_logo_matrix_org.xml | 30 +++++++ .../fragment_login_server_selection.xml | 3 +- ...fragment_login_signup_signin_selection.xml | 86 +++++++++++++++++++ vector/src/main/res/values/strings_riotX.xml | 3 + vector/src/main/res/values/styles_login.xml | 4 + 13 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/SignMode.kt create mode 100644 vector/src/main/res/drawable/ic_logo_matrix_org.xml create mode 100644 vector/src/main/res/layout/fragment_login_signup_signin_selection.xml diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 7c9f6489ee..00c6f6c865 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -17,6 +17,8 @@ msisdn pbkdf pkcs + signin + signup \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 8192d26a30..d1690f46af 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -25,4 +25,5 @@ sealed class LoginAction : VectorViewModelAction { data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() data class UpdateServerType(val serverType: ServerType) : LoginAction() + data class UpdateSignMode(val signMode: SignMode) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index ecf3fa45e2..65db61a1d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -61,6 +61,7 @@ class LoginActivity : VectorBaseActivity() { when (it) { is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerSelectionFragment::class.java) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected() is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } @@ -76,11 +77,15 @@ class LoginActivity : VectorBaseActivity() { } } + private fun onSignModeSelected() { + // TODO + } + private fun onServerSelectionDone() = withState(loginViewModel) { when (it.serverType) { ServerType.MatrixOrg -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) ServerType.Modular, - ServerType.Other -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginEnterHomeServerFragment::class.java) + ServerType.Other -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, LoginEnterHomeServerFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index 46a6c213d5..6f7fc174d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -22,6 +22,7 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() + object OnSignModeSelected : LoginNavigation() object OpenSsoLoginFallback : LoginNavigation() object GoBack : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt new file mode 100644 index 0000000000..ad99037f53 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 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.riotx.features.login + +import androidx.core.view.isVisible +import butterknife.OnClick +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* +import javax.inject.Inject + +/** + * + */ +class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection + + private fun updateViews(serverType: ServerType) { + when (serverType) { + ServerType.MatrixOrg -> { + loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginSignupSigninServerIcon.isVisible = true + loginSignupSigninTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_modular) + loginSignupSigninServerIcon.isVisible = true + loginSignupSigninTitle.text = getString(R.string.login_connect_to, "TODO MODULAR NAME") + loginSignupSigninText.text = "TODO MODULAR URL" + } + ServerType.Other -> { + loginSignupSigninServerIcon.isVisible = false + loginSignupSigninTitle.text = getString(R.string.login_server_other_title) + loginSignupSigninText.text = "TODO SERVER URL" + } + } + } + + @OnClick(R.id.loginSignupSigninSignUp) + fun signUp() { + viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + } + + @OnClick(R.id.loginSignupSigninSignIn) + fun signIn() { + viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + } + + override fun invalidate() = withState(viewModel) { + updateViews(it.serverType) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index a596603bc6..0dad62a816 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -63,6 +63,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi override fun handle(action: LoginAction) { when (action) { is LoginAction.UpdateServerType -> handleUpdateServerType(action) + is LoginAction.UpdateSignMode -> handleUpdateSignMode(action) is LoginAction.InitWith -> handleInitWith(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) @@ -70,6 +71,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun handleUpdateSignMode(action: LoginAction.UpdateSignMode) { + setState { + copy( + signMode = action.signMode + ) + } + } + private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 5c00614b09..bfa81a55ef 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.Uninitialized data class LoginViewState( val serverType: ServerType = ServerType.MatrixOrg, + val signMode: SignMode = SignMode.SignUp, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt new file mode 100644 index 0000000000..e8c9b9a734 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 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.riotx.features.login + +enum class SignMode { + // Account creation + SignUp, + // Login + SignIn +} diff --git a/vector/src/main/res/drawable/ic_logo_matrix_org.xml b/vector/src/main/res/drawable/ic_logo_matrix_org.xml new file mode 100644 index 0000000000..13a05fba4f --- /dev/null +++ b/vector/src/main/res/drawable/ic_logo_matrix_org.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index 58bc682d2a..3cd7bd3fb9 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -57,12 +57,11 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginServerText"> - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 1b92dca1ed..36318ec52d 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -38,5 +38,8 @@ Custom & advanced settings Continue + Connect to %1$s + Sign Up + Sign In diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml index ffcd5bc29e..cac4351c3f 100644 --- a/vector/src/main/res/values/styles_login.xml +++ b/vector/src/main/res/values/styles_login.xml @@ -11,4 +11,8 @@ false + + \ No newline at end of file From 6525314af840e98ce0203e0c4f6e65d5270d8f76 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 15:25:43 +0100 Subject: [PATCH 007/132] Login screens: server ur form --- vector/src/main/AndroidManifest.xml | 4 +- .../homeserver/ServerUrlsRepository.kt | 6 +- .../riotx/features/login/LoginActivity.kt | 13 +- .../riotx/features/login/LoginFragment.kt | 57 +------- .../login/LoginServerSelectionFragment.kt | 2 + .../login/LoginServerUrlFormFragment.kt | 94 +++++++++++++ vector/src/main/res/layout/fragment_login.xml | 87 ++++++------ .../fragment_login_server_selection.xml | 5 +- .../layout/fragment_login_server_url_form.xml | 125 ++++++++++++++++++ ...fragment_login_signup_signin_selection.xml | 5 +- .../main/res/layout/fragment_login_splash.xml | 3 +- vector/src/main/res/values/config.xml | 1 - vector/src/main/res/values/strings_riotX.xml | 12 +- vector/src/main/res/values/styles_login.xml | 2 + 14 files changed, 304 insertions(+), 112 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_server_url_form.xml diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 0c9bac61a1..5f1687c9c9 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -33,7 +33,9 @@ - + addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) ServerType.Modular, - ServerType.Other -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, LoginEnterHomeServerFragment::class.java) + ServerType.Other -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerUrlFormFragment::class.java) + } + } + + private fun onSignModeSelected() = withState(loginViewModel) { + when (it.signMode) { + SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, SignUpFragment::class.java) + SignMode.SignIn -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 4f918baa8a..582567aba5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -18,20 +18,15 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View -import android.view.inputmethod.EditorInfo import android.widget.Toast import androidx.core.view.isVisible import androidx.transition.TransitionManager import com.airbnb.mvrx.* -import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R -import im.vector.riotx.core.extensions.setTextWithColoredPart import im.vector.riotx.core.extensions.showPassword -import im.vector.riotx.core.utils.openUrlInExternalBrowser -import im.vector.riotx.features.homeserver.ServerUrlsRepository import io.reactivex.Observable -import io.reactivex.functions.Function3 +import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.synthetic.main.fragment_login.* import javax.inject.Inject @@ -49,41 +44,8 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupNotice() - setupAuthButton() + setupLoginButton() setupPasswordReveal() - - homeServerField.focusChanges() - .subscribe { - if (!it) { - viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) - } - } - .disposeOnDestroyView() - - homeServerField.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) - return@setOnEditorActionListener true - } - return@setOnEditorActionListener false - } - - val initHsUrl = viewModel.getInitialHomeServerUrl() - if (initHsUrl != null) { - homeServerField.setText(initHsUrl) - } else { - homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext())) - } - viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) - } - - private fun setupNotice() { - riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part) - - riotx_no_registration_notice.setOnClickListener { - openUrlInExternalBrowser(requireActivity(), "https://about.riot.im/downloads") - } } private fun authenticate() { @@ -93,23 +55,21 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { viewModel.handle(LoginAction.Login(login, password)) } - private fun setupAuthButton() { + private fun setupLoginButton() { Observable .combineLatest( loginField.textChanges().map { it.trim().isNotEmpty() }, passwordField.textChanges().map { it.trim().isNotEmpty() }, - homeServerField.textChanges().map { it.trim().isNotEmpty() }, - Function3 { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty -> - isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty + BiFunction { isLoginNotEmpty, isPasswordNotEmpty -> + isLoginNotEmpty && isPasswordNotEmpty } ) .subscribeBy { authenticateButton.isEnabled = it } .disposeOnDestroyView() authenticateButton.setOnClickListener { authenticate() } - - authenticateButtonSso.setOnClickListener { openSso() } } + // TODO Move to server selection screen private fun openSso() { loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) } @@ -148,7 +108,6 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginField.isVisible = false passwordContainer.isVisible = false authenticateButton.isVisible = false - authenticateButtonSso.isVisible = false passwordShown = false renderPasswordField() } @@ -158,7 +117,6 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginField.isVisible = false passwordContainer.isVisible = false authenticateButton.isVisible = false - authenticateButtonSso.isVisible = false Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show() } is Success -> { @@ -170,7 +128,6 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginField.isVisible = true passwordContainer.isVisible = true authenticateButton.isVisible = true - authenticateButtonSso.isVisible = false if (loginField.text.isNullOrBlank() && passwordField.text.isNullOrBlank()) { // Jump focus to login loginField.requestFocus() @@ -180,13 +137,11 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginField.isVisible = false passwordContainer.isVisible = false authenticateButton.isVisible = false - authenticateButtonSso.isVisible = true } LoginMode.Unsupported -> { loginField.isVisible = false passwordContainer.isVisible = false authenticateButton.isVisible = false - authenticateButtonSso.isVisible = false Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show() } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 011100a6f5..7be5223ac7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -21,6 +21,7 @@ import android.view.View import butterknife.OnClick import com.airbnb.mvrx.withState import im.vector.riotx.R +import im.vector.riotx.core.utils.openUrlInExternalBrowser import kotlinx.android.synthetic.main.fragment_login_server_selection.* import me.gujun.android.span.span import javax.inject.Inject @@ -50,6 +51,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment textDecorationLine = "underline" onClick = { // TODO + openUrlInExternalBrowser(requireActivity(), "https://example.org") } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt new file mode 100644 index 0000000000..bf5a6b9866 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2019 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.riotx.features.login + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.view.isVisible +import butterknife.OnClick +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.utils.openUrlInExternalBrowser +import kotlinx.android.synthetic.main.fragment_login_server_url_form.* +import javax.inject.Inject + +/** + * + */ +class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_server_url_form + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // TODO Import code from Riot to clear error on TIL + loginServerUrlFormHomeServerUrl.textChanges() + .subscribe( + { + loginServerUrlFormHomeServerUrlTil.error = null + }, + { + // Ignore error + }) + .disposeOnDestroy() + + loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + submit() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + } + + @OnClick(R.id.loginServerUrlFormLearnMore) + fun learMore() { + // TODO + openUrlInExternalBrowser(requireActivity(), "https://example.org") + } + + @OnClick(R.id.loginServerUrlFormSubmit) + fun submit() { + // TODO Static check of homeserver url, empty, malformed, etc. + viewModel.handle(LoginAction.InitWith(LoginConfig(loginServerUrlFormHomeServerUrl.text.toString(), null))) + } + + override fun invalidate() = withState(viewModel) { state -> + when (state.serverType) { + ServerType.Modular -> { + loginServerUrlFormIcon.isVisible = true + loginServerUrlFormTitle.text = getString(R.string.login_connect_to_modular) + loginServerUrlFormText.text = getString(R.string.login_server_url_form_modular_text) + loginServerUrlFormLearnMore.isVisible = true + loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_modular_hint) + loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_modular_notice) + } + ServerType.Other -> { + loginServerUrlFormIcon.isVisible = false + loginServerUrlFormTitle.text = getString(R.string.login_server_other_title) + loginServerUrlFormText.text = getString(R.string.login_connect_to_a_custom_server) + loginServerUrlFormLearnMore.isVisible = false + loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_other_hint) + loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_other_notice) + } + else -> error("This fragment should not be display in matrix.org mode") + } + } +} diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 3dde5e1748..94340cb97c 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -1,3 +1,4 @@ + - + style="@style/LoginTopIcon" + android:layout_gravity="center_horizontal" /> + android:layout_marginTop="84dp" + android:textAppearance="@style/TextAppearance.Vector.Login.Title" + tools:text="@string/login_signin_to" /> + + + android:layout_marginTop="32dp" + android:hint="@string/auth_user_name_placeholder" + app:errorEnabled="true"> + android:hint="@string/auth_password_placeholder" + app:errorEnabled="true"> - + android:layout_marginTop="22dp" + android:orientation="horizontal"> - + android:layout_gravity="start" + android:text="@string/auth_forgot_password" /> - + - - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index 21835a864c..240e8866f3 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -1,3 +1,4 @@ + - - - https://matrix.org - https://matrix.org https://piwik.riot.im https://riot.im/bugreports/submit diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 36318ec52d..cfd1b6721c 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -36,10 +36,20 @@ Learn more Other Custom & advanced settings - Continue + Continue Connect to %1$s + Connect to Modular + Connect to a custom server + Sign in to %1$s Sign Up Sign In + Modular Address + Address + Premium hosting for organisations + + Enter the address of the Modular Riot or Server you want to use + Enter the address of a server or a Riot you want to connect to + diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml index cac4351c3f..81964377bd 100644 --- a/vector/src/main/res/values/styles_login.xml +++ b/vector/src/main/res/values/styles_login.xml @@ -8,10 +8,12 @@ From 7f1f98c2e53d176af2e5882a457f1bcb99b05e32 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 17:42:50 +0100 Subject: [PATCH 008/132] Login screens: reset state when navigating back --- .../im/vector/riotx/core/di/FragmentModule.kt | 7 +++ .../features/login/AbstractLoginFragment.kt | 11 +++- .../riotx/features/login/LoginAction.kt | 12 ++++- .../riotx/features/login/LoginActivity.kt | 12 +++-- .../riotx/features/login/LoginFragment.kt | 4 ++ .../riotx/features/login/LoginNavigation.kt | 2 +- .../login/LoginServerSelectionFragment.kt | 4 ++ .../login/LoginServerUrlFormFragment.kt | 51 +++++++++++++++++-- .../LoginSignUpSignInSelectionFragment.kt | 4 ++ .../features/login/LoginSplashFragment.kt | 4 ++ .../login/LoginSsoFallbackFragment.kt | 1 + .../riotx/features/login/LoginViewModel.kt | 42 ++++++++++++++- .../riotx/features/login/LoginViewState.kt | 2 +- .../vector/riotx/features/login/SignMode.kt | 1 + 14 files changed, 141 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 6ae4619033..20b53b40db 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -36,6 +36,7 @@ import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.login.LoginFragment +import im.vector.riotx.features.login.LoginServerUrlFormFragment import im.vector.riotx.features.login.LoginSsoFallbackFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment @@ -194,4 +195,10 @@ interface FragmentModule { @IntoMap @FragmentKey(PublicRoomsFragment::class) fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment + + // TODO Add all other LoginFragment + @Binds + @IntoMap + @FragmentKey(LoginServerUrlFormFragment::class) + fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 25472fa9a1..c0ff5103f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -20,12 +20,13 @@ import android.os.Bundle import android.view.View import androidx.annotation.CallSuper import com.airbnb.mvrx.activityViewModel +import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.VectorBaseFragment /** * Parent Fragment for all the login/registration screens */ -abstract class AbstractLoginFragment() : VectorBaseFragment() { +abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { protected val viewModel: LoginViewModel by activityViewModel() protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @@ -37,4 +38,12 @@ abstract class AbstractLoginFragment() : VectorBaseFragment() { loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) } + override fun onBackPressed(): Boolean { + resetViewModel() + // Do not consume the Back event + return false + } + + // Reset any modification of the viewModel by the current fragment + abstract fun resetViewModel() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index d1690f46af..310d07b746 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -20,10 +20,18 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.riotx.core.platform.VectorViewModelAction sealed class LoginAction : VectorViewModelAction { + data class UpdateServerType(val serverType: ServerType) : LoginAction() data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() + data class UpdateSignMode(val signMode: SignMode) : LoginAction() data class Login(val login: String, val password: String) : LoginAction() data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() - data class UpdateServerType(val serverType: ServerType) : LoginAction() - data class UpdateSignMode(val signMode: SignMode) : LoginAction() + + // Reset actions + open class ResetAction : LoginAction() + + object ResetHomeServerType : ResetAction() + object ResetHomeServerUrl : ResetAction() + object ResetSignMode : ResetAction() + object ResetLogin : ResetAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index cf045b95e6..3c67971098 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -61,7 +61,7 @@ class LoginActivity : VectorBaseActivity() { when (it) { is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerSelectionFragment::class.java) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } @@ -85,10 +85,12 @@ class LoginActivity : VectorBaseActivity() { } } - private fun onSignModeSelected() = withState(loginViewModel) { - when (it.signMode) { - SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, SignUpFragment::class.java) - SignMode.SignIn -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) + private fun onSignModeSelected(mode: LoginNavigation.OnSignModeSelected) { + // We cannot use the state, it is not ready... + when (mode.signMode) { + SignMode.Unknown -> error("Sign mode has to be set before calling this method") + SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, SignUpFragment::class.java) + SignMode.SignIn -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 582567aba5..166a733d8a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -98,6 +98,10 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { } } + override fun resetViewModel() { + viewModel.handle(LoginAction.ResetLogin) + } + override fun invalidate() = withState(viewModel) { state -> TransitionManager.beginDelayedTransition(login_fragment) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index 6f7fc174d4..e906bfeba8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -22,7 +22,7 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() - object OnSignModeSelected : LoginNavigation() + data class OnSignModeSelected(val signMode: SignMode) : LoginNavigation() object OpenSsoLoginFallback : LoginNavigation() object GoBack : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 7be5223ac7..0af00348ff 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -77,6 +77,10 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) } + override fun resetViewModel() { + viewModel.handle(LoginAction.ResetHomeServerType) + } + override fun invalidate() = withState(viewModel) { updateSelectedChoice(it.serverType) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index bf5a6b9866..7304f15cf6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -16,14 +16,16 @@ package im.vector.riotx.features.login +import android.annotation.SuppressLint import android.os.Bundle import android.view.View import android.view.inputmethod.EditorInfo import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.withState +import com.airbnb.mvrx.* import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.utils.openUrlInExternalBrowser import kotlinx.android.synthetic.main.fragment_login_server_url_form.* import javax.inject.Inject @@ -31,7 +33,9 @@ import javax.inject.Inject /** * */ -class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() { +class LoginServerUrlFormFragment @Inject constructor( + private val errorFormatter: ErrorFormatter +) : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_server_url_form @@ -64,10 +68,29 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() openUrlInExternalBrowser(requireActivity(), "https://example.org") } + override fun resetViewModel() { + viewModel.handle(LoginAction.ResetHomeServerUrl) + } + + @SuppressLint("SetTextI18n") @OnClick(R.id.loginServerUrlFormSubmit) fun submit() { - // TODO Static check of homeserver url, empty, malformed, etc. - viewModel.handle(LoginAction.InitWith(LoginConfig(loginServerUrlFormHomeServerUrl.text.toString(), null))) + // Static check of homeserver url, empty, malformed, etc. + var serverUrl = loginServerUrlFormHomeServerUrl.text.toString() + + when { + serverUrl.isBlank() -> { + loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) + } + else -> { + if (serverUrl.startsWith("http").not()) { + serverUrl = "https://$serverUrl" + loginServerUrlFormHomeServerUrl.setText(serverUrl) + + } + viewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) + } + } } override fun invalidate() = withState(viewModel) { state -> @@ -90,5 +113,25 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() } else -> error("This fragment should not be display in matrix.org mode") } + + when (state.asyncHomeServerLoginFlowRequest) { + is Uninitialized -> { + progressBar.isVisible = false + touchArea.isVisible = false + } + is Loading -> { + progressBar.isVisible = true + touchArea.isVisible = true + } + is Fail -> { + progressBar.isVisible = false + touchArea.isVisible = false + // TODO Error text is not correct + loginServerUrlFormHomeServerUrlTil.error = errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error) + } + is Success -> { + // The home server is valid, the next screen will be opened by the Activity + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index ad99037f53..eb855f93c1 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -64,6 +64,10 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } + override fun resetViewModel() { + viewModel.handle(LoginAction.ResetSignMode) + } + override fun invalidate() = withState(viewModel) { updateViews(it.serverType) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt index 672502a167..33db8fa81a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -31,4 +31,8 @@ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { fun getStarted() { loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection) } + + override fun resetViewModel() { + // Nothing to do + } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index f77b36b1d9..74ff7bb4bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -44,6 +44,7 @@ import javax.inject.Inject /** * Only login is supported for the moment + * TODO Migrate to new flow */ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnBackPressed { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 0dad62a816..227ddf89da 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -68,6 +68,44 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) is LoginAction.SsoLoginSuccess -> handleSsoLoginSuccess(action) + is LoginAction.ResetAction -> handleResetAction(action) + } + } + + private fun handleResetAction(action: LoginAction.ResetAction) { + // Cancel any request + currentTask?.cancel() + currentTask = null + + when (action) { + LoginAction.ResetLogin -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + } + LoginAction.ResetHomeServerUrl -> { + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized + ) + } + } + LoginAction.ResetHomeServerType -> { + setState { + copy( + serverType = ServerType.MatrixOrg + ) + } + } + LoginAction.ResetSignMode -> { + setState { + copy( + signMode = SignMode.Unknown + ) + } + } } } @@ -107,7 +145,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } - authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback { + currentTask = authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback { override fun onSuccess(data: Session) { onSessionCreated(data) } @@ -153,7 +191,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun handleUpdateHomeserver(action: LoginAction.UpdateHomeServer) = withState { state -> - var newConfig: HomeServerConnectionConfig? = null Try { val homeServerUri = action.homeServerUrl @@ -167,6 +204,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi && state.asyncHomeServerLoginFlowRequest is Success) return@withState currentTask?.cancel() + currentTask = null homeServerConnectionConfig = newConfig val homeServerConnectionConfigFinal = homeServerConnectionConfig diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index bfa81a55ef..542bc0799d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -22,7 +22,7 @@ import com.airbnb.mvrx.Uninitialized data class LoginViewState( val serverType: ServerType = ServerType.MatrixOrg, - val signMode: SignMode = SignMode.SignUp, + val signMode: SignMode = SignMode.Unknown, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt index e8c9b9a734..b793a0fe1d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.login enum class SignMode { + Unknown, // Account creation SignUp, // Login From 3c93807fe63cead9e5aff7b5c68a8904d8c328ca Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 17:46:53 +0100 Subject: [PATCH 009/132] Login screens: add some doc --- .../java/im/vector/riotx/features/login/LoginFragment.kt | 5 +++-- .../riotx/features/login/LoginServerSelectionFragment.kt | 2 +- .../riotx/features/login/LoginServerUrlFormFragment.kt | 2 +- .../features/login/LoginSignUpSignInSelectionFragment.kt | 2 +- .../im/vector/riotx/features/login/LoginSplashFragment.kt | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 166a733d8a..4a4ca5fce0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -32,11 +32,12 @@ import kotlinx.android.synthetic.main.fragment_login.* import javax.inject.Inject /** - * What can be improved: - * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect + * In this screen, the user is asked for login and password to sign in to a homeserver. + * He also can reset his password */ class LoginFragment @Inject constructor() : AbstractLoginFragment() { + // TODO Move to viewState? private var passwordShown = false override fun getLayoutResId() = R.layout.fragment_login diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 0af00348ff..488bb340f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -27,7 +27,7 @@ import me.gujun.android.span.span import javax.inject.Inject /** - * + * In this screen, the user will choose between matrix.org, modular or other type of homeserver */ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 7304f15cf6..87043222f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -31,7 +31,7 @@ import kotlinx.android.synthetic.main.fragment_login_server_url_form.* import javax.inject.Inject /** - * + * In this screen, the user is prompted to enter a homeserver url */ class LoginServerUrlFormFragment @Inject constructor( private val errorFormatter: ErrorFormatter diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index eb855f93c1..dbf8abfe49 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -24,7 +24,7 @@ import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import javax.inject.Inject /** - * + * In this screen, the user is asked to sign up or to sign in to the homeserver */ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt index 33db8fa81a..53de8c2c43 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -21,7 +21,7 @@ import im.vector.riotx.R import javax.inject.Inject /** - * + * In this screen, the user is viewing an introduction to what he can do with this application */ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { From c6b0ae63ea8c5f4f3c9d77fa8dfdf8ab153495fd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 21:20:45 +0100 Subject: [PATCH 010/132] Login screens: handle loading Views and global navigation - WIP --- .../im/vector/riotx/core/di/FragmentModule.kt | 12 +-- .../vector/riotx/core/error/ErrorFormatter.kt | 19 +++-- .../riotx/features/login/LoginActivity.kt | 71 +++++++++++++---- .../riotx/features/login/LoginFragment.kt | 77 ++++--------------- .../riotx/features/login/LoginNavigation.kt | 5 +- .../login/LoginServerSelectionFragment.kt | 21 ++++- .../login/LoginServerUrlFormFragment.kt | 21 ++--- .../LoginSignUpSignInSelectionFragment.kt | 4 +- .../login/LoginSsoFallbackFragment.kt | 20 ++--- .../riotx/features/login/LoginViewModel.kt | 9 ++- .../riotx/features/login/LoginViewState.kt | 10 ++- vector/src/main/res/layout/activity_login.xml | 47 +++++++++++ vector/src/main/res/layout/fragment_login.xml | 27 +------ .../layout/fragment_login_server_url_form.xml | 23 ------ vector/src/main/res/values/strings_riotX.xml | 2 + 15 files changed, 194 insertions(+), 174 deletions(-) create mode 100644 vector/src/main/res/layout/activity_login.xml diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 20b53b40db..1457519052 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -116,6 +116,12 @@ interface FragmentModule { @FragmentKey(LoginFragment::class) fun bindLoginFragment(fragment: LoginFragment): Fragment + // TODO Add all other Login Fragments + @Binds + @IntoMap + @FragmentKey(LoginServerUrlFormFragment::class) + fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginSsoFallbackFragment::class) @@ -195,10 +201,4 @@ interface FragmentModule { @IntoMap @FragmentKey(PublicRoomsFragment::class) fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment - - // TODO Add all other LoginFragment - @Binds - @IntoMap - @FragmentKey(LoginServerUrlFormFragment::class) - fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 10c4fe3354..d08675ea1b 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -41,12 +41,19 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi } } is Failure.ServerError -> { - if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) { - // Special case for terms and conditions - stringProvider.getString(R.string.error_terms_not_accepted) - } else { - throwable.error.message.takeIf { it.isNotEmpty() } - ?: throwable.error.code.takeIf { it.isNotEmpty() } + when { + throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { + // Special case for terms and conditions + stringProvider.getString(R.string.error_terms_not_accepted) + } + throwable.error.code == MatrixError.FORBIDDEN + && throwable.error.message == "Invalid password" -> { + stringProvider.getString(R.string.auth_invalid_login_param) + } + else -> { + throwable.error.message.takeIf { it.isNotEmpty() } + ?: throwable.error.code.takeIf { it.isNotEmpty() } + } } } else -> throwable.localizedMessage diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 3c67971098..0ab10134e9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -18,6 +18,8 @@ package im.vector.riotx.features.login import android.content.Context import android.content.Intent +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel @@ -29,8 +31,12 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity +import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject +/** + * The LoginActivity manages the fragment navigation and also display the loading View + */ class LoginActivity : VectorBaseActivity() { private val loginViewModel: LoginViewModel by viewModel() @@ -42,16 +48,17 @@ class LoginActivity : VectorBaseActivity() { injector.inject(this) } - override fun getLayoutRes() = R.layout.activity_simple + override fun getLayoutRes() = R.layout.activity_login override fun initUiAndData() { if (isFirstCreation()) { - addFragment(R.id.simpleFragmentContainer, LoginSplashFragment::class.java) + addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) } // Get config extra val loginConfig = intent.getParcelableExtra(EXTRA_CONFIG) if (loginConfig != null && isFirstCreation()) { + // TODO Check this loginViewModel.handle(LoginAction.InitWith(loginConfig)) } @@ -59,29 +66,59 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerSelectionFragment::class.java) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) - is LoginNavigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) - is LoginNavigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) + is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved(it) + is LoginNavigation.OnSsoLoginFallbackError -> onSsoLoginFallbackError(it) } } .disposeOnDestroy() - loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) { - if (it is Success) { - val intent = HomeActivity.newIntent(this) - startActivity(intent) - finish() - } + loginViewModel + .subscribe(this) { + updateWithState(it) + } + .disposeOnDestroy() + } + + private fun onLoginFlowRetrieved(onLoginFlowRetrieved: LoginNavigation.OnLoginFlowRetrieved) { + when (onLoginFlowRetrieved.loginMode) { + LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSsoFallbackFragment::class.java) + LoginMode.Unsupported, + LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) } } + private fun updateWithState(loginViewState: LoginViewState) { + if (loginViewState.asyncLoginAction is Success) { + val intent = HomeActivity.newIntent(this) + startActivity(intent) + finish() + return + } + + // Loading + loginLoading.isVisible = loginViewState.isLoading() + } + + private fun onSsoLoginFallbackError(onSsoLoginFallbackError: LoginNavigation.OnSsoLoginFallbackError) { + // Pop the backstack + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + // And inform the user + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(getString(R.string.login_sso_error_message, onSsoLoginFallbackError.description, onSsoLoginFallbackError.errorCode)) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun onServerSelectionDone() = withState(loginViewModel) { when (it.serverType) { - ServerType.MatrixOrg -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) + ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.Modular, - ServerType.Other -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginServerUrlFormFragment::class.java) + ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerUrlFormFragment::class.java) } } @@ -89,8 +126,8 @@ class LoginActivity : VectorBaseActivity() { // We cannot use the state, it is not ready... when (mode.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.simpleFragmentContainer, SignUpFragment::class.java) - SignMode.SignIn -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginFragment::class.java) + SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.loginFragmentContainer, SignUpFragment::class.java) + SignMode.SignIn -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 4a4ca5fce0..571305b722 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -18,12 +18,14 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View -import android.widget.Toast -import androidx.core.view.isVisible import androidx.transition.TransitionManager -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.showPassword import io.reactivex.Observable import io.reactivex.functions.BiFunction @@ -35,7 +37,9 @@ import javax.inject.Inject * In this screen, the user is asked for login and password to sign in to a homeserver. * He also can reset his password */ -class LoginFragment @Inject constructor() : AbstractLoginFragment() { +class LoginFragment @Inject constructor( + private val errorFormatter: ErrorFormatter +) : AbstractLoginFragment() { // TODO Move to viewState? private var passwordShown = false @@ -70,10 +74,10 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { authenticateButton.setOnClickListener { authenticate() } } - // TODO Move to server selection screen - private fun openSso() { - loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) - } +// // TODO Move to server selection screen +// private fun openSso() { +// loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) +// } private fun setupPasswordReveal() { passwordShown = false @@ -106,65 +110,16 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { override fun invalidate() = withState(viewModel) { state -> TransitionManager.beginDelayedTransition(login_fragment) - when (state.asyncHomeServerLoginFlowRequest) { - is Incomplete -> { - progressBar.isVisible = true - touchArea.isVisible = true - loginField.isVisible = false - passwordContainer.isVisible = false - authenticateButton.isVisible = false - passwordShown = false - renderPasswordField() - } - is Fail -> { - progressBar.isVisible = false - touchArea.isVisible = false - loginField.isVisible = false - passwordContainer.isVisible = false - authenticateButton.isVisible = false - Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show() - } - is Success -> { - progressBar.isVisible = false - touchArea.isVisible = false - - when (state.asyncHomeServerLoginFlowRequest()) { - LoginMode.Password -> { - loginField.isVisible = true - passwordContainer.isVisible = true - authenticateButton.isVisible = true - if (loginField.text.isNullOrBlank() && passwordField.text.isNullOrBlank()) { - // Jump focus to login - loginField.requestFocus() - } - } - LoginMode.Sso -> { - loginField.isVisible = false - passwordContainer.isVisible = false - authenticateButton.isVisible = false - } - LoginMode.Unsupported -> { - loginField.isVisible = false - passwordContainer.isVisible = false - authenticateButton.isVisible = false - Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show() - } - } - } - } - when (state.asyncLoginAction) { is Loading -> { - progressBar.isVisible = true - touchArea.isVisible = true - + // Ensure password is hidden passwordShown = false renderPasswordField() } is Fail -> { - progressBar.isVisible = false - touchArea.isVisible = false - Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show() + // TODO Handle error text properly + // TODO Reset error when text is changed + passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncLoginAction.error) } // Success is handled by the LoginActivity is Success -> Unit diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index e906bfeba8..ba1e327e3a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -22,7 +22,8 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() + data class OnLoginFlowRetrieved(val loginMode: LoginMode) : LoginNavigation() data class OnSignModeSelected(val signMode: SignMode) : LoginNavigation() - object OpenSsoLoginFallback : LoginNavigation() - object GoBack : LoginNavigation() + //object OpenSsoLoginFallback : LoginNavigation() + data class OnSsoLoginFallbackError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 488bb340f5..59be9c7aa8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -19,6 +19,8 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View import butterknife.OnClick +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Success import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.utils.openUrlInExternalBrowser @@ -73,8 +75,13 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment } @OnClick(R.id.loginServerSubmit) - fun submit() { - loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) + fun submit() = withState(viewModel) { + if (it.serverType == ServerType.MatrixOrg) { + // Request login flow here + viewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) + } else { + loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) + } } override fun resetViewModel() { @@ -83,5 +90,15 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment override fun invalidate() = withState(viewModel) { updateSelectedChoice(it.serverType) + + when (it.asyncHomeServerLoginFlowRequest) { + is Fail -> { + // TODO Display error in a dialog? + } + is Success -> { + // The home server url is valid + loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved(it.asyncHomeServerLoginFlowRequest.invoke())) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 87043222f9..2ea5869448 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -22,7 +22,9 @@ import android.view.View import android.view.inputmethod.EditorInfo import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R import im.vector.riotx.core.error.ErrorFormatter @@ -115,22 +117,13 @@ class LoginServerUrlFormFragment @Inject constructor( } when (state.asyncHomeServerLoginFlowRequest) { - is Uninitialized -> { - progressBar.isVisible = false - touchArea.isVisible = false - } - is Loading -> { - progressBar.isVisible = true - touchArea.isVisible = true - } - is Fail -> { - progressBar.isVisible = false - touchArea.isVisible = false + is Fail -> { // TODO Error text is not correct loginServerUrlFormHomeServerUrlTil.error = errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error) } - is Success -> { - // The home server is valid, the next screen will be opened by the Activity + is Success -> { + // The home server url is valid + loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved(state.asyncHomeServerLoginFlowRequest.invoke())) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index dbf8abfe49..23c94425bc 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -55,13 +55,13 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr @OnClick(R.id.loginSignupSigninSignUp) fun signUp() { viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected(SignMode.SignUp)) } @OnClick(R.id.loginSignupSigninSignIn) fun signIn() { viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected(SignMode.SignIn)) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index 74ff7bb4bb..36ab47bb55 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -30,13 +30,10 @@ import android.webkit.SslErrorHandler import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AlertDialog -import com.airbnb.mvrx.activityViewModel import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R -import im.vector.riotx.core.platform.OnBackPressed -import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_login_sso_fallback.* import timber.log.Timber import java.net.URLDecoder @@ -44,14 +41,10 @@ import javax.inject.Inject /** * Only login is supported for the moment - * TODO Migrate to new flow */ -class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnBackPressed { +class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { - private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel - private val viewModel: LoginViewModel by activityViewModel() - - var homeServerUrl: String = "" + private var homeServerUrl: String = "" enum class Mode { MODE_LOGIN, @@ -71,7 +64,6 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB login_sso_fallback_toolbar.title = getString(R.string.login) setupWebview() - loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) } @SuppressLint("SetJavaScriptEnabled") @@ -146,7 +138,7 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB super.onReceivedError(view, errorCode, description, failingUrl) // on error case, close this fragment - loginSharedActionViewModel.post(LoginNavigation.GoBack) + loginSharedActionViewModel.post(LoginNavigation.OnSsoLoginFallbackError(errorCode, description, failingUrl)) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { @@ -294,12 +286,16 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB } } + override fun resetViewModel() { + // Nothing to do + } + override fun onBackPressed(): Boolean { return if (login_sso_fallback_webview.canGoBack()) { login_sso_fallback_webview.goBack() true } else { - false + super.onBackPressed() } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 227ddf89da..ab50d6f036 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -78,14 +78,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi currentTask = null when (action) { - LoginAction.ResetLogin -> { + LoginAction.ResetLogin -> { setState { copy( asyncLoginAction = Uninitialized ) } } - LoginAction.ResetHomeServerUrl -> { + LoginAction.ResetHomeServerUrl -> { setState { copy( asyncHomeServerLoginFlowRequest = Uninitialized @@ -99,10 +99,11 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } } - LoginAction.ResetSignMode -> { + LoginAction.ResetSignMode -> { setState { copy( - signMode = SignMode.Unknown + signMode = SignMode.Unknown, + asyncHomeServerLoginFlowRequest = Uninitialized ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 542bc0799d..0d1b592611 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.login import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized @@ -25,7 +26,14 @@ data class LoginViewState( val signMode: SignMode = SignMode.Unknown, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized -) : MvRxState +) : MvRxState { + + fun isLoading(): Boolean { + // TODO Add other async here + return asyncLoginAction is Loading + || asyncHomeServerLoginFlowRequest is Loading + } +} enum class LoginMode { Password, diff --git a/vector/src/main/res/layout/activity_login.xml b/vector/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000000..0add6040a7 --- /dev/null +++ b/vector/src/main/res/layout/activity_login.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 94340cb97c..03efb60ab2 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -66,11 +66,13 @@ android:layout_marginTop="16dp"> + app:errorEnabled="true" + app:errorIconDrawable="@null"> - - - - diff --git a/vector/src/main/res/layout/fragment_login_server_url_form.xml b/vector/src/main/res/layout/fragment_login_server_url_form.xml index f61ce48daf..5f6cc80072 100644 --- a/vector/src/main/res/layout/fragment_login_server_url_form.xml +++ b/vector/src/main/res/layout/fragment_login_server_url_form.xml @@ -98,28 +98,5 @@ - - - - diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index cfd1b6721c..c64b020eba 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -52,4 +52,6 @@ Enter the address of the Modular Riot or Server you want to use Enter the address of a server or a Riot you want to connect to + An error occurred when loading the page: %1$s (%2$d) + From d50b690523a68555b6dadc2d5539e2f4f173e21c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 21:48:06 +0100 Subject: [PATCH 011/132] Login screens: improve LoginFragment --- .../im/vector/riotx/core/utils/ViewUtils.kt | 55 +++++++++++++++++++ .../riotx/features/login/LoginFragment.kt | 39 +++++++++++-- .../riotx/features/login/LoginViewModel.kt | 1 + vector/src/main/res/layout/fragment_login.xml | 20 +++++-- 4 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt new file mode 100644 index 0000000000..335b9112ef --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 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.riotx.core.utils + +import android.text.Editable +import android.view.ViewGroup +import androidx.core.view.children +import com.google.android.material.textfield.TextInputLayout +import im.vector.riotx.core.platform.SimpleTextWatcher + +/** + * Find all TextInputLayout in a ViewGroup and in all its descendants + */ +fun ViewGroup.findAllTextInputLayout(): List { + val res = ArrayList() + + children.forEach { + if (it is TextInputLayout) { + res.add(it) + } else if (it is ViewGroup) { + // Recursive call + res.addAll(it.findAllTextInputLayout()) + } + } + + return res +} + +/** + * Add a text change listener to all TextInputEditText to reset error on its TextInputLayout when the text is changed + */ +fun autoResetTextInputLayoutErrors(textInputLayouts: List) { + textInputLayouts.forEach { + it.editText?.addTextChangedListener(object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + // Reset the error + it.error = null + } + }) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 571305b722..4cd25d7640 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -18,7 +18,7 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View -import androidx.transition.TransitionManager +import androidx.core.view.isVisible import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success @@ -69,9 +69,14 @@ class LoginFragment @Inject constructor( isLoginNotEmpty && isPasswordNotEmpty } ) - .subscribeBy { authenticateButton.isEnabled = it } - .disposeOnDestroyView() - authenticateButton.setOnClickListener { authenticate() } + .subscribeBy { + loginFieldTil.error = null + passwordFieldTil.error = null + loginSubmit.isEnabled = it + } + .disposeOnDestroy() + + loginSubmit.setOnClickListener { authenticate() } } // // TODO Move to server selection screen @@ -108,7 +113,28 @@ class LoginFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { state -> - TransitionManager.beginDelayedTransition(login_fragment) + when (state.serverType) { + ServerType.MatrixOrg -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginNotice.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_modular) + // TODO + loginTitle.text = getString(R.string.login_connect_to, "TODO") + // TODO Remove https:// + loginNotice.text = viewModel.getHomeServerUrl() + } + ServerType.Other -> { + loginServerIcon.isVisible = false + loginTitle.text = getString(R.string.login_server_other_title) + // TODO Remove https:// + loginNotice.text = viewModel.getHomeServerUrl() + } + } when (state.asyncLoginAction) { is Loading -> { @@ -117,8 +143,9 @@ class LoginFragment @Inject constructor( renderPasswordField() } is Fail -> { + // TODO This does not work, we want the error to be on without text. Fix that + loginFieldTil.error = "" // TODO Handle error text properly - // TODO Reset error when text is changed passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncLoginAction.error) } // Success is handled by the LoginActivity diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index ab50d6f036..96f8d5ea99 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -152,6 +152,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException setState { copy( asyncLoginAction = Fail(failure) diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 03efb60ab2..708afd6943 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -25,24 +25,32 @@ style="@style/LoginTopIcon" android:layout_gravity="center_horizontal" /> - + + + android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small" + tools:text="@string/login_server_matrix_org_text" /> Date: Thu, 14 Nov 2019 21:54:03 +0100 Subject: [PATCH 012/132] Login screens: re-click on an item submit it --- .../login/LoginServerSelectionFragment.kt | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 59be9c7aa8..fb0ec25ab0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -52,7 +52,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment text = getString(R.string.login_server_modular_learn_more) textDecorationLine = "underline" onClick = { - // TODO + // TODO this does not work openUrlInExternalBrowser(requireActivity(), "https://example.org") } } @@ -61,17 +61,32 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment @OnClick(R.id.loginServerChoiceMatrixOrg) fun selectMatrixOrg() { - viewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + if (loginServerChoiceMatrixOrg.isChecked) { + // Consider this is a submit + submit() + } else { + viewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + } } @OnClick(R.id.loginServerChoiceModular) fun selectModular() { - viewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + if (loginServerChoiceModular.isChecked) { + // Consider this is a submit + submit() + } else { + viewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + } } @OnClick(R.id.loginServerChoiceOther) fun selectOther() { - viewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + if (loginServerChoiceOther.isChecked) { + // Consider this is a submit + submit() + } else { + viewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + } } @OnClick(R.id.loginServerSubmit) From 5b9876a20cc3f12c363e3603eccf41997023bd16 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 Nov 2019 22:16:50 +0100 Subject: [PATCH 013/132] Login screens: Fix navigation issue --- .../vector/riotx/core/error/ErrorFormatter.kt | 4 +++ .../riotx/features/login/LoginActivity.kt | 27 +++++++++++-------- .../vector/riotx/features/login/LoginMode.kt | 23 ++++++++++++++++ .../riotx/features/login/LoginNavigation.kt | 2 +- .../login/LoginServerSelectionFragment.kt | 2 +- .../login/LoginServerUrlFormFragment.kt | 2 +- .../riotx/features/login/LoginViewState.kt | 15 +++++------ 7 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index d08675ea1b..65f48f5e7b 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import java.net.SocketTimeoutException +import java.net.UnknownHostException import javax.inject.Inject class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) { @@ -36,6 +37,9 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi is Failure.NetworkConnection -> { if (throwable.ioException is SocketTimeoutException) { stringProvider.getString(R.string.error_network_timeout) + } else if (throwable.ioException is UnknownHostException) { + // Invalid homeserver? + stringProvider.getString(R.string.login_error_unknown_host) } else { stringProvider.getString(R.string.error_no_network) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 0ab10134e9..0eaaea33d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -21,7 +21,6 @@ import android.content.Intent import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager -import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import im.vector.riotx.R @@ -69,7 +68,7 @@ class LoginActivity : VectorBaseActivity() { is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) - is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved(it) + is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() is LoginNavigation.OnSsoLoginFallbackError -> onSsoLoginFallbackError(it) } } @@ -82,16 +81,12 @@ class LoginActivity : VectorBaseActivity() { .disposeOnDestroy() } - private fun onLoginFlowRetrieved(onLoginFlowRetrieved: LoginNavigation.OnLoginFlowRetrieved) { - when (onLoginFlowRetrieved.loginMode) { - LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSsoFallbackFragment::class.java) - LoginMode.Unsupported, - LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) - } + private fun onLoginFlowRetrieved() { + addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) } private fun updateWithState(loginViewState: LoginViewState) { - if (loginViewState.asyncLoginAction is Success) { + if (loginViewState.isUserLogged()) { val intent = HomeActivity.newIntent(this) startActivity(intent) finish() @@ -123,11 +118,21 @@ class LoginActivity : VectorBaseActivity() { } private fun onSignModeSelected(mode: LoginNavigation.OnSignModeSelected) { - // We cannot use the state, it is not ready... + // We cannot use the state to get the SignMode, it is not ready... when (mode.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.loginFragmentContainer, SignUpFragment::class.java) - SignMode.SignIn -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + SignMode.SignIn -> { + // It depends on the LoginMode + withState(loginViewModel) { + when (it.asyncHomeServerLoginFlowRequest.invoke()) { + null -> error("Developer error") + LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSsoFallbackFragment::class.java) + LoginMode.Unsupported -> TODO() // TODO Import Fallback login fragment from Riot-Android + } + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt new file mode 100644 index 0000000000..ae40d3a95a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 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.riotx.features.login + +enum class LoginMode { + Password, + Sso, + Unsupported +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index ba1e327e3a..a223660708 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -22,7 +22,7 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() - data class OnLoginFlowRetrieved(val loginMode: LoginMode) : LoginNavigation() + object OnLoginFlowRetrieved : LoginNavigation() data class OnSignModeSelected(val signMode: SignMode) : LoginNavigation() //object OpenSsoLoginFallback : LoginNavigation() data class OnSsoLoginFallbackError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index fb0ec25ab0..27ae99c20b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -112,7 +112,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment } is Success -> { // The home server url is valid - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved(it.asyncHomeServerLoginFlowRequest.invoke())) + loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 2ea5869448..b5f002b353 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -123,7 +123,7 @@ class LoginServerUrlFormFragment @Inject constructor( } is Success -> { // The home server url is valid - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved(state.asyncHomeServerLoginFlowRequest.invoke())) + loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 0d1b592611..4be96d20c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -16,10 +16,7 @@ package im.vector.riotx.features.login -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.* data class LoginViewState( val serverType: ServerType = ServerType.MatrixOrg, @@ -33,10 +30,10 @@ data class LoginViewState( return asyncLoginAction is Loading || asyncHomeServerLoginFlowRequest is Loading } + + fun isUserLogged(): Boolean { + // TODO Add other async here + return asyncLoginAction is Success + } } -enum class LoginMode { - Password, - Sso, - Unsupported -} From 2849e1f8465a874fa82bb8b0ffc959ba7517d2a5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 09:59:19 +0100 Subject: [PATCH 014/132] Login screens: Splash: update icons --- .../res/drawable/ic_login_splash_lock.xml | 22 ++++++++++++++++++ .../ic_login_splash_message_circle.xml | 14 +++++++++++ .../res/drawable/ic_login_splash_sliders.xml | 14 +++++++++++ .../main/res/layout/fragment_login_splash.xml | 23 +++++++++---------- 4 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_login_splash_lock.xml create mode 100644 vector/src/main/res/drawable/ic_login_splash_message_circle.xml create mode 100644 vector/src/main/res/drawable/ic_login_splash_sliders.xml diff --git a/vector/src/main/res/drawable/ic_login_splash_lock.xml b/vector/src/main/res/drawable/ic_login_splash_lock.xml new file mode 100644 index 0000000000..26470cefce --- /dev/null +++ b/vector/src/main/res/drawable/ic_login_splash_lock.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_login_splash_message_circle.xml b/vector/src/main/res/drawable/ic_login_splash_message_circle.xml new file mode 100644 index 0000000000..81b5e9476a --- /dev/null +++ b/vector/src/main/res/drawable/ic_login_splash_message_circle.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_login_splash_sliders.xml b/vector/src/main/res/drawable/ic_login_splash_sliders.xml new file mode 100644 index 0000000000..b7c850eea7 --- /dev/null +++ b/vector/src/main/res/drawable/ic_login_splash_sliders.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index e49270ea51..b77493f6d1 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -31,13 +31,13 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashLogo" /> - @@ -46,22 +46,22 @@ android:id="@+id/loginSplashText1" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="12dp" + android:layout_marginStart="36dp" android:layout_marginTop="32dp" android:gravity="start" android:text="@string/login_splash_text1" android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@+id/loginSplashText2" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/loginSplashPicto1" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashTitle" /> - @@ -70,22 +70,21 @@ android:id="@+id/loginSplashText2" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="12dp" + android:layout_marginStart="36dp" android:layout_marginTop="16dp" android:gravity="start" android:text="@string/login_splash_text2" android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@id/loginSplashText3" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/loginSplashPicto2" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashText1" /> - @@ -94,14 +93,14 @@ android:id="@+id/loginSplashText3" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="12dp" + android:layout_marginStart="36dp" android:layout_marginTop="16dp" android:gravity="start" android:text="@string/login_splash_text3" android:textAppearance="@style/TextAppearance.Vector.Login.Text" app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/loginSplashPicto3" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSplashText2" /> Date: Fri, 15 Nov 2019 10:08:27 +0100 Subject: [PATCH 015/132] Login screens: Fix Other rendering issue --- .../res/layout/fragment_login_signup_signin_selection.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index 240e8866f3..3182818ca5 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -29,10 +29,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="172dp" + android:visibility="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninLogo" - app:layout_goneMarginTop="172dp" - tools:src="@drawable/ic_logo_matrix_org" /> + tools:src="@drawable/ic_logo_matrix_org" + tools:visibility="visible" /> Date: Fri, 15 Nov 2019 11:47:49 +0100 Subject: [PATCH 016/132] Login screens: move elements from ViewState to ViewModel --- .../features/login/AbstractLoginFragment.kt | 4 +- .../riotx/features/login/LoginActivity.kt | 11 ++- .../riotx/features/login/LoginFragment.kt | 55 +++++++------- .../riotx/features/login/LoginNavigation.kt | 2 +- .../login/LoginServerSelectionFragment.kt | 35 +++++---- .../login/LoginServerUrlFormFragment.kt | 73 ++++++++++--------- .../LoginSignUpSignInSelectionFragment.kt | 27 ++++--- .../login/LoginSsoFallbackFragment.kt | 6 +- .../riotx/features/login/LoginViewModel.kt | 26 +++---- .../riotx/features/login/LoginViewState.kt | 2 - 10 files changed, 124 insertions(+), 117 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index c0ff5103f9..9e57350b79 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -28,7 +28,7 @@ import im.vector.riotx.core.platform.VectorBaseFragment */ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { - protected val viewModel: LoginViewModel by activityViewModel() + protected val loginViewModel: LoginViewModel by activityViewModel() protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @CallSuper @@ -44,6 +44,6 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { return false } - // Reset any modification of the viewModel by the current fragment + // Reset any modification on the loginViewModel by the current fragment abstract fun resetViewModel() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 0eaaea33d4..7e90931716 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -67,7 +67,7 @@ class LoginActivity : VectorBaseActivity() { when (it) { is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected(it) + is LoginNavigation.OnSignModeSelected -> onSignModeSelected() is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() is LoginNavigation.OnSsoLoginFallbackError -> onSsoLoginFallbackError(it) } @@ -109,17 +109,16 @@ class LoginActivity : VectorBaseActivity() { .show() } - private fun onServerSelectionDone() = withState(loginViewModel) { - when (it.serverType) { + private fun onServerSelectionDone() { + when (loginViewModel.serverType) { ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.Modular, ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerUrlFormFragment::class.java) } } - private fun onSignModeSelected(mode: LoginNavigation.OnSignModeSelected) { - // We cannot use the state to get the SignMode, it is not ready... - when (mode.signMode) { + private fun onSignModeSelected() { + when (loginViewModel.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.loginFragmentContainer, SignUpFragment::class.java) SignMode.SignIn -> { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 4cd25d7640..2bc5a6171e 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -49,6 +49,7 @@ class LoginFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupUi() setupLoginButton() setupPasswordReveal() } @@ -57,7 +58,32 @@ class LoginFragment @Inject constructor( val login = loginField.text?.trim().toString() val password = passwordField.text?.trim().toString() - viewModel.handle(LoginAction.Login(login, password)) + loginViewModel.handle(LoginAction.Login(login, password)) + } + + private fun setupUi() { + when (loginViewModel.serverType) { + ServerType.MatrixOrg -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginNotice.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_modular) + // TODO + loginTitle.text = getString(R.string.login_connect_to, "TODO") + // TODO Remove https:// + loginNotice.text = loginViewModel.getHomeServerUrl() + } + ServerType.Other -> { + loginServerIcon.isVisible = false + loginTitle.text = getString(R.string.login_server_other_title) + // TODO Remove https:// + loginNotice.text = loginViewModel.getHomeServerUrl() + } + } } private fun setupLoginButton() { @@ -109,33 +135,10 @@ class LoginFragment @Inject constructor( } override fun resetViewModel() { - viewModel.handle(LoginAction.ResetLogin) + loginViewModel.handle(LoginAction.ResetLogin) } - override fun invalidate() = withState(viewModel) { state -> - when (state.serverType) { - ServerType.MatrixOrg -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(R.string.login_connect_to, "matrix.org") - loginNotice.text = getString(R.string.login_server_matrix_org_text) - } - ServerType.Modular -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_modular) - // TODO - loginTitle.text = getString(R.string.login_connect_to, "TODO") - // TODO Remove https:// - loginNotice.text = viewModel.getHomeServerUrl() - } - ServerType.Other -> { - loginServerIcon.isVisible = false - loginTitle.text = getString(R.string.login_server_other_title) - // TODO Remove https:// - loginNotice.text = viewModel.getHomeServerUrl() - } - } - + override fun invalidate() = withState(loginViewModel) { state -> when (state.asyncLoginAction) { is Loading -> { // Ensure password is hidden diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index a223660708..2c366a0eb7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -23,7 +23,7 @@ sealed class LoginNavigation : VectorSharedAction { object OpenServerSelection : LoginNavigation() object OnServerSelectionDone : LoginNavigation() object OnLoginFlowRetrieved : LoginNavigation() - data class OnSignModeSelected(val signMode: SignMode) : LoginNavigation() + object OnSignModeSelected : LoginNavigation() //object OpenSsoLoginFallback : LoginNavigation() data class OnSsoLoginFallbackError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 27ae99c20b..e819389b9c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -38,13 +38,16 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + updateSelectedChoice() initTextViews() } - private fun updateSelectedChoice(serverType: ServerType) { - loginServerChoiceMatrixOrg.isChecked = serverType == ServerType.MatrixOrg - loginServerChoiceModular.isChecked = serverType == ServerType.Modular - loginServerChoiceOther.isChecked = serverType == ServerType.Other + private fun updateSelectedChoice() { + loginViewModel.serverType.let { + loginServerChoiceMatrixOrg.isChecked = it == ServerType.MatrixOrg + loginServerChoiceModular.isChecked = it == ServerType.Modular + loginServerChoiceOther.isChecked = it == ServerType.Other + } } private fun initTextViews() { @@ -56,7 +59,6 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment openUrlInExternalBrowser(requireActivity(), "https://example.org") } } - } @OnClick(R.id.loginServerChoiceMatrixOrg) @@ -65,7 +67,8 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment // Consider this is a submit submit() } else { - viewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg)) + updateSelectedChoice() } } @@ -75,7 +78,8 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment // Consider this is a submit submit() } else { - viewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular)) + updateSelectedChoice() } } @@ -85,33 +89,32 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment // Consider this is a submit submit() } else { - viewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Other)) + updateSelectedChoice() } } @OnClick(R.id.loginServerSubmit) - fun submit() = withState(viewModel) { - if (it.serverType == ServerType.MatrixOrg) { + fun submit() { + if (loginViewModel.serverType == ServerType.MatrixOrg) { // Request login flow here - viewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) + loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) } else { loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) } } override fun resetViewModel() { - viewModel.handle(LoginAction.ResetHomeServerType) + loginViewModel.handle(LoginAction.ResetHomeServerType) } - override fun invalidate() = withState(viewModel) { - updateSelectedChoice(it.serverType) - + override fun invalidate() = withState(loginViewModel) { when (it.asyncHomeServerLoginFlowRequest) { is Fail -> { // TODO Display error in a dialog? } is Success -> { - // The home server url is valid + // LoginFlow for matrix.org has been retrieved loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index b5f002b353..c3d841369b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -44,6 +44,11 @@ class LoginServerUrlFormFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupUi() + setupHomeServerField() + } + + private fun setupHomeServerField() { // TODO Import code from Riot to clear error on TIL loginServerUrlFormHomeServerUrl.textChanges() .subscribe( @@ -64,39 +69,8 @@ class LoginServerUrlFormFragment @Inject constructor( } } - @OnClick(R.id.loginServerUrlFormLearnMore) - fun learMore() { - // TODO - openUrlInExternalBrowser(requireActivity(), "https://example.org") - } - - override fun resetViewModel() { - viewModel.handle(LoginAction.ResetHomeServerUrl) - } - - @SuppressLint("SetTextI18n") - @OnClick(R.id.loginServerUrlFormSubmit) - fun submit() { - // Static check of homeserver url, empty, malformed, etc. - var serverUrl = loginServerUrlFormHomeServerUrl.text.toString() - - when { - serverUrl.isBlank() -> { - loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) - } - else -> { - if (serverUrl.startsWith("http").not()) { - serverUrl = "https://$serverUrl" - loginServerUrlFormHomeServerUrl.setText(serverUrl) - - } - viewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) - } - } - } - - override fun invalidate() = withState(viewModel) { state -> - when (state.serverType) { + private fun setupUi() { + when (loginViewModel.serverType) { ServerType.Modular -> { loginServerUrlFormIcon.isVisible = true loginServerUrlFormTitle.text = getString(R.string.login_connect_to_modular) @@ -115,7 +89,40 @@ class LoginServerUrlFormFragment @Inject constructor( } else -> error("This fragment should not be display in matrix.org mode") } + } + @OnClick(R.id.loginServerUrlFormLearnMore) + fun learMore() { + // TODO + openUrlInExternalBrowser(requireActivity(), "https://example.org") + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetHomeServerUrl) + } + + @SuppressLint("SetTextI18n") + @OnClick(R.id.loginServerUrlFormSubmit) + fun submit() { + // Static check of homeserver url, empty, malformed, etc. + var serverUrl = loginServerUrlFormHomeServerUrl.text.toString() + + when { + serverUrl.isBlank() -> { + loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) + } + else -> { + if (serverUrl.startsWith("http").not()) { + serverUrl = "https://$serverUrl" + loginServerUrlFormHomeServerUrl.setText(serverUrl) + + } + loginViewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) + } + } + } + + override fun invalidate() = withState(loginViewModel) { state -> when (state.asyncHomeServerLoginFlowRequest) { is Fail -> { // TODO Error text is not correct diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 23c94425bc..cdb9aa8b7a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -16,9 +16,10 @@ package im.vector.riotx.features.login +import android.os.Bundle +import android.view.View import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.withState import im.vector.riotx.R import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import javax.inject.Inject @@ -30,8 +31,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection - private fun updateViews(serverType: ServerType) { - when (serverType) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + } + + private fun setupUi() { + when (loginViewModel.serverType) { ServerType.MatrixOrg -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) loginSignupSigninServerIcon.isVisible = true @@ -54,21 +61,17 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr @OnClick(R.id.loginSignupSigninSignUp) fun signUp() { - viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected(SignMode.SignUp)) + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } @OnClick(R.id.loginSignupSigninSignIn) fun signIn() { - viewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected(SignMode.SignIn)) + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } override fun resetViewModel() { - viewModel.handle(LoginAction.ResetSignMode) - } - - override fun invalidate() = withState(viewModel) { - updateViews(it.serverType) + loginViewModel.handle(LoginAction.ResetSignMode) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index 36ab47bb55..fcf34bbdc4 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -74,7 +74,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { // the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK) login_sso_fallback_webview.settings.userAgentString = "Mozilla/5.0 Google" - homeServerUrl = viewModel.getHomeServerUrl() + homeServerUrl = loginViewModel.getHomeServerUrl() if (!homeServerUrl.endsWith("/")) { homeServerUrl += "/" @@ -248,7 +248,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { refreshToken = null ) - viewModel.handle(LoginAction.SsoLoginSuccess(safeCredentials)) + loginViewModel.handle(LoginAction.SsoLoginSuccess(safeCredentials)) } } } catch (e: Exception) { @@ -273,7 +273,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { refreshToken = null ) - viewModel.handle(LoginAction.SsoLoginSuccess(credentials)) + loginViewModel.handle(LoginAction.SsoLoginSuccess(credentials)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 96f8d5ea99..f4a9b24812 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -55,6 +55,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + var serverType: ServerType = ServerType.MatrixOrg + private set + + var signMode: SignMode = SignMode.Unknown + private set + private var loginConfig: LoginConfig? = null private var homeServerConnectionConfig: HomeServerConnectionConfig? = null @@ -93,16 +99,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } LoginAction.ResetHomeServerType -> { - setState { - copy( - serverType = ServerType.MatrixOrg - ) - } + serverType = ServerType.MatrixOrg } LoginAction.ResetSignMode -> { + signMode = SignMode.Unknown setState { copy( - signMode = SignMode.Unknown, asyncHomeServerLoginFlowRequest = Uninitialized ) } @@ -111,19 +113,11 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun handleUpdateSignMode(action: LoginAction.UpdateSignMode) { - setState { - copy( - signMode = action.signMode - ) - } + signMode = action.signMode } private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { - setState { - copy( - serverType = action.serverType - ) - } + serverType = action.serverType } private fun handleInitWith(action: LoginAction.InitWith) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 4be96d20c2..0853765d63 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -19,8 +19,6 @@ package im.vector.riotx.features.login import com.airbnb.mvrx.* data class LoginViewState( - val serverType: ServerType = ServerType.MatrixOrg, - val signMode: SignMode = SignMode.Unknown, val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState { From b7bfb20a2e1ef2f0f0fa4dc8156154e865e02971 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 11:57:18 +0100 Subject: [PATCH 017/132] Login screens: login and registration fallback --- .../im/vector/riotx/core/di/FragmentModule.kt | 6 +- .../riotx/features/login/LoginAction.kt | 2 +- .../riotx/features/login/LoginActivity.kt | 35 ++++++---- .../riotx/features/login/LoginFragment.kt | 5 -- .../vector/riotx/features/login/LoginMode.kt | 8 +-- .../riotx/features/login/LoginNavigation.kt | 3 +- .../riotx/features/login/LoginViewModel.kt | 6 +- ...allbackFragment.kt => LoginWebFragment.kt} | 70 +++++++++---------- ...so_fallback.xml => fragment_login_web.xml} | 4 +- vector/src/main/res/values/strings_riotX.xml | 1 + 10 files changed, 73 insertions(+), 67 deletions(-) rename vector/src/main/java/im/vector/riotx/features/login/{LoginSsoFallbackFragment.kt => LoginWebFragment.kt} (86%) rename vector/src/main/res/layout/{fragment_login_sso_fallback.xml => fragment_login_web.xml} (86%) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 1457519052..d788f4c04c 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -37,7 +37,7 @@ import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.login.LoginFragment import im.vector.riotx.features.login.LoginServerUrlFormFragment -import im.vector.riotx.features.login.LoginSsoFallbackFragment +import im.vector.riotx.features.login.LoginWebFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment @@ -124,8 +124,8 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(LoginSsoFallbackFragment::class) - fun bindLoginSsoFallbackFragment(fragment: LoginSsoFallbackFragment): Fragment + @FragmentKey(LoginWebFragment::class) + fun bindLoginWebFragment(fragment: LoginWebFragment): Fragment @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 310d07b746..754372e82c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -24,7 +24,7 @@ sealed class LoginAction : VectorViewModelAction { data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() data class UpdateSignMode(val signMode: SignMode) : LoginAction() data class Login(val login: String, val password: String) : LoginAction() - data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() + data class WebLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() // Reset actions diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 7e90931716..c3a748f442 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -65,11 +65,11 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() - is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() - is LoginNavigation.OnSsoLoginFallbackError -> onSsoLoginFallbackError(it) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected() + is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() + is LoginNavigation.OnWebLoginError -> onWebLoginError(it) } } .disposeOnDestroy() @@ -97,14 +97,14 @@ class LoginActivity : VectorBaseActivity() { loginLoading.isVisible = loginViewState.isLoading() } - private fun onSsoLoginFallbackError(onSsoLoginFallbackError: LoginNavigation.OnSsoLoginFallbackError) { + private fun onWebLoginError(onWebLoginError: LoginNavigation.OnWebLoginError) { // Pop the backstack supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) // And inform the user AlertDialog.Builder(this) .setTitle(R.string.dialog_title_error) - .setMessage(getString(R.string.login_sso_error_message, onSsoLoginFallbackError.description, onSsoLoginFallbackError.errorCode)) + .setMessage(getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) .setPositiveButton(R.string.ok, null) .show() } @@ -124,17 +124,28 @@ class LoginActivity : VectorBaseActivity() { SignMode.SignIn -> { // It depends on the LoginMode withState(loginViewModel) { - when (it.asyncHomeServerLoginFlowRequest.invoke()) { - null -> error("Developer error") - LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) - LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginSsoFallbackFragment::class.java) - LoginMode.Unsupported -> TODO() // TODO Import Fallback login fragment from Riot-Android + when (val loginMode = it.asyncHomeServerLoginFlowRequest.invoke()) { + null -> error("Developer error") + LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginWebFragment::class.java) + is LoginMode.Unsupported -> onLoginModeNotSupported(loginMode) } } } } } + private fun onLoginModeNotSupported(unsupportedLoginMode: LoginMode.Unsupported) { + AlertDialog.Builder(this) + .setTitle(R.string.app_name) + .setMessage(getString(R.string.login_mode_not_supported, unsupportedLoginMode.types.joinToString { "'$it'" })) + .setPositiveButton(R.string.yes) { _, _ -> + addFragmentToBackstack(R.id.loginFragmentContainer, LoginWebFragment::class.java) + } + .setNegativeButton(R.string.no, null) + .show() + } + override fun onResume() { super.onResume() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 2bc5a6171e..58017d0f21 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -105,11 +105,6 @@ class LoginFragment @Inject constructor( loginSubmit.setOnClickListener { authenticate() } } -// // TODO Move to server selection screen -// private fun openSso() { -// loginSharedActionViewModel.post(LoginNavigation.OpenSsoLoginFallback) -// } - private fun setupPasswordReveal() { passwordShown = false diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt index ae40d3a95a..bea4c41cb8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt @@ -16,8 +16,8 @@ package im.vector.riotx.features.login -enum class LoginMode { - Password, - Sso, - Unsupported +sealed class LoginMode { + object Password : LoginMode() + object Sso : LoginMode() + data class Unsupported(val types: List) : LoginMode() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index 2c366a0eb7..f0ff456734 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -24,6 +24,5 @@ sealed class LoginNavigation : VectorSharedAction { object OnServerSelectionDone : LoginNavigation() object OnLoginFlowRetrieved : LoginNavigation() object OnSignModeSelected : LoginNavigation() - //object OpenSsoLoginFallback : LoginNavigation() - data class OnSsoLoginFallbackError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index f4a9b24812..9ba5fa739b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -73,7 +73,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.InitWith -> handleInitWith(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) - is LoginAction.SsoLoginSuccess -> handleSsoLoginSuccess(action) + is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) is LoginAction.ResetAction -> handleResetAction(action) } } @@ -167,7 +167,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } - private fun handleSsoLoginSuccess(action: LoginAction.SsoLoginSuccess) { + private fun handleWebLoginSuccess(action: LoginAction.WebLoginSuccess) { val homeServerConnectionConfigFinal = homeServerConnectionConfig if (homeServerConnectionConfigFinal == null) { @@ -233,7 +233,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi // SSO login is taken first data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_SSO } -> LoginMode.Sso data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_PASSWORD } -> LoginMode.Password - else -> LoginMode.Unsupported + else -> LoginMode.Unsupported(data.flows.mapNotNull { it.type }.toList()) } setState { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt similarity index 86% rename from vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt rename to vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index fcf34bbdc4..d7230f0075 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -34,47 +34,47 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R -import kotlinx.android.synthetic.main.fragment_login_sso_fallback.* +import kotlinx.android.synthetic.main.fragment_login_web.* import timber.log.Timber import java.net.URLDecoder import javax.inject.Inject /** - * Only login is supported for the moment + * This screen is displayed for SSO login and also when the application does not support login flow or registration flow + * of the homeserfver, as a fallback to login or to create an account */ -class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { +class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { - private var homeServerUrl: String = "" + private lateinit var homeServerUrl: String + private lateinit var signMode: SignMode - enum class Mode { - MODE_LOGIN, - // Not supported in RiotX for the moment - MODE_REGISTER - } - - // Mode (MODE_LOGIN or MODE_REGISTER) - private var mMode = Mode.MODE_LOGIN - - override fun getLayoutResId() = R.layout.fragment_login_sso_fallback + override fun getLayoutResId() = R.layout.fragment_login_web override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupToolbar(login_sso_fallback_toolbar) - login_sso_fallback_toolbar.title = getString(R.string.login) + homeServerUrl = loginViewModel.getHomeServerUrl() + signMode = loginViewModel.signMode.takeIf { it != SignMode.Unknown } ?: error("Developer error: Invalid sign mode") - setupWebview() + setupToolbar(loginWebToolbar) + setupTitle() + setupWebView() + } + + private fun setupTitle() { + loginWebToolbar.title = when (signMode) { + SignMode.SignIn -> getString(R.string.login_signin) + else -> getString(R.string.login_signup) + } } @SuppressLint("SetJavaScriptEnabled") - private fun setupWebview() { - login_sso_fallback_webview.settings.javaScriptEnabled = true + private fun setupWebView() { + loginWebWebView.settings.javaScriptEnabled = true // Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack // the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK) - login_sso_fallback_webview.settings.userAgentString = "Mozilla/5.0 Google" - - homeServerUrl = loginViewModel.getHomeServerUrl() + loginWebWebView.settings.userAgentString = "Mozilla/5.0 Google" if (!homeServerUrl.endsWith("/")) { homeServerUrl += "/" @@ -109,14 +109,14 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { } private fun launchWebView() { - if (mMode == Mode.MODE_LOGIN) { - login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/login/") + if (signMode == SignMode.SignIn) { + loginWebWebView.loadUrl(homeServerUrl + "_matrix/static/client/login/") } else { // MODE_REGISTER - login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/register/") + loginWebWebView.loadUrl(homeServerUrl + "_matrix/static/client/register/") } - login_sso_fallback_webview.webViewClient = object : WebViewClient() { + loginWebWebView.webViewClient = object : WebViewClient() { override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { AlertDialog.Builder(requireActivity()) @@ -131,20 +131,20 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { } false }) + .setCancelable(false) .show() } override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) - // on error case, close this fragment - loginSharedActionViewModel.post(LoginNavigation.OnSsoLoginFallbackError(errorCode, description, failingUrl)) + loginSharedActionViewModel.post(LoginNavigation.OnWebLoginError(errorCode, description, failingUrl)) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - login_sso_fallback_toolbar.subtitle = url + loginWebToolbar.subtitle = url } override fun onPageFinished(view: WebView, url: String) { @@ -160,7 +160,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { view.loadUrl(mxcJavascriptSendObjectMessage) - if (mMode == Mode.MODE_LOGIN) { + if (signMode == SignMode.SignIn) { // The function the fallback page calls when the login is complete val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" + " sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" + @@ -227,7 +227,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { if (parameters != null) { val action = parameters["action"] as String - if (mMode == Mode.MODE_LOGIN) { + if (signMode == SignMode.SignIn) { try { if (action == "onLogin") { @Suppress("UNCHECKED_CAST") @@ -248,7 +248,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { refreshToken = null ) - loginViewModel.handle(LoginAction.SsoLoginSuccess(safeCredentials)) + loginViewModel.handle(LoginAction.WebLoginSuccess(safeCredentials)) } } } catch (e: Exception) { @@ -273,7 +273,7 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { refreshToken = null ) - loginViewModel.handle(LoginAction.SsoLoginSuccess(credentials)) + loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) } } } @@ -291,8 +291,8 @@ class LoginSsoFallbackFragment @Inject constructor() : AbstractLoginFragment() { } override fun onBackPressed(): Boolean { - return if (login_sso_fallback_webview.canGoBack()) { - login_sso_fallback_webview.goBack() + return if (loginWebWebView.canGoBack()) { + loginWebWebView.goBack() true } else { super.onBackPressed() diff --git a/vector/src/main/res/layout/fragment_login_sso_fallback.xml b/vector/src/main/res/layout/fragment_login_web.xml similarity index 86% rename from vector/src/main/res/layout/fragment_login_sso_fallback.xml rename to vector/src/main/res/layout/fragment_login_web.xml index e83680d2cd..6383b9f137 100644 --- a/vector/src/main/res/layout/fragment_login_sso_fallback.xml +++ b/vector/src/main/res/layout/fragment_login_web.xml @@ -6,7 +6,7 @@ android:orientation="vertical"> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index c64b020eba..17bb99ac4a 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -53,5 +53,6 @@ Enter the address of a server or a Riot you want to connect to An error occurred when loading the page: %1$s (%2$d) + The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client? From 2871e4f5b196d34ec6fa5e014c126a0c2faa47c6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 14:22:49 +0100 Subject: [PATCH 018/132] Login screens: forget password screens --- .../matrix/android/api/auth/Authenticator.kt | 5 + .../internal/auth/DefaultAuthenticator.kt | 19 +++ .../riotx/features/login/LoginAction.kt | 2 + .../riotx/features/login/LoginActivity.kt | 16 ++- .../riotx/features/login/LoginFragment.kt | 16 ++- .../riotx/features/login/LoginNavigation.kt | 4 + .../login/LoginResetPasswordFragment.kt | 136 ++++++++++++++++++ .../LoginResetPasswordSuccessFragment.kt | 51 +++++++ .../riotx/features/login/LoginViewModel.kt | 55 ++++++- .../riotx/features/login/LoginViewState.kt | 6 +- .../layout/fragment_login_reset_password.xml | 118 +++++++++++++++ .../fragment_login_reset_password_success.xml | 62 ++++++++ vector/src/main/res/values/strings_riotX.xml | 14 ++ 13 files changed, 491 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_reset_password.xml create mode 100644 vector/src/main/res/layout/fragment_login_reset_password_success.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt index c1dfa465fb..def8293798 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt @@ -68,4 +68,9 @@ interface Authenticator { * Create a session after a SSO successful login */ fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable + + /** + * Reset user password + */ + fun resetPassword(homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index ff49d4308b..995ec0aedb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -131,6 +131,25 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated sessionManager.getOrCreateSession(sessionParams) } + override fun resetPassword(homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String, callback: MatrixCallback): Cancelable { + val job = GlobalScope.launch(coroutineDispatchers.main) { + val result = runCatching { + resetPasswordInternal(/*homeServerConnectionConfig, email, newPassword*/) + } + result.foldToCallback(callback) + } + return CancelableCoroutine(job) + } + + private fun resetPasswordInternal(/*homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String*/) { + // TODO + error("Not implemented") + //val authAPI = buildAuthAPI(homeServerConnectionConfig) + //executeRequest { + // apiCall = authAPI.getLoginFlows() + //} + } + private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 754372e82c..a03af1376d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -26,6 +26,7 @@ sealed class LoginAction : VectorViewModelAction { data class Login(val login: String, val password: String) : LoginAction() data class WebLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() + data class ResetPassword(val email: String, val newPassword: String) : LoginAction() // Reset actions open class ResetAction : LoginAction() @@ -34,4 +35,5 @@ sealed class LoginAction : VectorViewModelAction { object ResetHomeServerUrl : ResetAction() object ResetSignMode : ResetAction() object ResetLogin : ResetAction() + object ResetResetPassword : ResetAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index c3a748f442..7bc713a4f2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -65,11 +65,17 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() - is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() - is LoginNavigation.OnWebLoginError -> onWebLoginError(it) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() + is LoginNavigation.OnSignModeSelected -> onSignModeSelected() + is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() + is LoginNavigation.OnWebLoginError -> onWebLoginError(it) + is LoginNavigation.OnForgetPasswordClicked -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordFragment::class.java) + is LoginNavigation.OnResetPasswordSuccess -> { + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordSuccessFragment::class.java) + } + is LoginNavigation.OnResetPasswordSuccessDone -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 58017d0f21..14465fa48b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View import androidx.core.view.isVisible +import butterknife.OnClick import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success @@ -50,7 +51,7 @@ class LoginFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupUi() - setupLoginButton() + setupSubmitButton() setupPasswordReveal() } @@ -74,19 +75,17 @@ class LoginFragment @Inject constructor( loginServerIcon.setImageResource(R.drawable.ic_logo_modular) // TODO loginTitle.text = getString(R.string.login_connect_to, "TODO") - // TODO Remove https:// - loginNotice.text = loginViewModel.getHomeServerUrl() + loginNotice.text = loginViewModel.getHomeServerUrlSimple() } ServerType.Other -> { loginServerIcon.isVisible = false loginTitle.text = getString(R.string.login_server_other_title) - // TODO Remove https:// - loginNotice.text = loginViewModel.getHomeServerUrl() + loginNotice.text = loginViewModel.getHomeServerUrlSimple() } } } - private fun setupLoginButton() { + private fun setupSubmitButton() { Observable .combineLatest( loginField.textChanges().map { it.trim().isNotEmpty() }, @@ -105,6 +104,11 @@ class LoginFragment @Inject constructor( loginSubmit.setOnClickListener { authenticate() } } + @OnClick(R.id.forgetPasswordButton) + fun forgetPasswordClicked() { + loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + } + private fun setupPasswordReveal() { passwordShown = false diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt index f0ff456734..8eccaa0297 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt @@ -24,5 +24,9 @@ sealed class LoginNavigation : VectorSharedAction { object OnServerSelectionDone : LoginNavigation() object OnLoginFlowRetrieved : LoginNavigation() object OnSignModeSelected : LoginNavigation() + object OnForgetPasswordClicked : LoginNavigation() + object OnResetPasswordSuccess : LoginNavigation() + object OnResetPasswordSuccessDone : LoginNavigation() + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt new file mode 100644 index 0000000000..600013c5ba --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2019 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.riotx.features.login + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.showPassword +import io.reactivex.Observable +import io.reactivex.functions.BiFunction +import io.reactivex.rxkotlin.subscribeBy +import kotlinx.android.synthetic.main.fragment_login.* +import kotlinx.android.synthetic.main.fragment_login.passwordField +import kotlinx.android.synthetic.main.fragment_login.passwordFieldTil +import kotlinx.android.synthetic.main.fragment_login.passwordReveal +import kotlinx.android.synthetic.main.fragment_login_reset_password.* +import javax.inject.Inject + +/** + * In this screen, the user is asked for email and new password to reset his password + */ +class LoginResetPasswordFragment @Inject constructor( + private val errorFormatter: ErrorFormatter +) : AbstractLoginFragment() { + + private var passwordShown = false + + override fun getLayoutResId() = R.layout.fragment_login_reset_password + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + setupSubmitButton() + setupPasswordReveal() + } + + private fun setupUi() { + resetPasswordTitle.text = getString(R.string.login_reset_password_on, loginViewModel.getHomeServerUrlSimple()) + } + + private fun setupSubmitButton() { + Observable + .combineLatest( + resetPasswordEmail.textChanges().map { it.trim().isNotEmpty() }, + passwordField.textChanges().map { it.trim().isNotEmpty() }, + BiFunction { isEmailNotEmpty, isPasswordNotEmpty -> + isEmailNotEmpty && isPasswordNotEmpty + } + ) + .subscribeBy { + resetPasswordEmail.error = null + passwordFieldTil.error = null + loginSubmit.isEnabled = it + } + .disposeOnDestroy() + + resetPasswordSubmit.setOnClickListener { submit() } + } + + private fun submit() { + val email = resetPasswordEmail.text?.trim().toString() + val password = passwordField.text?.trim().toString() + + // TODO Add static check? + + loginViewModel.handle(LoginAction.ResetPassword(email, password)) + } + + private fun setupPasswordReveal() { + passwordShown = false + + passwordReveal.setOnClickListener { + passwordShown = !passwordShown + + renderPasswordField() + } + + renderPasswordField() + } + + private fun renderPasswordField() { + passwordField.showPassword(passwordShown) + + if (passwordShown) { + passwordReveal.setImageResource(R.drawable.ic_eye_closed_black) + passwordReveal.contentDescription = getString(R.string.a11y_hide_password) + } else { + passwordReveal.setImageResource(R.drawable.ic_eye_black) + passwordReveal.contentDescription = getString(R.string.a11y_show_password) + } + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetResetPassword) + } + + override fun invalidate() = withState(loginViewModel) { state -> + when (state.asyncResetPassword) { + is Loading -> { + // Ensure new password is hidden + passwordShown = false + renderPasswordField() + } + is Fail -> { + // TODO This does not work, we want the error to be on without text. Fix that + resetPasswordEmailTil.error = "" + // TODO Handle error text properly + passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error) + } + is Success -> { + loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSuccess) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt new file mode 100644 index 0000000000..20e209573e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 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.riotx.features.login + +import android.os.Bundle +import android.view.View +import butterknife.OnClick +import im.vector.riotx.R +import kotlinx.android.synthetic.main.fragment_login_reset_password_success.* +import javax.inject.Inject + +/** + * In this screen, the user is asked for email and new password to reset his password + */ +class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_reset_password_success + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + } + + private fun setupUi() { + resetPasswordSuccessNotice.text = getString(R.string.login_reset_password_success_notice, loginViewModel.resetPasswordEmail) + } + + @OnClick(R.id.resetPasswordSuccessSubmit) + fun submit() { + loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSuccessDone) + } + + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 9ba5fa739b..ac8362eb31 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -57,9 +57,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi var serverType: ServerType = ServerType.MatrixOrg private set - var signMode: SignMode = SignMode.Unknown private set + var resetPasswordEmail: String? = null + private set private var loginConfig: LoginConfig? = null @@ -74,6 +75,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.Login -> handleLogin(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) + is LoginAction.ResetPassword -> handleResetPassword(action) is LoginAction.ResetAction -> handleResetAction(action) } } @@ -109,6 +111,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } } + LoginAction.ResetResetPassword -> { + resetPasswordEmail = null + setState { + copy( + asyncResetPassword = Uninitialized + ) + } + } } } @@ -124,6 +134,45 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi loginConfig = action.loginConfig } + private fun handleResetPassword(action: LoginAction.ResetPassword) { + val homeServerConnectionConfigFinal = homeServerConnectionConfig + + if (homeServerConnectionConfigFinal == null) { + setState { + copy( + asyncResetPassword = Fail(Throwable("Bad configuration")) + ) + } + } else { + resetPasswordEmail = action.email + + setState { + copy( + asyncResetPassword = Loading() + ) + } + + currentTask = authenticator.resetPassword(homeServerConnectionConfigFinal, action.email, action.newPassword, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { + copy( + asyncResetPassword = Success(data) + ) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncResetPassword = Fail(failure) + ) + } + } + }) + } + } + private fun handleLogin(action: LoginAction.Login) { val homeServerConnectionConfigFinal = homeServerConnectionConfig @@ -259,4 +308,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi fun getHomeServerUrl(): String { return homeServerConnectionConfig?.homeServerUri?.toString() ?: "" } + + fun getHomeServerUrlSimple(): String { + return getHomeServerUrl().substringAfter("://") + } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 0853765d63..5ecfd7fad6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -16,17 +16,21 @@ package im.vector.riotx.features.login + import com.airbnb.mvrx.* data class LoginViewState( val asyncLoginAction: Async = Uninitialized, - val asyncHomeServerLoginFlowRequest: Async = Uninitialized + val asyncHomeServerLoginFlowRequest: Async = Uninitialized, + val asyncResetPassword: Async = Uninitialized ) : MvRxState { + fun isLoading(): Boolean { // TODO Add other async here return asyncLoginAction is Loading || asyncHomeServerLoginFlowRequest is Loading + || asyncResetPassword is Loading } fun isUserLogged(): Boolean { diff --git a/vector/src/main/res/layout/fragment_login_reset_password.xml b/vector/src/main/res/layout/fragment_login_reset_password.xml new file mode 100644 index 0000000000..8662b5b077 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_reset_password.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_reset_password_success.xml b/vector/src/main/res/layout/fragment_login_reset_password_success.xml new file mode 100644 index 0000000000..b777250f8d --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_reset_password_success.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 17bb99ac4a..089ac19b7e 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -38,9 +38,11 @@ Custom & advanced settings Continue + Connect to %1$s Connect to Modular Connect to a custom server + Sign in to %1$s Sign Up Sign In @@ -55,4 +57,16 @@ An error occurred when loading the page: %1$s (%2$d) The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client? + + Reset password on %1$s + A verification email will be sent to your inbox to confirm setting your new password. + Next + Email + New password + Check your inbox + + A verification email was sent to %1$s. + Tap on the link to confirm your new password. + Back to Sign In + From ca4e75a1a0b24765bd3fbc6d71d6092f92bce50c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 14:39:09 +0100 Subject: [PATCH 019/132] Login screens: Fix a few bugs --- .../im/vector/riotx/core/di/FragmentModule.kt | 30 ++++++++++++++++--- .../login/LoginResetPasswordFragment.kt | 2 +- .../LoginSignUpSignInSelectionFragment.kt | 5 ++-- .../riotx/features/login/LoginViewModel.kt | 7 ++++- vector/src/main/res/layout/fragment_login.xml | 5 +++- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index d788f4c04c..37ce9583b8 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -35,9 +35,7 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment -import im.vector.riotx.features.login.LoginFragment -import im.vector.riotx.features.login.LoginServerUrlFormFragment -import im.vector.riotx.features.login.LoginWebFragment +import im.vector.riotx.features.login.* import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment @@ -116,12 +114,36 @@ interface FragmentModule { @FragmentKey(LoginFragment::class) fun bindLoginFragment(fragment: LoginFragment): Fragment - // TODO Add all other Login Fragments @Binds @IntoMap @FragmentKey(LoginServerUrlFormFragment::class) fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LoginResetPasswordFragment::class) + fun bindLoginResetPasswordFragment(fragment: LoginResetPasswordFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginResetPasswordSuccessFragment::class) + fun bindLoginResetPasswordSuccessFragment(fragment: LoginResetPasswordSuccessFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginServerSelectionFragment::class) + fun bindLoginServerSelectionFragment(fragment: LoginServerSelectionFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginSignUpSignInSelectionFragment::class) + fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginSplashFragment::class) + fun bindLoginSplashFragment(fragment: LoginSplashFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginWebFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt index 600013c5ba..045e60e364 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -71,7 +71,7 @@ class LoginResetPasswordFragment @Inject constructor( .subscribeBy { resetPasswordEmail.error = null passwordFieldTil.error = null - loginSubmit.isEnabled = it + resetPasswordSubmit.isEnabled = it } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index cdb9aa8b7a..ec59f95874 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -48,13 +48,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr ServerType.Modular -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_modular) loginSignupSigninServerIcon.isVisible = true + // TODO loginSignupSigninTitle.text = getString(R.string.login_connect_to, "TODO MODULAR NAME") - loginSignupSigninText.text = "TODO MODULAR URL" + loginSignupSigninText.text = loginViewModel.getHomeServerUrlSimple() } ServerType.Other -> { loginSignupSigninServerIcon.isVisible = false loginSignupSigninTitle.text = getString(R.string.login_server_other_title) - loginSignupSigninText.text = "TODO SERVER URL" + loginSignupSigninText.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index ac8362eb31..abaa00283f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -309,7 +309,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi return homeServerConnectionConfig?.homeServerUri?.toString() ?: "" } + /** + * Ex: "https://matrix.org/" -> "matrix.org" + */ fun getHomeServerUrlSimple(): String { - return getHomeServerUrl().substringAfter("://") + return getHomeServerUrl() + .substringAfter("://") + .trim { it == '/' } } } diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 708afd6943..7eef66272f 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -25,11 +25,14 @@ style="@style/LoginTopIcon" android:layout_gravity="center_horizontal" /> + + Date: Fri, 15 Nov 2019 14:44:32 +0100 Subject: [PATCH 020/132] Fix compilation issue after rebase --- .../auth/registration/DefaultRegistrationService.kt | 5 +++-- .../auth/registration/DefaultRegistrationWizard.kt | 8 +++----- .../java/im/vector/riotx/features/login/LoginFragment.kt | 2 +- .../riotx/features/login/LoginResetPasswordFragment.kt | 2 +- .../riotx/features/login/LoginServerUrlFormFragment.kt | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt index 89dbda077b..fadbfe39cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.auth.registration +import dagger.Lazy import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.api.auth.registration.RegistrationWizard @@ -25,10 +26,10 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import okhttp3.OkHttpClient -import javax.inject.Provider +// TODO Add @Inject internal class DefaultRegistrationService(@Unauthenticated - private val okHttpClient: Provider, + private val okHttpClient: Lazy, private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionParamsStore: SessionParamsStore, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index d856d9211a..cda3240cb1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.auth.registration +import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams @@ -27,18 +28,15 @@ import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.AuthAPI import im.vector.matrix.android.internal.auth.SessionParamsStore -import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import okhttp3.OkHttpClient -import javax.inject.Provider internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: HomeServerConnectionConfig, - @Unauthenticated - private val okHttpClient: Provider, + private val okHttpClient: Lazy, private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionParamsStore: SessionParamsStore, @@ -100,7 +98,7 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: } private fun buildAuthAPI(): AuthAPI { - val retrofit = retrofitFactory.create(okHttpClient.get(), homeServerConnectionConfig.homeServerUri.toString()) + val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 14465fa48b..3a59ea51b5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -99,7 +99,7 @@ class LoginFragment @Inject constructor( passwordFieldTil.error = null loginSubmit.isEnabled = it } - .disposeOnDestroy() + .disposeOnDestroyView() loginSubmit.setOnClickListener { authenticate() } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt index 045e60e364..d90bfc4f84 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -73,7 +73,7 @@ class LoginResetPasswordFragment @Inject constructor( passwordFieldTil.error = null resetPasswordSubmit.isEnabled = it } - .disposeOnDestroy() + .disposeOnDestroyView() resetPasswordSubmit.setOnClickListener { submit() } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index c3d841369b..e4dd5ad7e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -58,7 +58,7 @@ class LoginServerUrlFormFragment @Inject constructor( { // Ignore error }) - .disposeOnDestroy() + .disposeOnDestroyView() loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { From 9a628c7b5dcd51f0e07190ffcf1c51554623d378 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 14:53:02 +0100 Subject: [PATCH 021/132] ktlint --- .../android/api/auth/registration/RegistrationService.kt | 1 - .../android/api/auth/registration/RegistrationWizard.kt | 1 - .../im/vector/matrix/android/api/auth/registration/Stage.kt | 6 +----- .../java/im/vector/matrix/android/api/util/Cancelable.kt | 1 - .../matrix/android/internal/auth/DefaultAuthenticator.kt | 6 +++--- .../internal/auth/registration/DefaultRegistrationWizard.kt | 1 - .../auth/registration/LocalizedFlowDataLoginTerms.kt | 2 +- .../internal/auth/registration/RegistrationParams.kt | 2 +- .../riotx/features/login/LoginServerUrlFormFragment.kt | 1 - .../java/im/vector/riotx/features/login/LoginViewState.kt | 3 --- 10 files changed, 6 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt index 7b131b922d..b314f92b9b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationService.kt @@ -21,5 +21,4 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig interface RegistrationService { fun getOrCreateRegistrationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): RegistrationWizard - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 879bd5d74b..332c1ef781 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -27,5 +27,4 @@ interface RegistrationWizard { fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable // TODO Add other method here - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt index f302b953c2..283a79348b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.api.auth.registration import im.vector.matrix.android.api.util.JsonDict - sealed class Stage(open val mandatory: Boolean) { // m.login.password @@ -45,7 +44,4 @@ sealed class Stage(open val mandatory: Boolean) { data class Other(override val mandatory: Boolean, val type: String, val params: JsonDict?) : Stage(mandatory) } - -class TermPolicies { - -} +class TermPolicies diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt index 7ec01cca10..8473f50796 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt @@ -30,5 +30,4 @@ interface Cancelable { } } - object NoOpCancellable : Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index 995ec0aedb..3a6a833dcd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -144,10 +144,10 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated private fun resetPasswordInternal(/*homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String*/) { // TODO error("Not implemented") - //val authAPI = buildAuthAPI(homeServerConnectionConfig) - //executeRequest { + // val authAPI = buildAuthAPI(homeServerConnectionConfig) + // executeRequest { // apiCall = authAPI.getLoginFlows() - //} + // } } private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index cda3240cb1..f37f2b97be 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -72,7 +72,6 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } - private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { val result = runCatching { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt index dd125e3c74..2cd52f702e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -28,4 +28,4 @@ data class LocalizedFlowDataLoginTerms( var version: String? = null, var localizedUrl: String? = null, var localizedName: String? = null -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt index db8475e06c..8d668f7f11 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt @@ -44,4 +44,4 @@ data class RegistrationParams( // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app // versions to end up in fallback because the HS returns the msisdn flow which they don't support val x_show_msisdn: Boolean? = null -) \ No newline at end of file +) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index e4dd5ad7e2..e53159da70 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -115,7 +115,6 @@ class LoginServerUrlFormFragment @Inject constructor( if (serverUrl.startsWith("http").not()) { serverUrl = "https://$serverUrl" loginServerUrlFormHomeServerUrl.setText(serverUrl) - } loginViewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 5ecfd7fad6..1f01e2348d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.login - import com.airbnb.mvrx.* data class LoginViewState( @@ -25,7 +24,6 @@ data class LoginViewState( val asyncResetPassword: Async = Uninitialized ) : MvRxState { - fun isLoading(): Boolean { // TODO Add other async here return asyncLoginAction is Loading @@ -38,4 +36,3 @@ data class LoginViewState( return asyncLoginAction is Success } } - From 3e91125872d57ea63e740f7b66e4a2c5bcde8002 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 Nov 2019 15:02:09 +0100 Subject: [PATCH 022/132] Fix issues --- .../main/java/im/vector/riotx/features/login/LoginFragment.kt | 4 ++-- .../features/login/LoginSignUpSignInSelectionFragment.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 3a59ea51b5..4161673a52 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -67,7 +67,7 @@ class LoginFragment @Inject constructor( ServerType.MatrixOrg -> { loginServerIcon.isVisible = true loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginTitle.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) loginNotice.text = getString(R.string.login_server_matrix_org_text) } ServerType.Modular -> { @@ -80,7 +80,7 @@ class LoginFragment @Inject constructor( ServerType.Other -> { loginServerIcon.isVisible = false loginTitle.text = getString(R.string.login_server_other_title) - loginNotice.text = loginViewModel.getHomeServerUrlSimple() + loginNotice.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index ec59f95874..c1023f3356 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -42,7 +42,7 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr ServerType.MatrixOrg -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) loginSignupSigninServerIcon.isVisible = true - loginSignupSigninTitle.text = getString(R.string.login_connect_to, "matrix.org") + loginSignupSigninTitle.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text) } ServerType.Modular -> { From 823acebf7879cecb7eea127f0d66eee6cacec8ed Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 09:43:38 +0100 Subject: [PATCH 023/132] Login screens: harmonize styles for containers --- vector/src/main/res/layout/fragment_login.xml | 4 ++-- .../main/res/layout/fragment_login_reset_password.xml | 4 ++-- .../res/layout/fragment_login_reset_password_success.xml | 4 ++-- .../main/res/layout/fragment_login_server_selection.xml | 7 ++----- .../main/res/layout/fragment_login_server_url_form.xml | 4 ++-- .../layout/fragment_login_signup_signin_selection.xml | 7 ++----- vector/src/main/res/layout/fragment_login_splash.xml | 4 ++-- vector/src/main/res/values/styles_login.xml | 9 ++++++++- 8 files changed, 22 insertions(+), 21 deletions(-) diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 7eef66272f..4c92a94b88 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -15,10 +15,10 @@ app:layout_constraintTop_toTopOf="parent"> + android:orientation="vertical"> + android:orientation="vertical"> + android:orientation="vertical"> + android:layout_height="match_parent"> + android:orientation="vertical"> + android:layout_height="match_parent"> + android:layout_height="match_parent"> + + - \ No newline at end of file + From 416bef790303bb1d035b6747b3aed57a88a6a8e7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 09:59:45 +0100 Subject: [PATCH 024/132] Login screens: button theme --- vector/src/main/res/layout/fragment_create_direct_room.xml | 2 +- .../main/res/layout/fragment_keys_backup_setup_step1.xml | 2 +- .../main/res/layout/fragment_keys_backup_setup_step2.xml | 2 +- .../main/res/layout/fragment_keys_backup_setup_step3.xml | 2 +- vector/src/main/res/layout/fragment_login.xml | 2 +- .../res/layout/fragment_login_signup_signin_selection.xml | 2 +- vector/src/main/res/layout/fragment_public_rooms.xml | 2 +- .../res/layout/fragment_sas_verification_display_code.xml | 2 +- .../layout/fragment_sas_verification_incoming_request.xml | 2 +- .../main/res/layout/fragment_sas_verification_start.xml | 4 ++-- vector/src/main/res/layout/item_room_filter_footer.xml | 6 +++--- vector/src/main/res/layout/vector_invite_view.xml | 2 +- vector/src/main/res/layout/view_button_state.xml | 4 ++-- vector/src/main/res/values/styles_login.xml | 7 ++++++- vector/src/main/res/values/styles_riot.xml | 4 +++- 15 files changed, 26 insertions(+), 19 deletions(-) diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index 66a040b935..f8450d1e6e 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -107,7 +107,7 @@ diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 4c92a94b88..7e6e24a48c 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -120,7 +120,7 @@ @@ -68,7 +68,7 @@ false - + + diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index c5b04de730..07091dedc0 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -136,7 +136,7 @@ - From f24889230c76baf14d97daec4265554a03b81820 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 10:27:00 +0100 Subject: [PATCH 027/132] Login screens: Captch screen (UI) --- .../features/login/LoginCaptchaFragment.kt | 34 +++++++++++++++++++ .../res/layout/fragment_login_captcha.xml | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_captcha.xml diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt new file mode 100644 index 0000000000..1490f3d5e0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 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.riotx.features.login + +import im.vector.riotx.R +import javax.inject.Inject + +/** + * In this screen, the user is asked to confirm he is not a robot + */ +class LoginCaptchaFragment @Inject constructor() : AbstractLoginFragment() { + + override fun getLayoutResId() = R.layout.fragment_login_captcha + + // TODO + + override fun resetViewModel() { + // Nothing to do + } +} diff --git a/vector/src/main/res/layout/fragment_login_captcha.xml b/vector/src/main/res/layout/fragment_login_captcha.xml new file mode 100644 index 0000000000..e3ba0ade22 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_captcha.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + From 08ea3d049e8ca723d8597a8cc129e05a427cdd03 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 15:03:07 +0100 Subject: [PATCH 028/132] Login screens: Simple Input form (UI) --- .../LoginGenericTextInputFormFragment.kt | 140 ++++++++++++++++++ ...fragment_login_generic_text_input_form.xml | 98 ++++++++++++ vector/src/main/res/values/strings_riotX.xml | 19 +++ 3 files changed, 257 insertions(+) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt create mode 100644 vector/src/main/res/layout/fragment_login_generic_text_input_form.xml diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt new file mode 100644 index 0000000000..98c30c685f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2019 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.riotx.features.login + +import android.os.Bundle +import android.os.Parcelable +import android.text.InputType +import android.view.View +import androidx.core.view.isVisible +import butterknife.OnClick +import com.airbnb.mvrx.args +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_login_generic_text_input_form.* +import javax.inject.Inject + +enum class TextInputFormFragmentMode { + SetEmailMandatory, + SetEmailOptional, + SetMsisdnMandatory, + SetMsisdnOptional, + ConfirmMsisdn +} + +@Parcelize +data class LoginGenericTextInputFormFragmentArgument( + val mode: TextInputFormFragmentMode +) : Parcelable + +/** + * In this screen, the user is asked for a text input + */ +class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFragment() { + + private val params: LoginGenericTextInputFormFragmentArgument by args() + + override fun getLayoutResId() = R.layout.fragment_login_generic_text_input_form + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + setupSubmitButton() + } + + private fun setupUi() { + when (params.mode) { + TextInputFormFragmentMode.SetEmailMandatory -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_set_email_mandatory_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + loginGenericTextInputFormOtherButton.isVisible = false + loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) + } + TextInputFormFragmentMode.SetEmailOptional -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_set_email_optional_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + loginGenericTextInputFormOtherButton.isVisible = false + loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) + } + TextInputFormFragmentMode.SetMsisdnMandatory -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_set_msisdn_mandatory_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE + loginGenericTextInputFormOtherButton.isVisible = false + loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) + } + TextInputFormFragmentMode.SetMsisdnOptional -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_set_msisdn_optional_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE + loginGenericTextInputFormOtherButton.isVisible = false + loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) + } + TextInputFormFragmentMode.ConfirmMsisdn -> { + loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title) + loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice) + loginGenericTextInputFormTil.hint = getString(R.string.login_msisdn_confirm_hint) + loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER + loginGenericTextInputFormOtherButton.isVisible = true + loginGenericTextInputFormOtherButton.text = getString(R.string.login_msisdn_confirm_send_again) + loginGenericTextInputFormSubmit.text = getString(R.string.login_msisdn_confirm_submit) + } + } + } + + @OnClick(R.id.loginGenericTextInputFormOtherButton) + fun onOtherButtonClicked() { + // TODO + } + + @OnClick(R.id.loginGenericTextInputFormSubmit) + fun onSubmitClicked() { + // TODO + } + + private fun setupSubmitButton() { + when (params.mode) { + TextInputFormFragmentMode.SetEmailMandatory, + TextInputFormFragmentMode.SetMsisdnMandatory, + TextInputFormFragmentMode.ConfirmMsisdn -> { + loginGenericTextInputFormSubmit.isEnabled = false + loginGenericTextInputFormTextInput.textChanges() + .subscribe { + // TODO Better check for email format, etc? + loginGenericTextInputFormSubmit.isEnabled = it.isNotBlank() + } + .disposeOnDestroyView() + } + TextInputFormFragmentMode.SetEmailOptional, + TextInputFormFragmentMode.SetMsisdnOptional -> { + loginGenericTextInputFormSubmit.isEnabled = true + } + } + } + + override fun resetViewModel() { + // Nothing to do + } +} diff --git a/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml new file mode 100644 index 0000000000..4edd635515 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 089ac19b7e..db6d9e673f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -69,4 +69,23 @@ Tap on the link to confirm your new password. Back to Sign In + Set email address + Set an email to recover your account. Later, you can optionally allow people you know to discover you by your email. + Email + Email (optional) + Next + + Set phone number + Set a phone number to optionally allow people you know to discover you. + Phone number + Phone number (optional) + Next + + Confirm phone number + + We just sent a code to %1$s. Enter it below to verify it’s you. + Enter code + Send again + Next + From 41ac2c6d7035a9130afc868e5ceaf0d764a83a36 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 17:39:51 +0100 Subject: [PATCH 029/132] Login screens: Registration WIP --- .../im/vector/matrix/android/api/Matrix.kt | 6 ++ .../auth/registration/RegistrationResult.kt | 31 +++++++++ .../auth/registration/RegistrationWizard.kt | 7 ++- .../android/api/auth/registration/Stage.kt | 17 +++-- .../matrix/android/internal/auth/AuthAPI.kt | 4 +- .../android/internal/auth/AuthModule.kt | 5 ++ .../internal/auth/data/LoginFlowTypes.kt | 2 + .../DefaultRegistrationService.kt | 14 ++--- .../registration/DefaultRegistrationWizard.kt | 18 ++++-- .../registration/RegistrationFlowResponse.kt | 40 ++++++++++++ .../android/internal/di/MatrixComponent.kt | 3 + .../vector/riotx/core/di/ScreenComponent.kt | 4 +- .../vector/riotx/core/di/VectorComponent.kt | 3 + .../im/vector/riotx/core/di/VectorModule.kt | 7 +++ .../riotx/features/login/LoginAction.kt | 9 +++ .../riotx/features/login/LoginActivity.kt | 2 +- .../riotx/features/login/LoginFragment.kt | 42 ++++++++++--- .../LoginSignUpSignInSelectionFragment.kt | 38 ++++++++++- .../riotx/features/login/LoginViewModel.kt | 63 ++++++++++++++++++- .../riotx/features/login/LoginViewState.kt | 6 +- vector/src/main/res/layout/fragment_login.xml | 4 +- vector/src/main/res/values/strings_riotX.xml | 6 ++ 22 files changed, 284 insertions(+), 47 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt index 1bfa871a42..3c4e9b23e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt @@ -23,6 +23,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.di.DaggerMatrixComponent import im.vector.matrix.android.internal.network.UserAgentHolder @@ -47,6 +48,7 @@ data class MatrixConfiguration( class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) { @Inject internal lateinit var authenticator: Authenticator + @Inject internal lateinit var registrationService: RegistrationService @Inject internal lateinit var userAgentHolder: UserAgentHolder @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var olmManager: OlmManager @@ -68,6 +70,10 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo return authenticator } + fun registrationService(): RegistrationService { + return registrationService + } + companion object { private lateinit var instance: Matrix diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt new file mode 100644 index 0000000000..ddc231f186 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.registration + +import im.vector.matrix.android.api.session.Session + +// Either a session or an object containing data about registration stages +sealed class RegistrationResult { + data class Success(val session: Session) : RegistrationResult() + data class FlowResponse(val flowResult: FlowResult) : RegistrationResult() +} + + +data class FlowResult( + val missingStages: List, + val completedStages: List +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 332c1ef781..7144ce389f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -17,14 +17,15 @@ package im.vector.matrix.android.api.auth.registration import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable interface RegistrationWizard { - fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback): Cancelable + fun getRegistrationFlow(callback: MatrixCallback): Cancelable - fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable + fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback): Cancelable + + fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable // TODO Add other method here } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt index 283a79348b..9f1883e4b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -16,32 +16,29 @@ package im.vector.matrix.android.api.auth.registration -import im.vector.matrix.android.api.util.JsonDict - sealed class Stage(open val mandatory: Boolean) { - // m.login.password - data class Password(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) - // m.login.recaptcha data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) // m.login.oauth2 // m.login.email.identity - data class Email(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + data class Email(override val mandatory: Boolean) : Stage(mandatory) // m.login.msisdn - data class Msisdn(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + data class Msisdn(override val mandatory: Boolean) : Stage(mandatory) + // m.login.token + // m.login.dummy + object Dummy : Stage(false) // Undocumented yet: m.login.terms data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) - // TODO SSO - // For unknown stages - data class Other(override val mandatory: Boolean, val type: String, val params: JsonDict?) : Stage(mandatory) + data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory) } +//TODO class TermPolicies diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index 8316589ad4..6e16393723 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -36,8 +36,8 @@ internal interface AuthAPI { * Register to the homeserver * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") - fun register(registrationParams: RegistrationParams): Call + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") + fun register(@Body registrationParams: RegistrationParams): Call /** * Get the supported login flow diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 31a85afbfb..e54073ac08 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -21,8 +21,10 @@ import dagger.Binds import dagger.Module import dagger.Provides import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore +import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationService import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase import io.realm.RealmConfiguration @@ -60,4 +62,7 @@ internal abstract class AuthModule { @Binds abstract fun bindAuthenticator(authenticator: DefaultAuthenticator): Authenticator + + @Binds + abstract fun bindRegistrationService(service: DefaultRegistrationService): RegistrationService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt index 81196c7414..59c962c0ba 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.auth.data +// TODO Move to [InteractiveAuthenticationFlow] object LoginFlowTypes { const val PASSWORD = "m.login.password" const val OAUTH2 = "m.login.oauth2" @@ -25,4 +26,5 @@ object LoginFlowTypes { const val MSISDN = "m.login.msisdn" const val RECAPTCHA = "m.login.recaptcha" const val DUMMY = "m.login.dummy" + const val TERMS = "m.login.terms" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt index fadbfe39cd..68915fd990 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationService.kt @@ -26,14 +26,14 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import okhttp3.OkHttpClient +import javax.inject.Inject -// TODO Add @Inject -internal class DefaultRegistrationService(@Unauthenticated - private val okHttpClient: Lazy, - private val retrofitFactory: RetrofitFactory, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val sessionParamsStore: SessionParamsStore, - private val sessionManager: SessionManager) : RegistrationService { +internal class DefaultRegistrationService @Inject constructor(@Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager) : RegistrationService { override fun getOrCreateRegistrationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): RegistrationWizard { // TODO Persist the wizard? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index f37f2b97be..1d234eaf8d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -20,9 +20,9 @@ import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.SessionManager @@ -47,10 +47,14 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: private val authAPI = buildAuthAPI() private val registerTask = DefaultRegisterTask(authAPI) + override fun getRegistrationFlow(callback: MatrixCallback): Cancelable { + return performRegistrationRequest(RegistrationParams(), callback) + } + override fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, - callback: MatrixCallback): Cancelable { + callback: MatrixCallback): Cancelable { return performRegistrationRequest(RegistrationParams( username = userName, password = password, @@ -58,7 +62,7 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } - override fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable { + override fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable { val safeSession = currentSession ?: run { callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) return NoOpCancellable @@ -72,7 +76,7 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } - private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { + private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { val result = runCatching { registerTask.execute(RegisterTask.Params(registrationParams)) @@ -83,13 +87,15 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: sessionParamsStore.save(sessionParams) val session = sessionManager.getOrCreateSession(sessionParams) - callback.onSuccess(session) + callback.onSuccess(RegistrationResult.Success(session)) }, { if (it is Failure.RegistrationFlowError) { currentSession = it.registrationFlowResponse.session + callback.onSuccess(RegistrationResult.FlowResponse(it.registrationFlowResponse.toFlowResult())) + } else { + callback.onFailure(it) } - callback.onFailure(it) } ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index 218251cfe5..aa9fae3362 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -18,8 +18,12 @@ package im.vector.matrix.android.internal.auth.registration import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.registration.FlowResult +import im.vector.matrix.android.api.auth.registration.Stage +import im.vector.matrix.android.api.auth.registration.TermPolicies import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes @JsonClass(generateAdapter = true) data class RegistrationFlowResponse( @@ -51,3 +55,39 @@ data class RegistrationFlowResponse( @Json(name = "params") var params: JsonDict? = null ) + +/** + * Convert to something easier to exploit on client side + */ +fun RegistrationFlowResponse.toFlowResult(): FlowResult { + // Get all the returned stages + val allFlowTypes = mutableSetOf() + + val missingStage = mutableListOf() + val completedStage = mutableListOf() + + this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } } + + allFlowTypes.forEach { type -> + val isMandatory = flows?.all { type in it.stages ?: emptyList() } == true + + val stage = when (type) { + LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) + ?: "") + LoginFlowTypes.DUMMY -> Stage.Dummy + LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, TermPolicies()) + LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory) + LoginFlowTypes.MSISDN -> Stage.Msisdn(isMandatory) + else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>)) + } + + if (type in completedStages ?: emptyList()) { + completedStage.add(stage) + } else { + missingStage.add(stage) + } + } + + return FlowResult(missingStage, completedStage) +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt index f7314fe6b4..97285bc75d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt @@ -23,6 +23,7 @@ import dagger.BindsInstance import dagger.Component import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.AuthModule import im.vector.matrix.android.internal.auth.SessionParamsStore @@ -46,6 +47,8 @@ internal interface MatrixComponent { fun authenticator(): Authenticator + fun registrationService(): RegistrationService + fun context(): Context fun resources(): Resources diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 17622020d0..9f0f83a41f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -32,8 +32,8 @@ import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsB import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity -import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.home.room.list.RoomListModule +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.login.LoginActivity @@ -47,7 +47,7 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.reactions.widget.ReactionButton import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity -import im.vector.riotx.features.settings.* +import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.ui.UiStateRepository diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index d31955ce8e..2106ebf750 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -22,6 +22,7 @@ import dagger.BindsInstance import dagger.Component import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.api.session.Session import im.vector.riotx.ActiveSessionDataSource import im.vector.riotx.EmojiCompatFontProvider @@ -99,6 +100,8 @@ interface VectorComponent { fun authenticator(): Authenticator + fun registrationService(): RegistrationService + fun bugReporter(): BugReporter fun vectorUncaughtExceptionHandler(): VectorUncaughtExceptionHandler diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt index e3df0eb635..3206c441e2 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt @@ -25,6 +25,7 @@ import dagger.Module import dagger.Provides import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.registration.RegistrationService import im.vector.matrix.android.api.session.Session import im.vector.riotx.features.navigation.DefaultNavigator import im.vector.riotx.features.navigation.Navigator @@ -67,6 +68,12 @@ abstract class VectorModule { fun providesAuthenticator(matrix: Matrix): Authenticator { return matrix.authenticator() } + + @Provides + @JvmStatic + fun providesRegistrationService(matrix: Matrix): RegistrationService { + return matrix.registrationService() + } } @Binds diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index a03af1376d..be76013ca3 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -28,6 +28,15 @@ sealed class LoginAction : VectorViewModelAction { data class InitWith(val loginConfig: LoginConfig) : LoginAction() data class ResetPassword(val email: String, val newPassword: String) : LoginAction() + // Register actions + open class RegisterAction : LoginAction() + + data class RegisterWith(val username: String, val password: String) : RegisterAction() + data class AddEmail(val email: String) : RegisterAction() + data class AddMsisdn(val msisdn: String) : RegisterAction() + data class ConfirmMsisdn(val code: String) : RegisterAction() + data class PerformCaptcha(val captcha: String /* TODO Add other params */) : RegisterAction() + // Reset actions open class ResetAction : LoginAction() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 7bc713a4f2..f934ebf27f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -126,7 +126,7 @@ class LoginActivity : VectorBaseActivity() { private fun onSignModeSelected() { when (loginViewModel.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> Unit // TODO addFragmentToBackstack(R.id.loginFragmentContainer, SignUpFragment::class.java) + SignMode.SignUp -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) SignMode.SignIn -> { // It depends on the LoginMode withState(loginViewModel) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 4161673a52..f21ce74d1a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -35,8 +35,11 @@ import kotlinx.android.synthetic.main.fragment_login.* import javax.inject.Inject /** - * In this screen, the user is asked for login and password to sign in to a homeserver. - * He also can reset his password + * In this screen, in signin mode: + * - the user is asked for login and password to sign in to a homeserver. + * - He also can reset his password + * In signup mode: + * - the user is asked for login and password */ class LoginFragment @Inject constructor( private val errorFormatter: ErrorFormatter @@ -53,38 +56,61 @@ class LoginFragment @Inject constructor( setupUi() setupSubmitButton() setupPasswordReveal() + setupButtons() } - private fun authenticate() { + @OnClick(R.id.loginSubmit) + fun submit() { val login = loginField.text?.trim().toString() val password = passwordField.text?.trim().toString() - loginViewModel.handle(LoginAction.Login(login, password)) + when (loginViewModel.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> loginViewModel.handle(LoginAction.RegisterWith(login, password)) + SignMode.SignIn -> loginViewModel.handle(LoginAction.Login(login, password)) + } } private fun setupUi() { + val resId = when (loginViewModel.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_to + SignMode.SignIn -> R.string.login_connect_to + } + when (loginViewModel.serverType) { ServerType.MatrixOrg -> { loginServerIcon.isVisible = true loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) + loginTitle.text = getString(resId, loginViewModel.getHomeServerUrlSimple()) loginNotice.text = getString(R.string.login_server_matrix_org_text) } ServerType.Modular -> { loginServerIcon.isVisible = true loginServerIcon.setImageResource(R.drawable.ic_logo_modular) // TODO - loginTitle.text = getString(R.string.login_connect_to, "TODO") + loginTitle.text = getString(resId, "TODO") loginNotice.text = loginViewModel.getHomeServerUrlSimple() } ServerType.Other -> { loginServerIcon.isVisible = false loginTitle.text = getString(R.string.login_server_other_title) - loginNotice.text = getString(R.string.login_connect_to, loginViewModel.getHomeServerUrlSimple()) + loginNotice.text = getString(resId, loginViewModel.getHomeServerUrlSimple()) } } } + private fun setupButtons() { + forgetPasswordButton.isVisible = loginViewModel.signMode == SignMode.SignIn + + loginSubmit.text = getString(when (loginViewModel.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_submit + SignMode.SignIn -> R.string.login_signin + }) + } + + private fun setupSubmitButton() { Observable .combineLatest( @@ -100,8 +126,6 @@ class LoginFragment @Inject constructor( loginSubmit.isEnabled = it } .disposeOnDestroyView() - - loginSubmit.setOnClickListener { authenticate() } } @OnClick(R.id.forgetPasswordButton) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index c1023f3356..08872606bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -20,6 +20,12 @@ import android.os.Bundle import android.view.View import androidx.core.view.isVisible import butterknife.OnClick +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.auth.registration.FlowResult +import im.vector.matrix.android.api.auth.registration.RegistrationResult +import im.vector.matrix.android.api.auth.registration.Stage import im.vector.riotx.R import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import javax.inject.Inject @@ -63,7 +69,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr @OnClick(R.id.loginSignupSigninSignUp) fun signUp() { loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } @OnClick(R.id.loginSignupSigninSignIn) @@ -75,4 +80,35 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetSignMode) } + + override fun invalidate() = withState(loginViewModel) { + when (it.asyncRegistration) { + is Success -> { + when (val res = it.asyncRegistration()) { + is RegistrationResult.Success -> + // Should not happen + Unit + is RegistrationResult.FlowResponse -> handleFlowResult(res.flowResult) + } + } + is Fail -> { + // TODO Registration disabled, etc + when (it.asyncRegistration.error) { + + } + } + } + } + + private fun handleFlowResult(flowResult: FlowResult) { + // Check that all flows are supported by the application + if (flowResult.missingStages.any { it is Stage.Other }) { + // Display a popup to propose use web fallback + // TODO + } else { + // Go on with registration flow + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + } + } + } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index abaa00283f..bfb6be6733 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -23,6 +23,10 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.auth.registration.FlowResult +import im.vector.matrix.android.api.auth.registration.RegistrationResult +import im.vector.matrix.android.api.auth.registration.RegistrationService +import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow @@ -36,6 +40,7 @@ import timber.log.Timber class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, private val authenticator: Authenticator, + private val registrationService: RegistrationService, private val activeSessionHolder: ActiveSessionHolder, private val pushRuleTriggerListener: PushRuleTriggerListener, private val sessionListener: SessionListener) @@ -55,6 +60,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private var registrationWizard: RegistrationWizard? = null + var serverType: ServerType = ServerType.MatrixOrg private set var signMode: SignMode = SignMode.Unknown @@ -89,7 +96,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi LoginAction.ResetLogin -> { setState { copy( - asyncLoginAction = Uninitialized + asyncLoginAction = Uninitialized, + asyncRegistration = Uninitialized ) } } @@ -124,6 +132,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleUpdateSignMode(action: LoginAction.UpdateSignMode) { signMode = action.signMode + + if (signMode == SignMode.SignUp) { + startRegistrationFlow() + } } private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { @@ -206,6 +218,53 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun startRegistrationFlow() { + val homeServerConnectionConfigFinal = homeServerConnectionConfig + + if (homeServerConnectionConfigFinal == null) { + setState { + copy( + asyncRegistration = Fail(Throwable("Bad configuration")) + ) + } + } else { + setState { + copy( + asyncRegistration = Loading() + ) + } + + registrationWizard = registrationService.getOrCreateRegistrationWizard(homeServerConnectionConfigFinal) + + currentTask = registrationWizard?.getRegistrationFlow(object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + } + + private fun onFlowResponse(flowResult: FlowResult) { + setState { + copy( + asyncRegistration = Success(RegistrationResult.FlowResponse(flowResult)) + ) + } + } + + private fun onSessionCreated(session: Session) { activeSessionHolder.setActiveSession(session) session.configureAndStart(pushRuleTriggerListener, sessionListener) @@ -246,7 +305,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi // Do not retry if we already have flows for this config -> causes infinite focus loop if (newConfig?.homeServerUri?.toString() == homeServerConnectionConfig?.homeServerUri?.toString() - && state.asyncHomeServerLoginFlowRequest is Success) return@withState + && state.asyncHomeServerLoginFlowRequest is Success) return@withState currentTask?.cancel() currentTask = null diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 1f01e2348d..f42d46e22c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -17,11 +17,13 @@ package im.vector.riotx.features.login import com.airbnb.mvrx.* +import im.vector.matrix.android.api.auth.registration.RegistrationResult data class LoginViewState( val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized, - val asyncResetPassword: Async = Uninitialized + val asyncResetPassword: Async = Uninitialized, + val asyncRegistration: Async = Uninitialized ) : MvRxState { fun isLoading(): Boolean { @@ -29,10 +31,10 @@ data class LoginViewState( return asyncLoginAction is Loading || asyncHomeServerLoginFlowRequest is Loading || asyncResetPassword is Loading + || asyncRegistration is Loading } fun isUserLogged(): Boolean { - // TODO Add other async here return asyncLoginAction is Success } } diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 990ccb3db3..1f9859f74b 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -58,7 +58,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="32dp" - android:hint="@string/auth_user_name_placeholder" + android:hint="@string/login_signup_username_hint" app:errorEnabled="true"> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index db6d9e673f..42667ce5fa 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -88,4 +88,10 @@ Send again Next + + Sign up to %1$s + Username + Password + Next + From 381084b2abb3ce4d70c89d1755a6fff33de19a89 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 18:04:14 +0100 Subject: [PATCH 030/132] Login screens: USER_IN_USE error --- .../vector/riotx/core/error/ErrorFormatter.kt | 3 ++ .../riotx/features/login/LoginFragment.kt | 13 +++++++ .../riotx/features/login/LoginViewModel.kt | 35 +++++++++++++++++++ vector/src/main/res/values/strings_riotX.xml | 1 + 4 files changed, 52 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 65f48f5e7b..29506cf880 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -54,6 +54,9 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi && throwable.error.message == "Invalid password" -> { stringProvider.getString(R.string.auth_invalid_login_param) } + throwable.error.code == MatrixError.USER_IN_USE -> { + stringProvider.getString(R.string.login_signup_error_user_in_use) + } else -> { throwable.error.message.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index f21ce74d1a..88c1917616 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -177,5 +177,18 @@ class LoginFragment @Inject constructor( // Success is handled by the LoginActivity is Success -> Unit } + + when (state.asyncRegistration) { + is Loading -> { + // Ensure password is hidden + passwordShown = false + renderPasswordField() + } + is Fail -> { + loginFieldTil.error = errorFormatter.toHumanReadable(state.asyncRegistration.error) + } + // Success is handled by the LoginActivity + is Success -> Unit + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index bfb6be6733..f0d872ce48 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -74,6 +74,24 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null + private val registrationCallback = object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + } + override fun handle(action: LoginAction) { when (action) { is LoginAction.UpdateServerType -> handleUpdateServerType(action) @@ -83,10 +101,27 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.Login -> handleLogin(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) is LoginAction.ResetPassword -> handleResetPassword(action) + is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.ResetAction -> handleResetAction(action) } } + private fun handleRegisterAction(action: LoginAction.RegisterAction) { + when (action) { + is LoginAction.RegisterWith -> handleRegisterWith(action) + } + } + + private fun handleRegisterWith(action: LoginAction.RegisterWith) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.createAccount(action.username, action.password, null /* TODO InitialDisplayName */, registrationCallback) + } + private fun handleResetAction(action: LoginAction.ResetAction) { // Cancel any request currentTask?.cancel() diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 42667ce5fa..abd44dc54c 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -93,5 +93,6 @@ Username Password Next + That username is taken From 95fc20dca019410cd8661925295659075b1a22e9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 Nov 2019 19:08:10 +0100 Subject: [PATCH 031/132] Login screens: Registration: login/password step --- .../riotx/features/login/LoginActivity.kt | 62 +++++++++++++++++++ .../LoginGenericTextInputFormFragment.kt | 61 ++++++------------ .../LoginSignUpSignInSelectionFragment.kt | 28 +-------- .../riotx/features/login/LoginViewEvents.kt | 27 ++++++++ .../riotx/features/login/LoginViewModel.kt | 52 ++++++++++------ 5 files changed, 143 insertions(+), 87 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index f934ebf27f..e05a8d2d5e 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -23,6 +23,8 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.auth.registration.FlowResult +import im.vector.matrix.android.api.auth.registration.Stage import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment @@ -85,6 +87,34 @@ class LoginActivity : VectorBaseActivity() { updateWithState(it) } .disposeOnDestroy() + + loginViewModel.viewEvents + .observe() + .subscribe { + handleLoginViewEvents(it) + } + .disposeOnDestroy() + } + + private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { + when (loginViewEvents) { + is LoginViewEvents.RegistrationFlowResult -> { + // Check that all flows are supported by the application + if (loginViewEvents.flowResult.missingStages.any { it is Stage.Other }) { + // Display a popup to propose use web fallback + // TODO + } else { + // Go on with registration flow + // loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + if (loginViewModel.isPasswordSent) { + handleRegistrationNavigation(loginViewEvents.flowResult) + } else { + // First ask for login and password + addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + } + } + } + } } private fun onLoginFlowRetrieved() { @@ -152,6 +182,38 @@ class LoginActivity : VectorBaseActivity() { .show() } + private fun handleRegistrationNavigation(flowResult: FlowResult) { + // Complete all mandatory stage first + val mandatoryStages = flowResult.missingStages.filter { it.mandatory } + + if (mandatoryStages.isEmpty()) { + // Consider optional stages + val optionalStages = flowResult.missingStages.filter { !it.mandatory } + if (optionalStages.isEmpty()) { + // Should not happen... + } else { + doStage(optionalStages.first()) + } + } else { + doStage(mandatoryStages.first()) + } + } + + private fun doStage(stage: Stage) { + when (stage) { + is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginCaptchaFragment::class.java) + is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory)) + is Stage.Msisdn + -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory)) + is Stage.Terms + -> TODO() + } + } + + override fun onResume() { super.onResume() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 98c30c685f..4e25769740 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -30,16 +30,15 @@ import kotlinx.android.synthetic.main.fragment_login_generic_text_input_form.* import javax.inject.Inject enum class TextInputFormFragmentMode { - SetEmailMandatory, - SetEmailOptional, - SetMsisdnMandatory, - SetMsisdnOptional, + SetEmail, + SetMsisdn, ConfirmMsisdn } @Parcelize data class LoginGenericTextInputFormFragmentArgument( - val mode: TextInputFormFragmentMode + val mode: TextInputFormFragmentMode, + val mandatory: Boolean ) : Parcelable /** @@ -60,39 +59,23 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra private fun setupUi() { when (params.mode) { - TextInputFormFragmentMode.SetEmailMandatory -> { + TextInputFormFragmentMode.SetEmail -> { loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) - loginGenericTextInputFormTil.hint = getString(R.string.login_set_email_mandatory_hint) + loginGenericTextInputFormTil.hint = getString(if (params.mandatory) R.string.login_set_email_mandatory_hint else R.string.login_set_email_optional_hint) loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS loginGenericTextInputFormOtherButton.isVisible = false loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) } - TextInputFormFragmentMode.SetEmailOptional -> { - loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) - loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) - loginGenericTextInputFormTil.hint = getString(R.string.login_set_email_optional_hint) - loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS - loginGenericTextInputFormOtherButton.isVisible = false - loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) - } - TextInputFormFragmentMode.SetMsisdnMandatory -> { + TextInputFormFragmentMode.SetMsisdn -> { loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) - loginGenericTextInputFormTil.hint = getString(R.string.login_set_msisdn_mandatory_hint) + loginGenericTextInputFormTil.hint = getString(if (params.mandatory) R.string.login_set_msisdn_mandatory_hint else R.string.login_set_msisdn_optional_hint) loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE loginGenericTextInputFormOtherButton.isVisible = false loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) } - TextInputFormFragmentMode.SetMsisdnOptional -> { - loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) - loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) - loginGenericTextInputFormTil.hint = getString(R.string.login_set_msisdn_optional_hint) - loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE - loginGenericTextInputFormOtherButton.isVisible = false - loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) - } - TextInputFormFragmentMode.ConfirmMsisdn -> { + TextInputFormFragmentMode.ConfirmMsisdn -> { loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title) loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice) loginGenericTextInputFormTil.hint = getString(R.string.login_msisdn_confirm_hint) @@ -115,22 +98,16 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra } private fun setupSubmitButton() { - when (params.mode) { - TextInputFormFragmentMode.SetEmailMandatory, - TextInputFormFragmentMode.SetMsisdnMandatory, - TextInputFormFragmentMode.ConfirmMsisdn -> { - loginGenericTextInputFormSubmit.isEnabled = false - loginGenericTextInputFormTextInput.textChanges() - .subscribe { - // TODO Better check for email format, etc? - loginGenericTextInputFormSubmit.isEnabled = it.isNotBlank() - } - .disposeOnDestroyView() - } - TextInputFormFragmentMode.SetEmailOptional, - TextInputFormFragmentMode.SetMsisdnOptional -> { - loginGenericTextInputFormSubmit.isEnabled = true - } + if (params.mandatory) { + loginGenericTextInputFormSubmit.isEnabled = false + loginGenericTextInputFormTextInput.textChanges() + .subscribe { + // TODO Better check for email format, etc? + loginGenericTextInputFormSubmit.isEnabled = it.isNotBlank() + } + .disposeOnDestroyView() + } else { + loginGenericTextInputFormSubmit.isEnabled = true } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 08872606bb..4425632fd1 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -21,11 +21,7 @@ import android.view.View import androidx.core.view.isVisible import butterknife.OnClick import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Success import com.airbnb.mvrx.withState -import im.vector.matrix.android.api.auth.registration.FlowResult -import im.vector.matrix.android.api.auth.registration.RegistrationResult -import im.vector.matrix.android.api.auth.registration.Stage import im.vector.riotx.R import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import javax.inject.Inject @@ -83,32 +79,12 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun invalidate() = withState(loginViewModel) { when (it.asyncRegistration) { - is Success -> { - when (val res = it.asyncRegistration()) { - is RegistrationResult.Success -> - // Should not happen - Unit - is RegistrationResult.FlowResponse -> handleFlowResult(res.flowResult) - } - } - is Fail -> { - // TODO Registration disabled, etc + is Fail -> { + // TODO Registration disabled, (move to Activity?) when (it.asyncRegistration.error) { } } } } - - private fun handleFlowResult(flowResult: FlowResult) { - // Check that all flows are supported by the application - if (flowResult.missingStages.any { it is Stage.Other }) { - // Display a popup to propose use web fallback - // TODO - } else { - // Go on with registration flow - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) - } - } - } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt new file mode 100644 index 0000000000..b8b7965c77 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 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.riotx.features.login + +import im.vector.matrix.android.api.auth.registration.FlowResult + +/** + * Transient events for Login + */ +sealed class LoginViewEvents { + data class RegistrationFlowResult(val flowResult: FlowResult) : LoginViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index f0d872ce48..362a126e8c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -34,6 +34,8 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.utils.DataSource +import im.vector.riotx.core.utils.PublishDataSource import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import timber.log.Timber @@ -60,6 +62,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + var isPasswordSent: Boolean = false + private set + private var registrationWizard: RegistrationWizard? = null var serverType: ServerType = ServerType.MatrixOrg @@ -74,23 +79,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null - private val registrationCallback = object : MatrixCallback { - override fun onSuccess(data: RegistrationResult) { - when (data) { - is RegistrationResult.Success -> onSessionCreated(data.session) - is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) - } - } - - override fun onFailure(failure: Throwable) { - // TODO Handled JobCancellationException - setState { - copy( - asyncRegistration = Fail(failure) - ) - } - } - } + private val _viewEvents = PublishDataSource() + val viewEvents: DataSource = _viewEvents override fun handle(action: LoginAction) { when (action) { @@ -109,6 +99,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleRegisterAction(action: LoginAction.RegisterAction) { when (action) { is LoginAction.RegisterWith -> handleRegisterWith(action) + // TODO Add other actions here } } @@ -119,7 +110,25 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } - currentTask = registrationWizard?.createAccount(action.username, action.password, null /* TODO InitialDisplayName */, registrationCallback) + currentTask = registrationWizard?.createAccount(action.username, action.password, null /* TODO InitialDisplayName */, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + isPasswordSent = true + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) } private fun handleResetAction(action: LoginAction.ResetAction) { @@ -129,6 +138,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi when (action) { LoginAction.ResetLogin -> { + isPasswordSent = false + setState { copy( asyncLoginAction = Uninitialized, @@ -292,9 +303,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun onFlowResponse(flowResult: FlowResult) { + // Notify the user + _viewEvents.post(LoginViewEvents.RegistrationFlowResult(flowResult)) + setState { copy( - asyncRegistration = Success(RegistrationResult.FlowResponse(flowResult)) + asyncRegistration = Uninitialized ) } } From dfbf448bb7be08523f6c959b8caf2a7fce0edba8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 13:30:15 +0100 Subject: [PATCH 032/132] Login screens: Captcha step for registration --- .../internal/auth/registration/AuthParams.kt | 47 +++++- .../auth/registration/AuthParamsCaptcha.kt | 30 ---- .../registration/AuthParamsEmailIdentity.kt | 40 ----- .../registration/DefaultRegistrationWizard.kt | 4 +- .../registration/RegistrationFlowResponse.kt | 11 +- .../auth/registration/RegistrationParams.kt | 2 +- vector/src/main/assets/reCaptchaPage.html | 22 +++ .../im/vector/riotx/core/di/FragmentModule.kt | 5 + .../vector/riotx/core/di/VectorComponent.kt | 3 + .../im/vector/riotx/core/utils/AssetReader.kt | 62 +++++++ .../riotx/features/login/LoginAction.kt | 2 +- .../riotx/features/login/LoginActivity.kt | 6 +- .../features/login/LoginCaptchaFragment.kt | 152 +++++++++++++++++- .../riotx/features/login/LoginViewModel.kt | 51 +++++- .../res/layout/fragment_login_captcha.xml | 10 +- 15 files changed, 358 insertions(+), 89 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt create mode 100644 vector/src/main/assets/reCaptchaPage.html create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt index 69ef4e2238..f0314b6c25 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt @@ -18,15 +18,58 @@ package im.vector.matrix.android.internal.auth.registration import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes /** * Open class, parent to all possible authentication parameters */ @JsonClass(generateAdapter = true) -open class AuthParams( +internal data class AuthParams( @Json(name = "type") val type: String, @Json(name = "session") - val session: String + val session: String, + + /** + * parameter for "m.login.recaptcha" type + */ + @Json(name = "response") + val captchaResponse: String? = null, + + /** + * parameter for "m.login.email.identity" type + */ + @Json(name = "threepid_creds") + val threePidCredentials: ThreePidCredentials? = null +) { + + companion object { + fun createForCaptcha(session: String, captchaResponse: String): AuthParams { + return AuthParams( + type = LoginFlowTypes.RECAPTCHA, + session = session, + captchaResponse = captchaResponse + ) + } + + fun createForEmailIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams { + return AuthParams( + type = LoginFlowTypes.EMAIL_IDENTITY, + session = session, + threePidCredentials = threePidCredentials + ) + } + } +} + + +data class ThreePidCredentials( + @Json(name = "client_secret") + val clientSecret: String? = null, + + @Json(name = "id_server") + val idServer: String? = null, + + val sid: String? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt deleted file mode 100644 index daf9f911c0..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsCaptcha.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2018 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package im.vector.matrix.android.internal.auth.registration - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes - -/** - * Class to define the authentication parameters for "m.login.recaptcha" type - */ -@JsonClass(generateAdapter = true) -class AuthParamsCaptcha(session: String, - - @Json(name = "response") - val response: String) - : AuthParams(LoginFlowTypes.RECAPTCHA, session) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt deleted file mode 100644 index 981b8682f9..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParamsEmailIdentity.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2018 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package im.vector.matrix.android.internal.auth.registration - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes - -/** - * Class to define the authentication parameters for "m.login.email.identity" type - */ -@JsonClass(generateAdapter = true) -class AuthParamsEmailIdentity(session: String, - - @Json(name = "threepid_creds") - val threePidCredentials: ThreePidCredentials) - : AuthParams(LoginFlowTypes.EMAIL_IDENTITY, session) - -data class ThreePidCredentials( - @Json(name = "client_secret") - val clientSecret: String? = null, - - @Json(name = "id_server") - val idServer: String? = null, - - val sid: String? = null -) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index 1d234eaf8d..a25071aa3c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -70,9 +70,7 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: return performRegistrationRequest( RegistrationParams( - auth = AuthParamsCaptcha( - session = safeSession, - response = response) + auth = AuthParams.createForCaptcha(safeSession, response) ), callback) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index aa9fae3362..17850397b8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -53,7 +53,16 @@ data class RegistrationFlowResponse( * For example, the public key of reCAPTCHA stage could be given here. */ @Json(name = "params") - var params: JsonDict? = null + var params: JsonDict? = null, + + /** + * The two MatrixError fields can also be present here in case of error when validating a stage + */ + @Json(name = "errcode") + var code: String? = null, + + @Json(name = "error") + var message: String? = null ) /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt index 8d668f7f11..6a874c7387 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt @@ -24,7 +24,7 @@ import com.squareup.moshi.JsonClass * Class to pass parameters to the different registration types for /register. */ @JsonClass(generateAdapter = true) -data class RegistrationParams( +internal data class RegistrationParams( // authentication parameters @Json(name = "auth") val auth: AuthParams? = null, diff --git a/vector/src/main/assets/reCaptchaPage.html b/vector/src/main/assets/reCaptchaPage.html new file mode 100644 index 0000000000..8029acf28c --- /dev/null +++ b/vector/src/main/assets/reCaptchaPage.html @@ -0,0 +1,22 @@ + + + + + + +
+ + + diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 37ce9583b8..19dfe75336 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -114,6 +114,11 @@ interface FragmentModule { @FragmentKey(LoginFragment::class) fun bindLoginFragment(fragment: LoginFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LoginCaptchaFragment::class) + fun bindLoginCaptchaFragment(fragment: LoginCaptchaFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginServerUrlFormFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 2106ebf750..e06093a5c8 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -29,6 +29,7 @@ import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.VectorApplication import im.vector.riotx.core.pushers.PushersManager +import im.vector.riotx.core.utils.AssetReader import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler @@ -70,6 +71,8 @@ interface VectorComponent { fun resources(): Resources + fun assetReader(): AssetReader + fun dimensionConverter(): DimensionConverter fun vectorConfiguration(): VectorConfiguration diff --git a/vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt b/vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt new file mode 100644 index 0000000000..908f0e68b6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2018 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.riotx.core.utils + +import android.content.Context +import timber.log.Timber +import javax.inject.Inject + +/** + * Read asset files + */ +class AssetReader @Inject constructor(private val context: Context) { + + /* ========================================================================================== + * CACHE + * ========================================================================================== */ + private val cache = mutableMapOf() + + /** + * Read an asset from resource and return a String or null in case of error. + * + * @param assetFilename Asset filename + * @return the content of the asset file, or null in case of error + */ + fun readAssetFile(assetFilename: String): String? { + return cache.getOrPut(assetFilename, { + return try { + context.assets.open(assetFilename) + .use { asset -> + buildString { + var ch = asset.read() + while (ch != -1) { + append(ch.toChar()) + ch = asset.read() + } + } + } + } catch (e: Exception) { + Timber.e(e, "## readAssetFile() failed") + null + } + }) + } + + fun clearCache() { + cache.clear() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index be76013ca3..b13305dc9a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -35,7 +35,7 @@ sealed class LoginAction : VectorViewModelAction { data class AddEmail(val email: String) : RegisterAction() data class AddMsisdn(val msisdn: String) : RegisterAction() data class ConfirmMsisdn(val code: String) : RegisterAction() - data class PerformCaptcha(val captcha: String /* TODO Add other params */) : RegisterAction() + data class CaptchaDone(val captchaResponse: String) : RegisterAction() // Reset actions open class ResetAction : LoginAction() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index e05a8d2d5e..4f558a10ae 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -201,12 +201,14 @@ class LoginActivity : VectorBaseActivity() { private fun doStage(stage: Stage) { when (stage) { - is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginCaptchaFragment::class.java) + is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginCaptchaFragment::class.java, LoginCaptchaFragmentArgument(stage.publicKey)) is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory)) is Stage.Msisdn - -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, + -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory)) is Stage.Terms -> TODO() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt index 1490f3d5e0..b7a4ced8a3 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -16,17 +16,165 @@ package im.vector.riotx.features.login +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.net.http.SslError +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import android.view.KeyEvent +import android.view.View +import android.webkit.* +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.args +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R +import im.vector.riotx.core.utils.AssetReader +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_login_captcha.* +import timber.log.Timber +import java.net.URLDecoder +import java.util.* import javax.inject.Inject +@Parcelize +data class LoginCaptchaFragmentArgument( + val siteKey: String +) : Parcelable + +@JsonClass(generateAdapter = true) +data class JavascriptResponse( + @Json(name = "action") + val action: String? = null, + @Json(name = "response") + val response: String? = null +) + /** * In this screen, the user is asked to confirm he is not a robot */ -class LoginCaptchaFragment @Inject constructor() : AbstractLoginFragment() { +class LoginCaptchaFragment @Inject constructor(private val assetReader: AssetReader) : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_captcha - // TODO + private val params: LoginCaptchaFragmentArgument by args() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupWebView() + } + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebView() { + loginCaptchaWevView.settings.javaScriptEnabled = true + + val reCaptchaPage = assetReader.readAssetFile("reCaptchaPage.html") ?: error("missing asset reCaptchaPage.html") + + val html = Formatter().format(reCaptchaPage, params.siteKey).toString() + val mime = "text/html" + val encoding = "utf-8" + + val homeServerUrl = loginViewModel.getHomeServerUrl() + loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null) + loginCaptchaWevView.requestLayout() + + loginCaptchaWevView.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + + // TODO Hide loader + } + + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { + Timber.d("## onReceivedSslError() : " + error.certificate) + + if (!isAdded) { + return + } + + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.ssl_could_not_verify) + .setPositiveButton(R.string.ssl_trust) { _, _ -> + Timber.d("## onReceivedSslError() : the user trusted") + handler.proceed() + } + .setNegativeButton(R.string.ssl_do_not_trust) { _, _ -> + Timber.d("## onReceivedSslError() : the user did not trust") + handler.cancel() + } + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + handler.cancel() + Timber.d("## onReceivedSslError() : the user dismisses the trust dialog.") + dialog.dismiss() + return@OnKeyListener true + } + false + }) + .setCancelable(false) + .show() + } + + // common error message + private fun onError(errorMessage: String) { + Timber.e("## onError() : $errorMessage") + + // TODO + // Toast.makeText(this@AccountCreationCaptchaActivity, errorMessage, Toast.LENGTH_LONG).show() + + // on error case, close this activity + // runOnUiThread(Runnable { finish() }) + } + + @SuppressLint("NewApi") + override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { + super.onReceivedHttpError(view, request, errorResponse) + + if (request.url.toString().endsWith("favicon.ico")) { + // Ignore this error + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + onError(errorResponse.reasonPhrase) + } else { + onError(errorResponse.toString()) + } + } + + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + @Suppress("DEPRECATION") + super.onReceivedError(view, errorCode, description, failingUrl) + onError(description) + } + + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { + if (url?.startsWith("js:") == true) { + var json = url.substring(3) + var parameters: JavascriptResponse? = null + + try { + // URL decode + json = URLDecoder.decode(json, "UTF-8") + parameters = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java).fromJson(json) + } catch (e: Exception) { + Timber.e(e, "## shouldOverrideUrlLoading(): failed") + } + + val response = parameters?.response + if (parameters?.action == "verifyCallback" && response != null) { + loginViewModel.handle(LoginAction.CaptchaDone(response)) + } + } + return true + } + } + + } + override fun resetViewModel() { // Nothing to do diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 362a126e8c..b7fb27f565 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -99,6 +99,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleRegisterAction(action: LoginAction.RegisterAction) { when (action) { is LoginAction.RegisterWith -> handleRegisterWith(action) + is LoginAction.CaptchaDone -> handleCaptchaDone(action) // TODO Add other actions here } } @@ -114,6 +115,44 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi override fun onSuccess(data: RegistrationResult) { isPasswordSent = true + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + + private fun handleCaptchaDone(action: LoginAction.CaptchaDone) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.performReCaptcha(action.captchaResponse, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + when (data) { is RegistrationResult.Success -> onSessionCreated(data.session) is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) @@ -284,6 +323,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi currentTask = registrationWizard?.getRegistrationFlow(object : MatrixCallback { override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + when (data) { is RegistrationResult.Success -> onSessionCreated(data.session) is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) @@ -305,12 +350,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun onFlowResponse(flowResult: FlowResult) { // Notify the user _viewEvents.post(LoginViewEvents.RegistrationFlowResult(flowResult)) - - setState { - copy( - asyncRegistration = Uninitialized - ) - } } diff --git a/vector/src/main/res/layout/fragment_login_captcha.xml b/vector/src/main/res/layout/fragment_login_captcha.xml index e3ba0ade22..8dec490a14 100644 --- a/vector/src/main/res/layout/fragment_login_captcha.xml +++ b/vector/src/main/res/layout/fragment_login_captcha.xml @@ -22,11 +22,19 @@ style="@style/LoginTopIcon" android:layout_gravity="center_horizontal" /> + + + android:layout_marginTop="8dp" />
From 3f80076fb1e5909e5bffe4d521ce95f7583104d7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 14:37:36 +0100 Subject: [PATCH 033/132] Login screens: Terms step for registration --- .../auth/registration/RegistrationWizard.kt | 2 + .../android/api/auth/registration/Stage.kt | 3 +- .../registration/DefaultRegistrationWizard.kt | 16 +++ .../registration/RegistrationFlowResponse.kt | 2 +- .../im/vector/riotx/core/di/FragmentModule.kt | 6 + .../riotx/features/login/LoginAction.kt | 1 + .../riotx/features/login/LoginActivity.kt | 25 ++-- .../riotx/features/login/LoginViewModel.kt | 36 ++++++ .../LocalizedFlowDataLoginTermsChecked.kt | 22 ++++ .../login/terms/LoginTermsFragment.kt | 104 +++++++++++++++ .../login/terms/LoginTermsViewState.kt | 36 ++++++ .../features/login/terms/PolicyController.kt | 47 +++++++ .../riotx/features/login/terms/PolicyModel.kt | 62 +++++++++ .../riotx/features/login/terms/UrlAndName.kt | 22 ++++ .../riotx/features/login/terms/converter.kt | 120 ++++++++++++++++++ .../main/res/layout/fragment_login_terms.xml | 43 +++++++ vector/src/main/res/layout/item_policy.xml | 48 +++++++ 17 files changed, 582 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt create mode 100755 vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt create mode 100644 vector/src/main/res/layout/fragment_login_terms.xml create mode 100644 vector/src/main/res/layout/item_policy.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 7144ce389f..85ac0d0aae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -27,5 +27,7 @@ interface RegistrationWizard { fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable + fun acceptTerms(callback: MatrixCallback): Cancelable + // TODO Add other method here } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt index 9f1883e4b1..e6d6f87869 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -40,5 +40,4 @@ sealed class Stage(open val mandatory: Boolean) { data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory) } -//TODO -class TermPolicies +typealias TermPolicies = Map<*, *> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index a25071aa3c..c81c6221ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.AuthAPI import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @@ -74,6 +75,21 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } + override fun acceptTerms(callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + return performRegistrationRequest( + RegistrationParams( + auth = AuthParams( + type = LoginFlowTypes.TERMS, + session = safeSession + ) + ), callback) + } + private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { val result = runCatching { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index 17850397b8..1d67369afc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -84,7 +84,7 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult { LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) ?: "") LoginFlowTypes.DUMMY -> Stage.Dummy - LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, TermPolicies()) + LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap()) LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory) LoginFlowTypes.MSISDN -> Stage.Msisdn(isMandatory) else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>)) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 19dfe75336..71588575db 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -36,6 +36,7 @@ import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.login.* +import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment @@ -119,6 +120,11 @@ interface FragmentModule { @FragmentKey(LoginCaptchaFragment::class) fun bindLoginCaptchaFragment(fragment: LoginCaptchaFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LoginTermsFragment::class) + fun bindLoginTermsFragment(fragment: LoginTermsFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginServerUrlFormFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index b13305dc9a..ec6ae9ea88 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -36,6 +36,7 @@ sealed class LoginAction : VectorViewModelAction { data class AddMsisdn(val msisdn: String) : RegisterAction() data class ConfirmMsisdn(val code: String) : RegisterAction() data class CaptchaDone(val captchaResponse: String) : RegisterAction() + object AcceptTerms : RegisterAction() // Reset actions open class ResetAction : LoginAction() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 4f558a10ae..6644b2c193 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -32,6 +32,9 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity +import im.vector.riotx.features.login.terms.LoginTermsFragment +import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument +import im.vector.riotx.features.login.terms.toLocalizedLoginTerms import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject @@ -184,21 +187,22 @@ class LoginActivity : VectorBaseActivity() { private fun handleRegistrationNavigation(flowResult: FlowResult) { // Complete all mandatory stage first - val mandatoryStages = flowResult.missingStages.filter { it.mandatory } + val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } - if (mandatoryStages.isEmpty()) { + if (mandatoryStage != null) { + doStage(mandatoryStage) + } else { // Consider optional stages - val optionalStages = flowResult.missingStages.filter { !it.mandatory } - if (optionalStages.isEmpty()) { + val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy } + if (optionalStage == null) { // Should not happen... } else { - doStage(optionalStages.first()) + doStage(optionalStage) } - } else { - doStage(mandatoryStages.first()) } } + // TODO Unstack fragment when stage is complete private fun doStage(stage: Stage) { when (stage) { is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, @@ -210,12 +214,13 @@ class LoginActivity : VectorBaseActivity() { -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory)) - is Stage.Terms - -> TODO() + is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginTermsFragment::class.java, + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language)))) + else -> TODO() } } - override fun onResume() { super.onResume() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index b7fb27f565..8fa78da496 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -40,6 +40,9 @@ import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import timber.log.Timber +/** + * TODO To speed up registration, consider fetching registration flow instead of login flow at startup + */ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, private val authenticator: Authenticator, private val registrationService: RegistrationService, @@ -100,10 +103,43 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi when (action) { is LoginAction.RegisterWith -> handleRegisterWith(action) is LoginAction.CaptchaDone -> handleCaptchaDone(action) + is LoginAction.AcceptTerms -> handleAcceptTerms() // TODO Add other actions here } } + private fun handleAcceptTerms() { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.acceptTerms(object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + private fun handleRegisterWith(action: LoginAction.RegisterWith) { setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt new file mode 100644 index 0000000000..52aaa9d4a4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2018 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.riotx.features.login.terms + +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms + +data class LocalizedFlowDataLoginTermsChecked(val localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, + var checked: Boolean = false) diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt new file mode 100755 index 0000000000..3e202b5245 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2018 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.riotx.features.login.terms + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import butterknife.OnClick +import com.airbnb.mvrx.args +import im.vector.riotx.R +import im.vector.riotx.core.utils.openUrlInExternalBrowser +import im.vector.riotx.features.login.AbstractLoginFragment +import im.vector.riotx.features.login.LoginAction +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_login_terms.* +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms +import javax.inject.Inject + +@Parcelize +data class LoginTermsFragmentArgument( + val localizedFlowDataLoginTerms: List +) : Parcelable + +/** + * LoginTermsFragment displays the list of policies the user has to accept + */ +class LoginTermsFragment @Inject constructor(private val policyController: PolicyController) : AbstractLoginFragment(), + PolicyController.PolicyControllerListener { + + private val params: LoginTermsFragmentArgument by args() + + override fun getLayoutResId() = R.layout.fragment_login_terms + + private var loginTermsViewState: LoginTermsViewState = LoginTermsViewState(emptyList()) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + loginTermsPolicyList.setController(policyController) + policyController.listener = this + + val list = ArrayList() + + params.localizedFlowDataLoginTerms + .forEach { + list.add(LocalizedFlowDataLoginTermsChecked(it)) + } + + loginTermsViewState = LoginTermsViewState(list) + + renderState() + } + + private fun renderState() { + policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked) + + // Button is enabled only if all checkboxes are checked + loginTermsSubmit.isEnabled = loginTermsViewState.allChecked() + } + + override fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) { + if (isChecked) { + loginTermsViewState.check(localizedFlowDataLoginTerms) + } else { + loginTermsViewState.uncheck(localizedFlowDataLoginTerms) + } + + renderState() + } + + override fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) { + openUrlInExternalBrowser(requireContext(), localizedFlowDataLoginTerms.localizedUrl!!) + + // This code crashed, because user is not authenticated yet + //val intent = VectorWebViewActivity.getIntent(requireContext(), + // localizedFlowDataLoginTerms.localizedUrl!!, + // localizedFlowDataLoginTerms.localizedName!!, + // WebViewMode.DEFAULT) + //startActivity(intent) + } + + @OnClick(R.id.loginTermsSubmit) + internal fun submit() { + loginViewModel.handle(LoginAction.AcceptTerms) + } + + override fun resetViewModel() { + // No op + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt new file mode 100644 index 0000000000..104ea88daa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018 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.riotx.features.login.terms + +import com.airbnb.mvrx.MvRxState +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms + +data class LoginTermsViewState( + val localizedFlowDataLoginTermsChecked: List +) : MvRxState { + fun check(data: LocalizedFlowDataLoginTerms) { + localizedFlowDataLoginTermsChecked.find { it.localizedFlowDataLoginTerms == data }?.checked = true + } + + fun uncheck(data: LocalizedFlowDataLoginTerms) { + localizedFlowDataLoginTermsChecked.find { it.localizedFlowDataLoginTerms == data }?.checked = false + } + + fun allChecked(): Boolean { + return localizedFlowDataLoginTermsChecked.all { it.checked } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt new file mode 100644 index 0000000000..6e86d40e3d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 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.riotx.features.login.terms + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms +import javax.inject.Inject + +class PolicyController @Inject constructor() : TypedEpoxyController>() { + + var listener: PolicyControllerListener? = null + + override fun buildModels(data: List) { + data.forEach { entry -> + policy { + id(entry.localizedFlowDataLoginTerms.policyName) + checked(entry.checked) + title(entry.localizedFlowDataLoginTerms.localizedName!!) + + clickListener(View.OnClickListener { listener?.openPolicy(entry.localizedFlowDataLoginTerms) }) + checkChangeListener { _, isChecked -> + listener?.setChecked(entry.localizedFlowDataLoginTerms, isChecked) + } + } + } + } + + interface PolicyControllerListener { + fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) + fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt new file mode 100644 index 0000000000..85b7c80dd0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2018 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.riotx.features.login.terms + +import android.view.View +import android.widget.CheckBox +import android.widget.CompoundButton +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_policy) +abstract class PolicyModel : EpoxyModelWithHolder() { + @EpoxyAttribute + var checked: Boolean = false + + @EpoxyAttribute + var title: String? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var clickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + holder.let { + it.checkbox.isChecked = checked + it.checkbox.setOnCheckedChangeListener(checkChangeListener) + it.title.text = title + it.view.setOnClickListener(clickListener) + } + } + + // Ensure checkbox behaves as expected (remove the listener) + override fun unbind(holder: Holder) { + super.unbind(holder) + holder.checkbox.setOnCheckedChangeListener(null) + } + + class Holder : VectorEpoxyHolder() { + val checkbox by bind(R.id.adapter_item_policy_checkbox) + val title by bind(R.id.adapter_item_policy_title) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt new file mode 100644 index 0000000000..1ccb7cac49 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 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.riotx.features.login.terms + +data class UrlAndName( + val url: String, + val name: String +) diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt new file mode 100644 index 0000000000..c9e6dcf3fd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2019 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.riotx.features.login.terms + +import im.vector.matrix.android.api.auth.registration.TermPolicies +import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms + +/** + * This method extract the policies from the login terms parameter, regarding the user language. + * For each policy, if user language is not found, the default language is used and if not found, the first url and name are used (not predictable) + * + * Example of Data: + *
+ * "m.login.terms": {
+ *       "policies": {
+ *         "privacy_policy": {
+ *           "version": "1.0",
+ *           "en": {
+ *             "url": "http:\/\/matrix.org\/_matrix\/consent?v=1.0",
+ *             "name": "Terms and Conditions"
+ *           }
+ *         }
+ *       }
+ *     }
+ *
+ * + * @param userLanguage the user language + * @param defaultLanguage the default language to use if the user language is not found for a policy in registrationFlowResponse + */ +fun TermPolicies.toLocalizedLoginTerms(userLanguage: String, + defaultLanguage: String = "en"): List { + val result = ArrayList() + + val policies = get("policies") + if (policies is Map<*, *>) { + policies.keys.forEach { policyName -> + val localizedFlowDataLoginTerms = LocalizedFlowDataLoginTerms() + localizedFlowDataLoginTerms.policyName = policyName as String + + val policy = policies[policyName] + + // Enter this policy + if (policy is Map<*, *>) { + // Version + localizedFlowDataLoginTerms.version = policy["version"] as String? + + var userLanguageUrlAndName: UrlAndName? = null + var defaultLanguageUrlAndName: UrlAndName? = null + var firstUrlAndName: UrlAndName? = null + + // Search for language + policy.keys.forEach { policyKey -> + when (policyKey) { + "version" -> Unit // Ignore + userLanguage -> { + // We found the data for the user language + userLanguageUrlAndName = extractUrlAndName(policy[policyKey]) + } + defaultLanguage -> { + // We found default language + defaultLanguageUrlAndName = extractUrlAndName(policy[policyKey]) + } + else -> { + if (firstUrlAndName == null) { + // Get at least some data + firstUrlAndName = extractUrlAndName(policy[policyKey]) + } + } + } + } + + // Copy found language data by priority + when { + userLanguageUrlAndName != null -> { + localizedFlowDataLoginTerms.localizedUrl = userLanguageUrlAndName!!.url + localizedFlowDataLoginTerms.localizedName = userLanguageUrlAndName!!.name + } + defaultLanguageUrlAndName != null -> { + localizedFlowDataLoginTerms.localizedUrl = defaultLanguageUrlAndName!!.url + localizedFlowDataLoginTerms.localizedName = defaultLanguageUrlAndName!!.name + } + firstUrlAndName != null -> { + localizedFlowDataLoginTerms.localizedUrl = firstUrlAndName!!.url + localizedFlowDataLoginTerms.localizedName = firstUrlAndName!!.name + } + } + } + + result.add(localizedFlowDataLoginTerms) + } + } + + return result +} + +private fun extractUrlAndName(policyData: Any?): UrlAndName? { + if (policyData is Map<*, *>) { + val url = policyData["url"] as String? + val name = policyData["name"] as String? + + if (url != null && name != null) { + return UrlAndName(url, name) + } + } + return null +} diff --git a/vector/src/main/res/layout/fragment_login_terms.xml b/vector/src/main/res/layout/fragment_login_terms.xml new file mode 100644 index 0000000000..98f773e284 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_terms.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_policy.xml b/vector/src/main/res/layout/item_policy.xml new file mode 100644 index 0000000000..f28beef73d --- /dev/null +++ b/vector/src/main/res/layout/item_policy.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + \ No newline at end of file From 9aa270c7ad8aa97f1a2d72e232b1d633b8d88ea8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 14:51:51 +0100 Subject: [PATCH 034/132] Login screens: Perform dummy action when user does not want to enter an email -> account created! --- .../auth/registration/RegistrationWizard.kt | 2 + .../registration/DefaultRegistrationWizard.kt | 15 +++++++ .../riotx/features/login/LoginAction.kt | 1 + .../LoginGenericTextInputFormFragment.kt | 19 ++++++++- .../riotx/features/login/LoginViewModel.kt | 39 +++++++++++++++++-- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 85ac0d0aae..6e967fc235 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -29,5 +29,7 @@ interface RegistrationWizard { fun acceptTerms(callback: MatrixCallback): Cancelable + fun dummy(callback: MatrixCallback): Cancelable + // TODO Add other method here } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index c81c6221ea..ef3e8d1c30 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -90,6 +90,21 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } + override fun dummy(callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + return performRegistrationRequest( + RegistrationParams( + auth = AuthParams( + type = LoginFlowTypes.DUMMY, + session = safeSession + ) + ), callback) + } + private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { val result = runCatching { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index ec6ae9ea88..2367b273ea 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -37,6 +37,7 @@ sealed class LoginAction : VectorViewModelAction { data class ConfirmMsisdn(val code: String) : RegisterAction() data class CaptchaDone(val captchaResponse: String) : RegisterAction() object AcceptTerms : RegisterAction() + object RegisterDummy : RegisterAction() // Reset actions open class ResetAction : LoginAction() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 4e25769740..4ffd149620 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -94,7 +94,24 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra @OnClick(R.id.loginGenericTextInputFormSubmit) fun onSubmitClicked() { - // TODO + val text = loginGenericTextInputFormTextInput.text.toString() + + if (text.isEmpty()) { + // Perform dummy action + loginViewModel.handle(LoginAction.RegisterDummy) + } else { + when (params.mode) { + TextInputFormFragmentMode.SetEmail -> { + // TODO + } + TextInputFormFragmentMode.SetMsisdn -> { + // TODO + } + TextInputFormFragmentMode.ConfirmMsisdn -> { + // TODO + } + } + } } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 8fa78da496..1dd445ca20 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -101,9 +101,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleRegisterAction(action: LoginAction.RegisterAction) { when (action) { - is LoginAction.RegisterWith -> handleRegisterWith(action) - is LoginAction.CaptchaDone -> handleCaptchaDone(action) - is LoginAction.AcceptTerms -> handleAcceptTerms() + is LoginAction.RegisterWith -> handleRegisterWith(action) + is LoginAction.CaptchaDone -> handleCaptchaDone(action) + is LoginAction.AcceptTerms -> handleAcceptTerms() + is LoginAction.RegisterDummy -> handleRegisterDummy() // TODO Add other actions here } } @@ -140,6 +141,38 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi }) } + private fun handleRegisterDummy() { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.dummy(object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + private fun handleRegisterWith(action: LoginAction.RegisterWith) { setState { copy( From 1c03163a33b3911409c93e9efb6ec6491dff0775 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 15:13:50 +0100 Subject: [PATCH 035/132] Login screens: prepare email and msisdn --- .../auth/registration/RegistrationWizard.kt | 7 +- .../registration/DefaultRegistrationWizard.kt | 43 +++++++- .../LoginGenericTextInputFormFragment.kt | 6 +- .../riotx/features/login/LoginViewModel.kt | 100 +++++++++++++++++- 4 files changed, 150 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt index 6e967fc235..b17c8c9dfa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt @@ -31,5 +31,10 @@ interface RegistrationWizard { fun dummy(callback: MatrixCallback): Cancelable - // TODO Add other method here + fun addEmail(email: String, callback: MatrixCallback): Cancelable + + fun addMsisdn(msisdn: String, callback: MatrixCallback): Cancelable + + fun confirmMsisdn(code: String, callback: MatrixCallback): Cancelable + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index ef3e8d1c30..8d1bd8d393 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -42,7 +42,6 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionParamsStore: SessionParamsStore, private val sessionManager: SessionManager) : RegistrationWizard { - private var currentSession: String? = null private val authAPI = buildAuthAPI() @@ -90,6 +89,48 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig: ), callback) } + override fun addEmail(email: String, callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + // TODO + return performRegistrationRequest( + RegistrationParams( + // TODO + auth = AuthParams.createForEmailIdentity(safeSession, ThreePidCredentials(email)) + ), callback) + } + + override fun addMsisdn(msisdn: String, callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + // TODO + return performRegistrationRequest( + RegistrationParams( + // TODO + auth = AuthParams.createForEmailIdentity(safeSession, ThreePidCredentials(msisdn)) + ), callback) + } + + override fun confirmMsisdn(code: String, callback: MatrixCallback): Cancelable { + val safeSession = currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + + // TODO + return performRegistrationRequest( + RegistrationParams( + // TODO + auth = AuthParams.createForEmailIdentity(safeSession, ThreePidCredentials(code)) + ), callback) + } + override fun dummy(callback: MatrixCallback): Cancelable { val safeSession = currentSession ?: run { callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 4ffd149620..a2b5feb1b1 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -102,13 +102,13 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra } else { when (params.mode) { TextInputFormFragmentMode.SetEmail -> { - // TODO + loginViewModel.handle(LoginAction.AddEmail(text)) } TextInputFormFragmentMode.SetMsisdn -> { - // TODO + loginViewModel.handle(LoginAction.AddMsisdn(text)) } TextInputFormFragmentMode.ConfirmMsisdn -> { - // TODO + loginViewModel.handle(LoginAction.ConfirmMsisdn(text)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 1dd445ca20..6cc73233a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -105,10 +105,108 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.CaptchaDone -> handleCaptchaDone(action) is LoginAction.AcceptTerms -> handleAcceptTerms() is LoginAction.RegisterDummy -> handleRegisterDummy() - // TODO Add other actions here + is LoginAction.AddEmail -> handleAddEmail(action) + is LoginAction.AddMsisdn -> handleAddMsisdn(action) + is LoginAction.ConfirmMsisdn -> handleConfirmMsisdn(action) } } + private fun handleConfirmMsisdn(action: LoginAction.ConfirmMsisdn) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.confirmMsisdn(action.code, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + + private fun handleAddMsisdn(action: LoginAction.AddMsisdn) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.addMsisdn(action.msisdn, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + + private fun handleAddEmail(action: LoginAction.AddEmail) { + setState { + copy( + asyncRegistration = Loading() + ) + } + + currentTask = registrationWizard?.addEmail(action.email, object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + setState { + copy( + asyncRegistration = Success(data) + ) + } + + when (data) { + is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) + } + } + + override fun onFailure(failure: Throwable) { + // TODO Handled JobCancellationException + setState { + copy( + asyncRegistration = Fail(failure) + ) + } + } + }) + } + private fun handleAcceptTerms() { setState { copy( From 1dc7dfc896e5b917084abf3d91535db525ffa9bc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 16:04:57 +0100 Subject: [PATCH 036/132] Login screens: registration fallback --- .../android/api/auth/data/Credentials.kt | 1 + vector/src/main/assets/onLogin.js | 1 + vector/src/main/assets/onRegistered.js | 1 + vector/src/main/assets/sendObject.js | 1 + .../features/login/JavascriptResponse.kt | 39 +++++++++ .../riotx/features/login/LoginActivity.kt | 19 ++++- .../features/login/LoginCaptchaFragment.kt | 10 --- .../riotx/features/login/LoginWebFragment.kt | 79 ++++--------------- .../riotx/features/login/SupportedStage.kt | 30 +++++++ vector/src/main/res/values/strings_riotX.xml | 1 + 10 files changed, 105 insertions(+), 77 deletions(-) create mode 100644 vector/src/main/assets/onLogin.js create mode 100644 vector/src/main/assets/onRegistered.js create mode 100644 vector/src/main/assets/sendObject.js create mode 100644 vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index 089129967b..082ffe8f1a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -31,4 +31,5 @@ data class Credentials( @Json(name = "access_token") val accessToken: String, @Json(name = "refresh_token") val refreshToken: String?, @Json(name = "device_id") val deviceId: String? + // TODO Add Wellknown ) diff --git a/vector/src/main/assets/onLogin.js b/vector/src/main/assets/onLogin.js new file mode 100644 index 0000000000..dd33227e40 --- /dev/null +++ b/vector/src/main/assets/onLogin.js @@ -0,0 +1 @@ +javascript:window.matrixLogin.onLogin = function(response) { sendObjectMessage({ 'action': 'onLogin', 'credentials': response }); }; \ No newline at end of file diff --git a/vector/src/main/assets/onRegistered.js b/vector/src/main/assets/onRegistered.js new file mode 100644 index 0000000000..67aca7b79e --- /dev/null +++ b/vector/src/main/assets/onRegistered.js @@ -0,0 +1 @@ +javascript:window.matrixRegistration.onRegistered = function(homeserverUrl, userId, accessToken) { sendObjectMessage({ 'action': 'onRegistered', 'homeServer': homeserverUrl, 'userId': userId, 'accessToken': accessToken }); } \ No newline at end of file diff --git a/vector/src/main/assets/sendObject.js b/vector/src/main/assets/sendObject.js new file mode 100644 index 0000000000..ebde72b58d --- /dev/null +++ b/vector/src/main/assets/sendObject.js @@ -0,0 +1 @@ +javascript:window.sendObjectMessage = function(parameters) { var iframe = document.createElement('iframe'); iframe.setAttribute('src', 'js:' + JSON.stringify(parameters)); document.documentElement.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null;}; \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt b/vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt new file mode 100644 index 0000000000..4d88cf6097 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 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.riotx.features.login + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.Credentials + +@JsonClass(generateAdapter = true) +data class JavascriptResponse( + @Json(name = "action") + val action: String? = null, + + /** + * Use for captcha result + */ + @Json(name = "response") + val response: String? = null, + + /** + * Used for login/registration result + */ + @Json(name = "credentials") + val credentials: Credentials? = null +) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 6644b2c193..95206afefe 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -103,9 +103,9 @@ class LoginActivity : VectorBaseActivity() { when (loginViewEvents) { is LoginViewEvents.RegistrationFlowResult -> { // Check that all flows are supported by the application - if (loginViewEvents.flowResult.missingStages.any { it is Stage.Other }) { + if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { // Display a popup to propose use web fallback - // TODO + onRegistrationStageNotSupported() } else { // Go on with registration flow // loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) @@ -159,7 +159,9 @@ class LoginActivity : VectorBaseActivity() { private fun onSignModeSelected() { when (loginViewModel.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + SignMode.SignUp -> { + // This is managed by the LoginViewEvents + } SignMode.SignIn -> { // It depends on the LoginMode withState(loginViewModel) { @@ -174,6 +176,17 @@ class LoginActivity : VectorBaseActivity() { } } + private fun onRegistrationStageNotSupported() { + AlertDialog.Builder(this) + .setTitle(R.string.app_name) + .setMessage(getString(R.string.login_registration_not_supported)) + .setPositiveButton(R.string.yes) { _, _ -> + addFragmentToBackstack(R.id.loginFragmentContainer, LoginWebFragment::class.java) + } + .setNegativeButton(R.string.no, null) + .show() + } + private fun onLoginModeNotSupported(unsupportedLoginMode: LoginMode.Unsupported) { AlertDialog.Builder(this) .setTitle(R.string.app_name) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt index b7a4ced8a3..a3dc9b4d9a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -27,8 +27,6 @@ import android.view.View import android.webkit.* import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.args -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R import im.vector.riotx.core.utils.AssetReader @@ -44,14 +42,6 @@ data class LoginCaptchaFragmentArgument( val siteKey: String ) : Parcelable -@JsonClass(generateAdapter = true) -data class JavascriptResponse( - @Json(name = "action") - val action: String? = null, - @Json(name = "response") - val response: String? = null -) - /** * In this screen, the user is asked to confirm he is not a robot */ diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index d7230f0075..5b0fe743cf 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -30,10 +30,9 @@ import android.webkit.SslErrorHandler import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AlertDialog -import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R +import im.vector.riotx.core.utils.AssetReader import kotlinx.android.synthetic.main.fragment_login_web.* import timber.log.Timber import java.net.URLDecoder @@ -43,7 +42,7 @@ import javax.inject.Inject * This screen is displayed for SSO login and also when the application does not support login flow or registration flow * of the homeserfver, as a fallback to login or to create an account */ -class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { +class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) : AbstractLoginFragment() { private lateinit var homeServerUrl: String private lateinit var signMode: SignMode @@ -151,33 +150,17 @@ class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { // avoid infinite onPageFinished call if (url.startsWith("http")) { // Generic method to make a bridge between JS and the UIWebView - val mxcJavascriptSendObjectMessage = "javascript:window.sendObjectMessage = function(parameters) {" + - " var iframe = document.createElement('iframe');" + - " iframe.setAttribute('src', 'js:' + JSON.stringify(parameters));" + - " document.documentElement.appendChild(iframe);" + - " iframe.parentNode.removeChild(iframe); iframe = null;" + - " };" - + val mxcJavascriptSendObjectMessage = assetReader.readAssetFile("sendObject.js") view.loadUrl(mxcJavascriptSendObjectMessage) if (signMode == SignMode.SignIn) { // The function the fallback page calls when the login is complete - val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" + - " sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" + - " };" - - view.loadUrl(mxcJavascriptOnRegistered) + val mxcJavascriptOnLogin = assetReader.readAssetFile("onLogin.js") + view.loadUrl(mxcJavascriptOnLogin) } else { // MODE_REGISTER // The function the fallback page calls when the registration is complete - val mxcJavascriptOnRegistered = "javascript:window.matrixRegistration.onRegistered" + - " = function(homeserverUrl, userId, accessToken) {" + - " sendObjectMessage({ 'action': 'onRegistered'," + - " 'homeServer': homeserverUrl," + - " 'userId': userId," + - " 'accessToken': accessToken });" + - " };" - + val mxcJavascriptOnRegistered = assetReader.readAssetFile("onRegistered.js") view.loadUrl(mxcJavascriptOnRegistered) } } @@ -209,46 +192,27 @@ class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { if (null != url && url.startsWith("js:")) { var json = url.substring(3) - var parameters: Map? = null + var parameters: JavascriptResponse? = null try { // URL decode json = URLDecoder.decode(json, "UTF-8") - - val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) - - @Suppress("UNCHECKED_CAST") - parameters = adapter.fromJson(json) as JsonDict? + val adapter = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java) + parameters = adapter.fromJson(json) } catch (e: Exception) { Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed") } // succeeds to parse parameters if (parameters != null) { - val action = parameters["action"] as String + val action = parameters.action if (signMode == SignMode.SignIn) { try { if (action == "onLogin") { - @Suppress("UNCHECKED_CAST") - val credentials = parameters["credentials"] as Map - - val userId = credentials["user_id"] - val accessToken = credentials["access_token"] - val homeServer = credentials["home_server"] - val deviceId = credentials["device_id"] - - // check if the parameters are defined - if (null != homeServer && null != userId && null != accessToken) { - val safeCredentials = Credentials( - userId = userId, - accessToken = accessToken, - homeServer = homeServer, - deviceId = deviceId, - refreshToken = null - ) - - loginViewModel.handle(LoginAction.WebLoginSuccess(safeCredentials)) + val credentials = parameters.credentials + if (credentials != null) { + loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) } } } catch (e: Exception) { @@ -258,21 +222,8 @@ class LoginWebFragment @Inject constructor() : AbstractLoginFragment() { // MODE_REGISTER // check the required parameters if (action == "onRegistered") { - // TODO The keys are very strange, this code comes from Riot-Android... - if (parameters.containsKey("homeServer") - && parameters.containsKey("userId") - && parameters.containsKey("accessToken")) { - // We cannot parse Credentials here because of https://github.com/matrix-org/synapse/issues/4756 - // Build on object manually - val credentials = Credentials( - userId = parameters["userId"] as String, - accessToken = parameters["accessToken"] as String, - homeServer = parameters["homeServer"] as String, - // TODO We need deviceId on RiotX... - deviceId = "TODO", - refreshToken = null - ) - + val credentials = parameters.credentials + if (credentials != null) { loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt b/vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt new file mode 100644 index 0000000000..5fd4505b00 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 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.riotx.features.login + +import im.vector.matrix.android.api.auth.registration.Stage + +/** + * Stage.Other is not supported, as well as any other new stage added to the SDK before it is added to the list below + */ +fun Stage.isSupported(): Boolean { + return this is Stage.ReCaptcha + || this is Stage.Dummy + || this is Stage.Msisdn + || this is Stage.Terms + || this is Stage.Email +} diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index abd44dc54c..6db6c1a2d1 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -56,6 +56,7 @@ An error occurred when loading the page: %1$s (%2$d) The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client? + The application is not able to create an account on this homeserver.\n\nDo you want to signup using a web client? Reset password on %1$s From c18c140ec9a00ecaaa1a308ece7312ace971d47c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 17:25:33 +0100 Subject: [PATCH 037/132] Login screens: Animate the logo in screen transition --- .../java/im/vector/riotx/core/extensions/Activity.kt | 8 +++++++- .../im/vector/riotx/features/login/LoginActivity.kt | 10 +++++++++- .../features/login/LoginServerSelectionFragment.kt | 10 ++++++++++ .../res/layout/fragment_login_server_selection.xml | 1 + vector/src/main/res/layout/fragment_login_splash.xml | 1 + 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt index 6d7c3d39e6..f9f5d3b3d2 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt @@ -18,6 +18,7 @@ package im.vector.riotx.core.extensions import android.os.Parcelable import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentTransaction import im.vector.riotx.core.platform.VectorBaseActivity fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) { @@ -44,8 +45,13 @@ fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragment: Fragment, supportFragmentManager.commitTransaction { replace(frameId, fragment).addToBackStack(tag) } } -fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { +fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, + fragmentClass: Class, + params: Parcelable? = null, + tag: String? = null, + option: ((FragmentTransaction) -> Unit)? = null) { supportFragmentManager.commitTransaction { + option?.invoke(this) replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 95206afefe..79d6eb874d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -18,7 +18,9 @@ package im.vector.riotx.features.login import android.content.Context import android.content.Intent +import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.viewModel @@ -70,7 +72,13 @@ class LoginActivity : VectorBaseActivity() { loginSharedActionViewModel.observe() .subscribe { when (it) { - is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java) + is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java, + option = { ft -> + val view = findViewById(R.id.loginSplashLogo) + if (view != null) { + ft.addSharedElement(view, ViewCompat.getTransitionName(view) ?: "") + } + }) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() is LoginNavigation.OnSignModeSelected -> onSignModeSelected() is LoginNavigation.OnLoginFlowRetrieved -> onLoginFlowRetrieved() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index e819389b9c..b08c46e335 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -16,8 +16,10 @@ package im.vector.riotx.features.login +import android.os.Build import android.os.Bundle import android.view.View +import androidx.transition.TransitionInflater import butterknife.OnClick import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Success @@ -35,6 +37,14 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment override fun getLayoutResId() = R.layout.fragment_login_server_selection + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index 570272e392..d380cc39c9 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -16,6 +16,7 @@ diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index f19e2e6603..c0d9a7c2c6 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -14,6 +14,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/riotx_logo" + android:transitionName="loginLogoTransition" app:layout_constraintBottom_toTopOf="@+id/loginSplashTitle" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" From 6d8e5b892e56d4c96da251e3ca323cdb1d25af08 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 17:28:39 +0100 Subject: [PATCH 038/132] Login screens: Show disclaimer dialog only in HomeActivity, now that RiotX supports registration --- .../java/im/vector/riotx/features/login/LoginActivity.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 79d6eb874d..3c7148b802 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -32,7 +32,6 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.VectorBaseActivity -import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument @@ -242,12 +241,6 @@ class LoginActivity : VectorBaseActivity() { } } - override fun onResume() { - super.onResume() - - showDisclaimerDialog(this) - } - companion object { private const val EXTRA_CONFIG = "EXTRA_CONFIG" From 375833482445f494a39ac41d5eabb964daecedaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 18:09:05 +0100 Subject: [PATCH 039/132] Login screens: cleanup the Fragment stack after completing stage --- .../riotx/features/login/LoginActivity.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 3c7148b802..a0b718f134 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -222,26 +222,34 @@ class LoginActivity : VectorBaseActivity() { } } - // TODO Unstack fragment when stage is complete private fun doStage(stage: Stage) { + // Ensure there is no fragment for registration stage in the backstack + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + when (stage) { is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, - LoginCaptchaFragment::class.java, LoginCaptchaFragmentArgument(stage.publicKey)) + LoginCaptchaFragment::class.java, + LoginCaptchaFragmentArgument(stage.publicKey), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory)) - is Stage.Msisdn - -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) + is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory)) + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginTermsFragment::class.java, - LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language)))) - else -> TODO() + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))), + tag = FRAGMENT_REGISTRATION_STAGE_TAG) + else -> Unit // Should not happen } } companion object { + private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" + private const val EXTRA_CONFIG = "EXTRA_CONFIG" fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { From 8ae9544b48a792a13412240186d1589b66aab305 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 18:23:33 +0100 Subject: [PATCH 040/132] Login screens: Loading on Captcha step --- .../features/login/LoginCaptchaFragment.kt | 12 +++- .../res/layout/fragment_login_captcha.xml | 62 ++++++++++--------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt index a3dc9b4d9a..7728d4eaac 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.login import android.annotation.SuppressLint import android.content.DialogInterface +import android.graphics.Bitmap import android.net.http.SslError import android.os.Build import android.os.Bundle @@ -26,6 +27,7 @@ import android.view.KeyEvent import android.view.View import android.webkit.* import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import com.airbnb.mvrx.args import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R @@ -72,10 +74,18 @@ class LoginCaptchaFragment @Inject constructor(private val assetReader: AssetRea loginCaptchaWevView.requestLayout() loginCaptchaWevView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + // Show loader + loginCaptchaProgress.isVisible = true + } + override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) - // TODO Hide loader + // Hide loader + loginCaptchaProgress.isVisible = false } override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { diff --git a/vector/src/main/res/layout/fragment_login_captcha.xml b/vector/src/main/res/layout/fragment_login_captcha.xml index 8dec490a14..2dfc37871a 100644 --- a/vector/src/main/res/layout/fragment_login_captcha.xml +++ b/vector/src/main/res/layout/fragment_login_captcha.xml @@ -2,41 +2,47 @@ + android:layout_height="match_parent" + android:paddingStart="16dp" + android:paddingTop="32dp" + android:paddingEnd="16dp" + android:paddingBottom="16dp"> - + app:layout_constraintTop_toTopOf="parent" /> - + - + - - + From a8f24e5c39b7a6388295ae3bf2a676412c4c76b7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 18:44:11 +0100 Subject: [PATCH 041/132] Login screens: a11y --- vector/src/main/res/layout/fragment_loading.xml | 3 ++- vector/src/main/res/layout/fragment_login.xml | 3 ++- vector/src/main/res/layout/fragment_login_captcha.xml | 4 +++- .../res/layout/fragment_login_generic_text_input_form.xml | 3 ++- .../src/main/res/layout/fragment_login_reset_password.xml | 3 ++- .../res/layout/fragment_login_reset_password_success.xml | 3 ++- .../src/main/res/layout/fragment_login_server_selection.xml | 6 ++++++ .../src/main/res/layout/fragment_login_server_url_form.xml | 4 +++- .../res/layout/fragment_login_signup_signin_selection.xml | 1 + vector/src/main/res/layout/fragment_login_splash.xml | 3 +++ vector/src/main/res/layout/fragment_login_terms.xml | 1 + vector/src/main/res/layout/item_policy.xml | 5 +---- vector/src/main/res/values/strings_riotX.xml | 5 +++++ 13 files changed, 33 insertions(+), 11 deletions(-) diff --git a/vector/src/main/res/layout/fragment_loading.xml b/vector/src/main/res/layout/fragment_loading.xml index 96bafda319..ae605097cd 100644 --- a/vector/src/main/res/layout/fragment_loading.xml +++ b/vector/src/main/res/layout/fragment_loading.xml @@ -4,12 +4,13 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no" /> @@ -26,15 +27,16 @@ android:text="@string/auth_recaptcha_message" app:layout_constraintTop_toBottomOf="@+id/logoImageView" /> + - + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no" /> + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no" /> + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no" /> + android:layout_marginBottom="130dp" + android:importantForAccessibility="no" /> diff --git a/vector/src/main/res/layout/item_policy.xml b/vector/src/main/res/layout/item_policy.xml index f28beef73d..ae68e33598 100644 --- a/vector/src/main/res/layout/item_policy.xml +++ b/vector/src/main/res/layout/item_policy.xml @@ -10,8 +10,6 @@ android:id="@+id/adapter_item_policy_checkbox" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginLeft="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -37,8 +35,7 @@ android:id="@+id/adapter_item_policy_arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="16dp" - android:layout_marginRight="16dp" + android:importantForAccessibility="no" android:rotationY="@integer/rtl_mirror_flip" android:src="@drawable/ic_material_chevron_right_black" app:layout_constraintBottom_toBottomOf="parent" diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 6db6c1a2d1..bf8f22e59a 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -96,4 +96,9 @@ Next That username is taken + Select matrix.org + Select modular + Select a custom homeserver + Please perform the captcha challenge + From 20f969d563f66ac43fef5645fa945adc20f71d78 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 Nov 2019 19:16:34 +0100 Subject: [PATCH 042/132] Login screens: fix issue on terms --- .../login/terms/LoginTermsFragment.kt | 19 +++++++----- .../features/login/terms/PolicyController.kt | 7 +++-- .../terms/{PolicyModel.kt => PolicyItem.kt} | 7 ++++- .../src/main/res/color/login_button_tint.xml | 7 +++++ .../main/res/layout/fragment_login_terms.xml | 13 ++++++-- vector/src/main/res/layout/item_policy.xml | 31 ++++++++++++++----- vector/src/main/res/values/strings_riotX.xml | 1 + 7 files changed, 65 insertions(+), 20 deletions(-) rename vector/src/main/java/im/vector/riotx/features/login/terms/{PolicyModel.kt => PolicyItem.kt} (89%) create mode 100644 vector/src/main/res/color/login_button_tint.xml diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt index 3e202b5245..4b98563503 100755 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -51,6 +51,7 @@ class LoginTermsFragment @Inject constructor(private val policyController: Polic super.onViewCreated(view, savedInstanceState) loginTermsPolicyList.setController(policyController) + policyController.homeServer = loginViewModel.getHomeServerUrlSimple() policyController.listener = this val list = ArrayList() @@ -83,14 +84,18 @@ class LoginTermsFragment @Inject constructor(private val policyController: Polic } override fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) { - openUrlInExternalBrowser(requireContext(), localizedFlowDataLoginTerms.localizedUrl!!) + localizedFlowDataLoginTerms.localizedUrl + ?.takeIf { it.isNotBlank() } + ?.let { + openUrlInExternalBrowser(requireContext(), it) - // This code crashed, because user is not authenticated yet - //val intent = VectorWebViewActivity.getIntent(requireContext(), - // localizedFlowDataLoginTerms.localizedUrl!!, - // localizedFlowDataLoginTerms.localizedName!!, - // WebViewMode.DEFAULT) - //startActivity(intent) + // This code crashed, because user is not authenticated yet + //val intent = VectorWebViewActivity.getIntent(requireContext(), + // localizedFlowDataLoginTerms.localizedUrl!!, + // localizedFlowDataLoginTerms.localizedName!!, + // WebViewMode.DEFAULT) + //startActivity(intent) + } } @OnClick(R.id.loginTermsSubmit) diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt index 6e86d40e3d..c301463c2a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt @@ -25,12 +25,15 @@ class PolicyController @Inject constructor() : TypedEpoxyController) { data.forEach { entry -> - policy { + policyItem { id(entry.localizedFlowDataLoginTerms.policyName) checked(entry.checked) - title(entry.localizedFlowDataLoginTerms.localizedName!!) + title(entry.localizedFlowDataLoginTerms.localizedName) + subtitle(homeServer) clickListener(View.OnClickListener { listener?.openPolicy(entry.localizedFlowDataLoginTerms) }) checkChangeListener { _, isChecked -> diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyItem.kt similarity index 89% rename from vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt rename to vector/src/main/java/im/vector/riotx/features/login/terms/PolicyItem.kt index 85b7c80dd0..9931d33068 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyItem.kt @@ -27,13 +27,16 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder @EpoxyModelClass(layout = R.layout.item_policy) -abstract class PolicyModel : EpoxyModelWithHolder() { +abstract class PolicyItem : EpoxyModelWithHolder() { @EpoxyAttribute var checked: Boolean = false @EpoxyAttribute var title: String? = null + @EpoxyAttribute + var subtitle: String? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null @@ -45,6 +48,7 @@ abstract class PolicyModel : EpoxyModelWithHolder() { it.checkbox.isChecked = checked it.checkbox.setOnCheckedChangeListener(checkChangeListener) it.title.text = title + it.subtitle.text = subtitle it.view.setOnClickListener(clickListener) } } @@ -58,5 +62,6 @@ abstract class PolicyModel : EpoxyModelWithHolder() { class Holder : VectorEpoxyHolder() { val checkbox by bind(R.id.adapter_item_policy_checkbox) val title by bind(R.id.adapter_item_policy_title) + val subtitle by bind(R.id.adapter_item_policy_subtitle) } } diff --git a/vector/src/main/res/color/login_button_tint.xml b/vector/src/main/res/color/login_button_tint.xml new file mode 100644 index 0000000000..719335766c --- /dev/null +++ b/vector/src/main/res/color/login_button_tint.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_login_terms.xml b/vector/src/main/res/layout/fragment_login_terms.xml index c595587a5d..e44139e18b 100644 --- a/vector/src/main/res/layout/fragment_login_terms.xml +++ b/vector/src/main/res/layout/fragment_login_terms.xml @@ -14,14 +14,23 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintTop_toBottomOf="@+id/loginTermsTitle" /> + android:foreground="?attr/selectableItemBackground" + android:minHeight="72dp"> - + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index bf8f22e59a..8a013a255b 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -100,5 +100,6 @@ Select modular Select a custom homeserver Please perform the captcha challenge + Accept terms to continue From 7caa8ce3bc4c34d887979f2281cd0dde75b83ed0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 10:18:14 +0100 Subject: [PATCH 043/132] Login screens: disabled registration --- .../vector/riotx/core/error/ErrorFormatter.kt | 24 ++++++++++++----- .../riotx/features/login/LoginActivity.kt | 27 +++++++++++++++++++ .../riotx/features/login/LoginViewEvents.kt | 1 + .../riotx/features/login/LoginViewModel.kt | 12 +++++++++ vector/src/main/res/values/strings_riotX.xml | 2 ++ 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 29506cf880..c943a86fbc 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -35,13 +35,14 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi return when (throwable) { null -> null is Failure.NetworkConnection -> { - if (throwable.ioException is SocketTimeoutException) { - stringProvider.getString(R.string.error_network_timeout) - } else if (throwable.ioException is UnknownHostException) { - // Invalid homeserver? - stringProvider.getString(R.string.login_error_unknown_host) - } else { - stringProvider.getString(R.string.error_no_network) + when { + throwable.ioException is SocketTimeoutException -> + stringProvider.getString(R.string.error_network_timeout) + throwable.ioException is UnknownHostException -> + // Invalid homeserver? + stringProvider.getString(R.string.login_error_unknown_host) + else -> + stringProvider.getString(R.string.error_no_network) } } is Failure.ServerError -> { @@ -57,6 +58,15 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi throwable.error.code == MatrixError.USER_IN_USE -> { stringProvider.getString(R.string.login_signup_error_user_in_use) } + throwable.error.code == MatrixError.BAD_JSON -> { + stringProvider.getString(R.string.login_error_bad_json) + } + throwable.error.code == MatrixError.NOT_JSON -> { + stringProvider.getString(R.string.login_error_not_json) + } + throwable.error.code == MatrixError.LIMIT_EXCEEDED -> { + stringProvider.getString(R.string.login_error_limit_exceeded) + } else -> { throwable.error.message.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index a0b718f134..e7cb90f5df 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -27,8 +27,11 @@ import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.Stage +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.VectorBaseActivity @@ -38,6 +41,7 @@ import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument import im.vector.riotx.features.login.terms.toLocalizedLoginTerms import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection /** * The LoginActivity manages the fragment navigation and also display the loading View @@ -48,6 +52,7 @@ class LoginActivity : VectorBaseActivity() { private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory + @Inject lateinit var errorFormatter: ErrorFormatter override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -124,9 +129,31 @@ class LoginActivity : VectorBaseActivity() { } } } + is LoginViewEvents.RegistrationError -> displayRegistrationError(loginViewEvents.throwable) } } + private fun displayRegistrationError(throwable: Throwable) { + val message = when(throwable) { + is Failure.ServerError -> { + if(throwable.error.code == MatrixError.FORBIDDEN + && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { + getString(R.string.login_registration_disabled) + } else { + null + } + } + else -> null + } + ?: errorFormatter.toHumanReadable(throwable) + + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun onLoginFlowRetrieved() { addFragmentToBackstack(R.id.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt index b8b7965c77..b7351feead 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt @@ -24,4 +24,5 @@ import im.vector.matrix.android.api.auth.registration.FlowResult */ sealed class LoginViewEvents { data class RegistrationFlowResult(val flowResult: FlowResult) : LoginViewEvents() + data class RegistrationError(val throwable: Throwable) : LoginViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 6cc73233a7..49676b4a35 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -489,7 +489,16 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi registrationWizard = registrationService.getOrCreateRegistrationWizard(homeServerConnectionConfigFinal) currentTask = registrationWizard?.getRegistrationFlow(object : MatrixCallback { + override fun onSuccess(data: RegistrationResult) { + /* + // Simulate registration disabled + onFailure(Failure.ServerError(MatrixError( + code = MatrixError.FORBIDDEN, + message = "Registration is disabled" + ), 403)) + */ + setState { copy( asyncRegistration = Success(data) @@ -503,6 +512,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } override fun onFailure(failure: Throwable) { + // Notify the user + _viewEvents.post(LoginViewEvents.RegistrationError(failure)) + // TODO Handled JobCancellationException setState { copy( diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8a013a255b..958ebec6cc 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -46,6 +46,7 @@ Sign in to %1$s Sign Up Sign In + Sign In with SSO Modular Address Address @@ -56,6 +57,7 @@ An error occurred when loading the page: %1$s (%2$d) The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client? + Sorry, the homeserver does not accept new account creation. The application is not able to create an account on this homeserver.\n\nDo you want to signup using a web client? From f12e6c941d9d8f5b3d30d080400b6ad1fe514154 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 10:38:27 +0100 Subject: [PATCH 044/132] Login screens: sigin button for SSO --- .../LoginSignUpSignInSelectionFragment.kt | 36 +++++++++++-------- ...fragment_login_signup_signin_selection.xml | 12 ++++--- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 4425632fd1..bcbef9e807 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -20,7 +20,6 @@ import android.os.Bundle import android.view.View import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.Fail import com.airbnb.mvrx.withState import im.vector.riotx.R import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* @@ -33,10 +32,15 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection + private var isSsoSignIn: Boolean = false + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + isSsoSignIn = withState(loginViewModel) { it.asyncHomeServerLoginFlowRequest.invoke() } == LoginMode.Sso + setupUi() + setupButtons() } private fun setupUi() { @@ -62,9 +66,24 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr } } - @OnClick(R.id.loginSignupSigninSignUp) + private fun setupButtons() { + if (isSsoSignIn) { + loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) + loginSignupSigninSignIn.isVisible = false + } else { + loginSignupSigninSubmit.text = getString(R.string.login_signup) + loginSignupSigninSignIn.isVisible = true + } + } + + + @OnClick(R.id.loginSignupSigninSubmit) fun signUp() { - loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + if (isSsoSignIn) { + signIn() + } else { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + } } @OnClick(R.id.loginSignupSigninSignIn) @@ -76,15 +95,4 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetSignMode) } - - override fun invalidate() = withState(loginViewModel) { - when (it.asyncRegistration) { - is Fail -> { - // TODO Registration disabled, (move to Activity?) - when (it.asyncRegistration.error) { - - } - } - } - } } diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index e58a986b14..de9a7e403c 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -56,17 +56,19 @@ app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninTitle" tools:text="@string/login_server_matrix_org_text" /> + + app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninText" + tools:text="@string/login_signup" /> + + app:layout_constraintTop_toBottomOf="@+id/loginSignupSigninSubmit" + tools:visibility="visible" />
From 62d5aba796f3fa324feb33907e00623d898fde9e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 11:12:08 +0100 Subject: [PATCH 045/132] Login screens: back button management for SSO --- .../vector/riotx/core/platform/OnBackPressed.kt | 3 ++- .../riotx/core/platform/VectorBaseActivity.kt | 16 ++++++++++------ .../features/home/room/list/RoomListFragment.kt | 2 +- .../features/login/AbstractLoginFragment.kt | 2 +- .../vector/riotx/features/login/LoginActivity.kt | 14 ++++++++++---- .../riotx/features/login/LoginWebFragment.kt | 13 ++++++------- 6 files changed, 30 insertions(+), 20 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt b/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt index 17f7730f86..c8a58997a1 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt @@ -21,6 +21,7 @@ interface OnBackPressed { /** * Returns true, if the on back pressed event has been handled by this Fragment. * Otherwise return false + * @param toolbarButton true if this is the back button from the toolbar */ - fun onBackPressed(): Boolean + fun onBackPressed(toolbarButton: Boolean): Boolean } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 4a3056657f..79b040cd41 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -278,7 +278,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressed(true) return true } @@ -286,20 +286,24 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } override fun onBackPressed() { - val handled = recursivelyDispatchOnBackPressed(supportFragmentManager) + onBackPressed(false) + } + + private fun onBackPressed(fromToolbar: Boolean) { + val handled = recursivelyDispatchOnBackPressed(supportFragmentManager, fromToolbar) if (!handled) { super.onBackPressed() } } - private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { - val reverseOrder = fm.fragments.filter { it is VectorBaseFragment }.reversed() + private fun recursivelyDispatchOnBackPressed(fm: FragmentManager, fromToolbar: Boolean): Boolean { + val reverseOrder = fm.fragments.filterIsInstance().reversed() for (f in reverseOrder) { - val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) + val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager, fromToolbar) if (handledByChildFragments) { return true } - if (f is OnBackPressed && f.onBackPressed()) { + if (f is OnBackPressed && f.onBackPressed(fromToolbar)) { return true } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index a5e9a7b4bf..04d1802264 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -329,7 +329,7 @@ class RoomListFragment @Inject constructor( stateView.state = StateView.State.Error(message) } - override fun onBackPressed(): Boolean { + override fun onBackPressed(toolbarButton: Boolean): Boolean { if (createChatFabMenu.onBackPressed()) { return true } diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 9e57350b79..1d8ad9b05f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -38,7 +38,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) } - override fun onBackPressed(): Boolean { + override fun onBackPressed(toolbarButton: Boolean): Boolean { resetViewModel() // Do not consume the Back event return false diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index e7cb90f5df..f273316c69 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.Intent import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager @@ -34,6 +35,7 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.login.terms.LoginTermsFragment @@ -46,7 +48,7 @@ import javax.net.ssl.HttpsURLConnection /** * The LoginActivity manages the fragment navigation and also display the loading View */ -class LoginActivity : VectorBaseActivity() { +class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private val loginViewModel: LoginViewModel by viewModel() private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @@ -134,16 +136,16 @@ class LoginActivity : VectorBaseActivity() { } private fun displayRegistrationError(throwable: Throwable) { - val message = when(throwable) { + val message = when (throwable) { is Failure.ServerError -> { - if(throwable.error.code == MatrixError.FORBIDDEN + if (throwable.error.code == MatrixError.FORBIDDEN && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { getString(R.string.login_registration_disabled) } else { null } } - else -> null + else -> null } ?: errorFormatter.toHumanReadable(throwable) @@ -274,6 +276,10 @@ class LoginActivity : VectorBaseActivity() { } } + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } + companion object { private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index 5b0fe743cf..4416a9261f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -40,7 +40,7 @@ import javax.inject.Inject /** * This screen is displayed for SSO login and also when the application does not support login flow or registration flow - * of the homeserfver, as a fallback to login or to create an account + * of the homeserver, as a fallback to login or to create an account */ class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) : AbstractLoginFragment() { @@ -241,12 +241,11 @@ class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) // Nothing to do } - override fun onBackPressed(): Boolean { - return if (loginWebWebView.canGoBack()) { - loginWebWebView.goBack() - true - } else { - super.onBackPressed() + override fun onBackPressed(toolbarButton: Boolean): Boolean { + return when { + toolbarButton -> super.onBackPressed(toolbarButton) + loginWebWebView.canGoBack() -> loginWebWebView.goBack().run { true } + else -> super.onBackPressed(toolbarButton) } } } From 0e2237226fa914569d82d583ebf023c010848779 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 11:45:13 +0100 Subject: [PATCH 046/132] Login screens: back button management for registration --- .../features/login/AbstractLoginFragment.kt | 23 ++++++++++++++++--- .../riotx/features/login/LoginActivity.kt | 8 +++++-- .../features/login/LoginCaptchaFragment.kt | 4 +--- .../LoginGenericTextInputFormFragment.kt | 2 +- .../riotx/features/login/LoginWebFragment.kt | 2 +- .../login/terms/LoginTermsFragment.kt | 2 +- vector/src/main/res/values/strings_riotX.xml | 4 ++++ 7 files changed, 34 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 1d8ad9b05f..0752d50e79 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -16,10 +16,12 @@ package im.vector.riotx.features.login +import android.app.AlertDialog import android.os.Bundle import android.view.View import androidx.annotation.CallSuper import com.airbnb.mvrx.activityViewModel +import im.vector.riotx.R import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.VectorBaseFragment @@ -39,9 +41,24 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { } override fun onBackPressed(toolbarButton: Boolean): Boolean { - resetViewModel() - // Do not consume the Back event - return false + if (loginViewModel.isPasswordSent) { + // Ask for confirmation before cancelling the registration + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.login_signup_cancel_confirmation_title) + .setMessage(R.string.login_signup_cancel_confirmation_content) + .setPositiveButton(R.string.login_signup_cancel_confirmation_yes) { _, _ -> + resetViewModel() + vectorBaseActivity.onBackPressed() + } + .setNegativeButton(R.string.login_signup_cancel_confirmation_no, null) + .show() + + return true + } else { + resetViewModel() + // Do not consume the Back event + return false + } } // Reset any modification on the loginViewModel by the current fragment diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index f273316c69..09a5777108 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -127,7 +127,11 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { handleRegistrationNavigation(loginViewEvents.flowResult) } else { // First ask for login and password - addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java) + // I add a tag to indicate that this fragment is a registration stage. + // This way it will be automatically popped in when starting the next registration stage + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_REGISTRATION_STAGE_TAG) } } } @@ -235,7 +239,7 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { } private fun handleRegistrationNavigation(flowResult: FlowResult) { - // Complete all mandatory stage first + // Complete all mandatory stages first val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } if (mandatoryStage != null) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt index 7728d4eaac..3795c9ddb1 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -172,11 +172,9 @@ class LoginCaptchaFragment @Inject constructor(private val assetReader: AssetRea return true } } - } - override fun resetViewModel() { - // Nothing to do + loginViewModel.handle(LoginAction.ResetLogin) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index a2b5feb1b1..70af1af484 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -129,6 +129,6 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra } override fun resetViewModel() { - // Nothing to do + loginViewModel.handle(LoginAction.ResetLogin) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index 4416a9261f..2aaf734ca6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -238,7 +238,7 @@ class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) } override fun resetViewModel() { - // Nothing to do + loginViewModel.handle(LoginAction.ResetLogin) } override fun onBackPressed(toolbarButton: Boolean): Boolean { diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt index 4b98563503..6c8f86c1e2 100755 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -104,6 +104,6 @@ class LoginTermsFragment @Inject constructor(private val policyController: Polic } override fun resetViewModel() { - // No op + loginViewModel.handle(LoginAction.ResetLogin) } } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 958ebec6cc..ff74833104 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -97,6 +97,10 @@ Password Next That username is taken + Warning + Your account is not created yet.\n\nStop the registration process? + Yes + No Select matrix.org Select modular From f74cabd1455974731cd552c838895dc124a4f90c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Nov 2019 12:55:20 +0100 Subject: [PATCH 047/132] Login screens: UI: style to prepare for landscape --- vector/src/main/res/layout/fragment_login.xml | 206 +++++------ .../res/layout/fragment_login_captcha.xml | 68 ++-- ...fragment_login_generic_text_input_form.xml | 133 +++---- .../layout/fragment_login_reset_password.xml | 159 ++++---- .../fragment_login_reset_password_success.xml | 78 ++-- .../fragment_login_server_selection.xml | 342 +++++++++--------- .../layout/fragment_login_server_url_form.xml | 151 ++++---- ...fragment_login_signup_signin_selection.xml | 138 ++++--- .../main/res/layout/fragment_login_terms.xml | 75 ++-- .../src/main/res/values-land/styles_login.xml | 19 + vector/src/main/res/values/styles_login.xml | 31 +- 11 files changed, 665 insertions(+), 735 deletions(-) create mode 100644 vector/src/main/res/values-land/styles_login.xml diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index 28eeee1549..977c694952 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -6,143 +6,123 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + + + + + + + + + android:layout_marginTop="32dp" + android:hint="@string/login_signup_username_hint" + app:errorEnabled="true"> - - - - - + android:inputType="textEmailAddress" + android:maxLines="1" /> - + - + + android:hint="@string/login_signup_password_hint" + app:errorEnabled="true" + app:errorIconDrawable="@null"> + android:ems="10" + android:inputType="textPassword" + android:maxLines="1" + android:paddingEnd="48dp" + android:paddingRight="48dp" + tools:ignore="RtlSymmetry" /> - + + + + + + + android:layout_gravity="start" + android:text="@string/auth_forgot_password" /> - - - - - - - - - - - + android:layout_alignParentEnd="true" + android:layout_gravity="end" + android:text="@string/auth_login" + tools:enabled="false" + tools:ignore="RelativeOverlap" /> - + - - - - - - - +
diff --git a/vector/src/main/res/layout/fragment_login_captcha.xml b/vector/src/main/res/layout/fragment_login_captcha.xml index 762b6af4c0..31e1e882b1 100644 --- a/vector/src/main/res/layout/fragment_login_captcha.xml +++ b/vector/src/main/res/layout/fragment_login_captcha.xml @@ -2,49 +2,47 @@ + android:layout_height="match_parent"> - + - - - - + android:orientation="vertical" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="16dp"> + + + + + + +
+ app:layout_constraintBottom_toBottomOf="@id/loginFormContainer" + app:layout_constraintEnd_toEndOf="@id/loginFormContainer" + app:layout_constraintStart_toStartOf="@id/loginFormContainer" + app:layout_constraintTop_toTopOf="@id/loginFormContainer" /> diff --git a/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml index c61babea95..20e050fe9e 100644 --- a/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml +++ b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml @@ -6,94 +6,77 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + + + + + + + android:layout_marginTop="26dp" + app:errorEnabled="true" + tools:hint="@string/login_set_email_optional_hint"> - - - - - - - + android:imeOptions="actionDone" + android:maxLines="1" + tools:inputType="textEmailAddress" /> - + - + - + android:layout_gravity="start" + android:visibility="gone" + tools:text="@string/login_msisdn_confirm_send_again" + tools:visibility="visible" /> - + - + - - - - - +
diff --git a/vector/src/main/res/layout/fragment_login_reset_password.xml b/vector/src/main/res/layout/fragment_login_reset_password.xml index 7f09f0444b..b1dd495417 100644 --- a/vector/src/main/res/layout/fragment_login_reset_password.xml +++ b/vector/src/main/res/layout/fragment_login_reset_password.xml @@ -5,115 +5,98 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + + + + + + + android:layout_marginTop="32dp" + android:hint="@string/login_reset_password_email_hint" + app:errorEnabled="true"> - - - + android:inputType="textEmailAddress" + android:maxLines="1" /> - + + + + android:hint="@string/login_reset_password_password_hint" + app:errorEnabled="true" + app:errorIconDrawable="@null"> + android:ems="10" + android:inputType="textPassword" + android:maxLines="1" + android:paddingEnd="48dp" + android:paddingRight="48dp" + tools:ignore="RtlSymmetry" /> - - - - - - - - - - - - - + android:layout_marginTop="8dp" + android:background="?attr/selectableItemBackground" + android:scaleType="center" + android:src="@drawable/ic_eye_black" + android:tint="?attr/colorAccent" + tools:contentDescription="@string/a11y_show_password" /> - + - + + + diff --git a/vector/src/main/res/layout/fragment_login_reset_password_success.xml b/vector/src/main/res/layout/fragment_login_reset_password_success.xml index aaa54937a2..bffb9c11ef 100644 --- a/vector/src/main/res/layout/fragment_login_reset_password_success.xml +++ b/vector/src/main/res/layout/fragment_login_reset_password_success.xml @@ -1,63 +1,45 @@ - + - + + + android:text="@string/login_reset_password_success_title" + android:textAppearance="@style/TextAppearance.Vector.Login.Title" /> - + - + - + - - - - - - - + diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index a5b2c32084..d120aa3a75 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -1,200 +1,188 @@ - - + - + + + + + + + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:background="@drawable/bg_login_server_selector" + android:contentDescription="@string/login_a11y_choose_matrix_org" + android:minHeight="80dp" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingEnd="@dimen/layout_horizontal_margin" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/loginServerText"> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceMatrixOrgIcon" /> - + - + - + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_url_form.xml b/vector/src/main/res/layout/fragment_login_server_url_form.xml index a6bf98bbe4..a0476673e3 100644 --- a/vector/src/main/res/layout/fragment_login_server_url_form.xml +++ b/vector/src/main/res/layout/fragment_login_server_url_form.xml @@ -6,99 +6,82 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + + + + + + + + + + + android:layout_marginTop="26dp" + app:errorEnabled="true" + tools:hint="@string/login_server_url_form_modular_hint"> - - - - - - - - - - - + android:imeOptions="actionDone" + android:inputType="textUri" + android:maxLines="1" /> - + - + - - - - - - + + diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index de9a7e403c..32bc445884 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -1,91 +1,79 @@ - - + - + - + - + - + - + + - - + + - - + - - - - + diff --git a/vector/src/main/res/layout/fragment_login_terms.xml b/vector/src/main/res/layout/fragment_login_terms.xml index e44139e18b..ecad15079c 100644 --- a/vector/src/main/res/layout/fragment_login_terms.xml +++ b/vector/src/main/res/layout/fragment_login_terms.xml @@ -2,52 +2,49 @@ - + - + - + - + - + + + + diff --git a/vector/src/main/res/values-land/styles_login.xml b/vector/src/main/res/values-land/styles_login.xml new file mode 100644 index 0000000000..29ddebedd2 --- /dev/null +++ b/vector/src/main/res/values-land/styles_login.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml index 9e74e8abec..989b1615d9 100644 --- a/vector/src/main/res/values/styles_login.xml +++ b/vector/src/main/res/values/styles_login.xml @@ -8,10 +8,39 @@ 36dp - + + + + + + + - + + - From 11bc7051fd45b5a5bbd01650d4d4287195e181a1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Nov 2019 11:29:12 +0100 Subject: [PATCH 089/132] Login screens: splash scrollable --- .../main/res/layout/fragment_login_splash.xml | 223 +++++++++--------- 1 file changed, 116 insertions(+), 107 deletions(-) diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index c237cb8aba..90f29764ea 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -1,125 +1,134 @@ - - + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + - + - - + - + + - + - + - + - + - + - + - + + + + From af45c554fde3f69fcc815d796e5d76e8373d4bf7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Nov 2019 11:41:01 +0100 Subject: [PATCH 090/132] Login screens: fix scroll issue --- vector/src/main/res/layout/fragment_login_terms.xml | 2 -- vector/src/main/res/values/styles_login.xml | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vector/src/main/res/layout/fragment_login_terms.xml b/vector/src/main/res/layout/fragment_login_terms.xml index 10bf9683b4..10830daaea 100644 --- a/vector/src/main/res/layout/fragment_login_terms.xml +++ b/vector/src/main/res/layout/fragment_login_terms.xml @@ -14,8 +14,6 @@ @id/loginFormContainer - 32dp - 32dp 36dp 36dp match_parent @@ -43,6 +41,9 @@ parent 0dp wrap_content + false + 32dp + 32dp + + \ No newline at end of file diff --git a/vector/src/main/res/values/integers.xml b/vector/src/main/res/values/integers.xml index 59c1327f30..75e8bb6f9a 100644 --- a/vector/src/main/res/values/integers.xml +++ b/vector/src/main/res/values/integers.xml @@ -1,7 +1,11 @@ - 500 + 200 + + 400 + + 200 0 diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml index c753e13aff..3bcda048dc 100644 --- a/vector/src/main/res/values/styles_login.xml +++ b/vector/src/main/res/values/styles_login.xml @@ -22,7 +22,9 @@ parent - From c06b8486eac9befcaec6eaac0d7110a52d5d095d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 15:36:54 +0100 Subject: [PATCH 130/132] Update wording --- vector/src/main/res/values/strings_riotX.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 9bc13cdec2..5299d0db1f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -46,7 +46,7 @@ Sign in to %1$s Sign Up Sign In - Sign In with SSO + Continue with SSO Modular Address Address From e23763e6db176b48f91e9d75a8bbf58b852dd993 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 15:43:39 +0100 Subject: [PATCH 131/132] Update password from email twice --- .../java/im/vector/riotx/features/login/LoginViewModel.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 7dc8a1119b..de76f6b416 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -322,13 +322,15 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi if (safeLoginWizard == null) { setState { copy( - asyncResetPassword = Fail(Throwable("Bad configuration")) + asyncResetPassword = Fail(Throwable("Bad configuration")), + asyncResetMailConfirmed = Uninitialized ) } } else { setState { copy( - asyncResetPassword = Loading() + asyncResetPassword = Loading(), + asyncResetMailConfirmed = Uninitialized ) } @@ -360,12 +362,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi if (safeLoginWizard == null) { setState { copy( + asyncResetPassword = Uninitialized, asyncResetMailConfirmed = Fail(Throwable("Bad configuration")) ) } } else { setState { copy( + asyncResetPassword = Uninitialized, asyncResetMailConfirmed = Loading() ) } From 938289e8eba424c175de80b7f8bc265fc7105eb7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 15:44:08 +0100 Subject: [PATCH 132/132] ktlint --- .../main/java/im/vector/riotx/features/login/LoginActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index eb1dc4a0e6..2dec402f85 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -128,7 +128,7 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // TODO Disabled because it provokes a flickering - //ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() is LoginNavigation.OnSignModeSelected -> onSignModeSelected()