Merge pull request #689 from vector-im/feature/signin_signup

Login and Registration
This commit is contained in:
Benoit Marty 2019-12-02 13:47:28 +01:00 committed by GitHub
commit 3f4f7457c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
172 changed files with 8335 additions and 748 deletions

View file

@ -3,7 +3,9 @@
<words>
<w>backstack</w>
<w>bytearray</w>
<w>checkables</w>
<w>ciphertext</w>
<w>coroutine</w>
<w>decryptor</w>
<w>emoji</w>
<w>emojis</w>
@ -12,8 +14,11 @@
<w>linkified</w>
<w>linkify</w>
<w>megolm</w>
<w>msisdn</w>
<w>pbkdf</w>
<w>pkcs</w>
<w>signin</w>
<w>signup</w>
</words>
</dictionary>
</component>

View file

@ -2,7 +2,8 @@ Changes in RiotX 0.9.0 (2019-XX-XX)
===================================================
Features ✨:
-
- Account creation. It's now possible to create account on any homeserver with RiotX (#34)
- Iteration of the login flow (#613)
Improvements 🙌:
- Send mention Pills from composer

260
docs/signin.md Normal file
View file

@ -0,0 +1,260 @@
# Sign in to a homeserver
This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver.
## Sign up flows
### Get the flow
Client request the sign-in flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`)
> curl -X GET 'https://matrix.org/_matrix/client/r0/login'
200
```json
{
"flows": [
{
"type": "m.login.password"
}
]
}
```
### Login with username
The user is able to connect using `m.login.password`
> curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
```json
{
"identifier": {
"type": "m.id.user",
"user": "alice"
},
"password": "weak_password",
"type": "m.login.password",
"initial_device_display_name": "Portable"
}
```
#### Incorrect password
403
```json
{
"errcode": "M_FORBIDDEN",
"error": "Invalid password"
}
```
#### Correct password:
We get credential (200)
```json
{
"user_id": "@benoit0816:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
"home_server": "matrix.org",
"device_id": "GTVREDALBF",
"well_known": {
"m.homeserver": {
"base_url": "https:\/\/matrix.org\/"
}
}
}
```
### Login with email
If the user has associated an email with its account, he can signin using the email.
> curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@yopmail.com"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
```json
{
"identifier": {
"type": "m.id.thirdparty",
"medium": "email",
"address": "alice@yopmail.com"
},
"password": "weak_password",
"type": "m.login.password",
"initial_device_display_name": "Portable"
}
```
#### Unknown email
403
```json
{
"errcode": "M_FORBIDDEN",
"error": ""
}
```
#### Known email, wrong password
403
```json
{
"errcode": "M_FORBIDDEN",
"error": "Invalid password"
}
```
##### Known email, correct password
We get the credentials (200)
```json
{
"user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg",
"home_server": "matrix.org",
"device_id": "WBSREDASND",
"well_known": {
"m.homeserver": {
"base_url": "https:\/\/matrix.org\/"
}
}
}
```
### Login with Msisdn
Not supported yet in RiotX
### Login with SSO
> curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login'
200
```json
{
"flows": [
{
"type": "m.login.sso"
}
]
}
```
In this case, the user can click on "Sign in with SSO" and the web screen will be displayed on the page `https://homeserver.with.sso/_matrix/static/client/login/` and the credentials will be passed back to the native code through the JS bridge
## Reset password
Ref: `https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-password-email-requesttoken`
When the user has forgotten his password, he can reset it by providing an email and a new password.
Here is the flow:
### Send email
User is asked to enter the email linked to his account and a new password.
We display a warning regarding e2e.
At the first step, we do not send the password, only the email and a client secret, generated by the application
> curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken'
```json
{
"client_secret": "6c57f284-85e2-421b-8270-fb1795a120a7",
"send_attempt": 0,
"email": "user@domain.com"
}
```
#### When the email is not known
We get a 400
```json
{
"errcode": "M_THREEPID_NOT_FOUND",
"error": "Email not found"
}
```
#### When the email is known
We get a 200 with a `sid`
```json
{
"sid": "tQNbrREDACTEDldA"
}
```
Then the user is asked to click on the link in the email he just received, and to confirm when it's done.
During this step, the new password is sent to the homeserver.
If the user confirms before the link is clicked, we get an error:
> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
```json
{
"auth": {
"type": "m.login.email.identity",
"threepid_creds": {
"client_secret": "6c57f284-85e2-421b-8270-fb1795a120a7",
"sid": "tQNbrREDACTEDldA"
}
},
"new_password": "weak_password"
}
```
401
```json
{
"errcode": "M_UNAUTHORIZED",
"error": ""
}
```
### User clicks on the link
The link has the form:
https://matrix.org/_matrix/client/unstable/password_reset/email/submit_token?token=fzZLBlcqhTKeaFQFSRbsQnQCkzbwtGAD&client_secret=6c57f284-85e2-421b-8270-fb1795a120a7&sid=tQNbrREDACTEDldA
It contains the client secret, a token and the sid
When the user click the link, if validate his ownership and the new password can now be ent by the application (on user demand):
> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
```json
{
"auth": {
"type": "m.login.email.identity",
"threepid_creds": {
"client_secret": "6c57f284-85e2-421b-8270-fb1795a120a7",
"sid": "tQNbrREDACTEDldA"
}
},
"new_password": "weak_password"
}
```
200
```json
{}
```
The password has been changed, and all the existing token are invalidated. User can now login with the new password.

579
docs/signup.md Normal file
View file

@ -0,0 +1,579 @@
# Sign up to a homeserver
This document describes the flow of registration to a homeserver. Examples come from the `matrix.org` homeserver.
*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 known (in the example it's `https://matrix.org`)
> curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register'
```json
{
}
```
We get the flows with a 401, which also means the the registration is possible on this homeserver.
```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"
}
}
}
}
}
}
```
If the registration is not possible, we get a 403
```json
{
"errcode": "M_FORBIDDEN",
"error": "Registration is disabled"
}
```
### Step 1: entering user name and password
The app is displaying a form to enter username and password.
> curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register'
```json
{
"initial_device_display_name": "Mobile device",
"username": "alice",
"password": "weak_password"
}
```
401. Note that the `session` value has changed (because we did not provide the previous value in the request body), but it's ok, we will use the new value for the next steps.
```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
We get a 400:
```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"}}' 'https://matrix.org/_matrix/client/r0/register'
```json
{
"auth": {
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"type": "m.login.dummy"
}
}
```
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
We request a token to the homeserver. The `client_secret` is generated by the application
> 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"}}' '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"
}
}
```
We get 401 since the email is not validated yet:
```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"}}' '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"
}
}
```
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
- The `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"}}' 'https://matrix.org/_matrix/client/r0/register'
```json
{
"auth": {
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"type": "m.login.terms"
}
}
```
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":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' '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": "xptUYoREDACTEDogOWAGVnbJQ",
"type": "m.login.recaptcha"
}
}
```
200
```json
{
"user_id": "@alice:matrix.org",
"home_server": "matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmcKMoREDACTEDo50aWZpZXIga2V5CjAwMTBjaWQgZ2VuID0gMQowMDI5Y2lkIHVzZXJfaWQgPSBAYmVub2l0eHh4eDptYXRoREDACTEDoCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNHVSVm00aVFDaWlKdoREDACTEDoJmc2lnbmF0dXJlIOmHnTLRfxiPjhrWhS-dThUX-qAzZktfRThzH1YyAsxaCg",
"device_id": "FLBAREDAJZ"
}
```
The account is created!
### Step 5: MSISDN
Some homeservers may require the user to enter MSISDN.
On matrix.org, it's not required, and not even optional, but it's still possible for the app to add a MSISDN during the registration.
The user enter a phone number and select a country, the `client_secret` is generated by the application
> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}' 'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken'
```json
{
"client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7",
"send_attempt": 1,
"country": "FR",
"phone_number": "+33611223344"
}
```
If the msisdn is already associated to another account, you will received an error:
```json
{
"errcode": "M_THREEPID_IN_USE",
"error": "Phone number is already in use"
}
```
If it is not the case, the homeserver send the SMS and returns some data, especially a `sid` and a `submit_url`:
```json
{
"msisdn": "33611223344",
"intl_fmt": "+336 11 22 33 44",
"success": true,
"sid": "1678881798",
"submit_url": "https:\/\/matrix.org\/_matrix\/client\/unstable\/add_threepid\/msisdn\/submit_token"
}
```
When you execute the register request, with the received `sid`, you get an error since the MSISDN is not validated yet:
> curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
```json
"auth": {
"type": "m.login.msisdn",
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"threepid_creds": {
"client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7",
"sid": "1678881798"
}
}
}
```
There is an issue on Synapse, which return a 401, it sends too much data along with the classical MatrixError fields:
```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": "6LcgI54UAAAAABGdGmruw6DdOocFpYVdjYBRe4zb"
},
"m.login.terms": {
"policies": {
"privacy_policy": {
"version": "1.0",
"en": {
"name": "Terms and Conditions",
"url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0"
}
}
}
}
},
"completed": [],
"error": "",
"errcode": "M_UNAUTHORIZED"
}
```
The user receive the SMS, he can enter the SMS code in the app, which is sent using the "submit_url" received ie the response of the `requestToken` request:
> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token'
```json
{
"client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7",
"sid": "1678881798",
"token": "123456"
}
```
If the code is not correct, we get a 200 with:
```json
{
"success": false
}
```
And if the code is correct we get a 200 with:
```json
{
"success": true
}
```
We can now execute the registration request, to the homeserver
> curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
```json
{
"auth": {
"type": "m.login.msisdn",
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"threepid_creds": {
"client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7",
"sid": "1678881798"
}
}
}
```
Now the homeserver consider that the `m.login.msisdn` step is completed (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": "6LcgI54UAAAAABGdGmruw6DdOocFpYVdjYBRe4zb"
},
"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.msisdn"
]
}
```

View file

@ -21,7 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.OkReplayRuleChainNoActivity
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.AuthenticationService
import okreplay.*
import org.junit.ClassRule
import org.junit.Rule
@ -29,9 +29,9 @@ import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class AuthenticatorTest : InstrumentedTest {
internal class AuthenticationServiceTest : InstrumentedTest {
lateinit var authenticator: Authenticator
lateinit var authenticationService: AuthenticationService
lateinit var okReplayInterceptor: OkReplayInterceptor
private val okReplayConfig = OkReplayConfig.Builder()

View file

@ -22,7 +22,7 @@ import androidx.work.Configuration
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.AuthenticationService
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.di.DaggerMatrixComponent
import im.vector.matrix.android.internal.network.UserAgentHolder
@ -46,7 +46,7 @@ data class MatrixConfiguration(
*/
class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) {
@Inject internal lateinit var authenticator: Authenticator
@Inject internal lateinit var authenticationService: AuthenticationService
@Inject internal lateinit var userAgentHolder: UserAgentHolder
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager
@ -64,8 +64,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun getUserAgent() = userAgentHolder.userAgent
fun authenticator(): Authenticator {
return authenticator
fun authenticationService(): AuthenticationService {
return authenticationService
}
companion object {

View file

@ -19,29 +19,48 @@ package im.vector.matrix.android.api.auth
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.login.LoginWizard
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.LoginFlowResponse
/**
* This interface defines methods to authenticate to a matrix server.
* This interface defines methods to authenticate or to create an account to a matrix server.
*/
interface Authenticator {
interface AuthenticationService {
/**
* Request the supported login flows for this homeserver
* Request the supported login flows for this homeserver.
* This is the first method to call to be able to get a wizard to login or the create an account
*/
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable
/**
* @param homeServerConnectionConfig this param is used to configure the Homeserver
* @param login the login field
* @param password the password field
* @param callback the matrix callback on which you'll receive the result of authentication.
* @return return a [Cancelable]
* Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first.
*/
fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String, callback: MatrixCallback<Session>): Cancelable
fun getLoginWizard(): LoginWizard
/**
* Return a RegistrationWizard, to create an matrix account on the homeserver. The login flow has to be retrieved first.
*/
fun getRegistrationWizard(): RegistrationWizard
/**
* True when login and password has been sent with success to the homeserver
*/
val isRegistrationStarted: Boolean
/**
* Cancel pending login or pending registration
*/
fun cancelPendingLoginOrRegistration()
/**
* Reset all pending settings, including current HomeServerConnectionConfig
*/
fun reset()
/**
* Check if there is an authenticated [Session].
@ -67,5 +86,7 @@ interface Authenticator {
/**
* Create a session after a SSO successful login
*/
fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<Session>): Cancelable
fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
credentials: Credentials,
callback: MatrixCallback<Session>): Cancelable
}

View file

@ -30,4 +30,7 @@ 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?,
// Optional data that may contain info to override home server and/or identity server
@Json(name = "well_known") val wellKnown: WellKnown? = null
)

View file

@ -25,7 +25,7 @@ import okhttp3.TlsVersion
/**
* This data class holds how to connect to a specific Homeserver.
* It's used with [im.vector.matrix.android.api.auth.Authenticator] class.
* It's used with [im.vector.matrix.android.api.auth.AuthenticationService] class.
* You should use the [Builder] to create one.
*/
@JsonClass(generateAdapter = true)

View file

@ -0,0 +1,29 @@
/*
* 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.data
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
// Either a LoginFlowResponse, or an error if the homeserver is outdated
sealed class LoginFlowResult {
data class Success(
val loginFlowResponse: LoginFlowResponse,
val isLoginAndRegistrationSupported: Boolean
) : LoginFlowResult()
object OutdatedHomeserver : LoginFlowResult()
}

View file

@ -0,0 +1,111 @@
/*
* 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.api.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions
*
* Ex:
* <pre>
* {
* "unstable_features": {
* "m.lazy_load_members": true
* },
* "versions": [
* "r0.0.1",
* "r0.1.0",
* "r0.2.0",
* "r0.3.0"
* ]
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
data class Versions(
@Json(name = "versions")
val supportedVersions: List<String>? = null,
@Json(name = "unstable_features")
val unstableFeatures: Map<String, Boolean>? = null
)
// MatrixClientServerAPIVersion
private const val r0_0_1 = "r0.0.1"
private const val r0_1_0 = "r0.1.0"
private const val r0_2_0 = "r0.2.0"
private const val r0_3_0 = "r0.3.0"
private const val r0_4_0 = "r0.4.0"
private const val r0_5_0 = "r0.5.0"
private const val r0_6_0 = "r0.6.0"
// MatrixVersionsFeature
private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
/**
* Return true if the SDK supports this homeserver version
*/
fun Versions.isSupportedBySdk(): Boolean {
return supportLazyLoadMembers()
}
/**
* Return true if the SDK supports this homeserver version for login and registration
*/
fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
return !doesServerRequireIdentityServerParam()
&& doesServerAcceptIdentityAccessToken()
&& doesServerSeparatesAddAndBind()
}
/**
* Return true if the server support the lazy loading of room members
*
* @return true if the server support the lazy loading of room members
*/
private fun Versions.supportLazyLoadMembers(): Boolean {
return supportedVersions?.contains(r0_5_0) == true
|| unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true
}
/**
* Indicate if the `id_server` parameter is required when registering with an 3pid,
* adding a 3pid or resetting password.
*/
private fun Versions.doesServerRequireIdentityServerParam(): Boolean {
if (supportedVersions?.contains(r0_6_0) == true) return false
return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true
}
/**
* Indicate if the `id_access_token` parameter can be safely passed to the homeserver.
* Some homeservers may trigger errors if they are not prepared for the new parameter.
*/
private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean {
return supportedVersions?.contains(r0_6_0) == true
|| unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false
}
private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
return supportedVersions?.contains(r0_6_0) == true
|| unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
}

View file

@ -0,0 +1,82 @@
/*
* 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.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
* <pre>
* {
* "m.homeserver": {
* "base_url": "https://matrix.org"
* },
* "m.identity_server": {
* "base_url": "https://vector.im"
* }
* "m.integrations": {
* "managers": [
* {
* "api_url": "https://integrations.example.org",
* "ui_url": "https://integrations.example.org/ui"
* },
* {
* "api_url": "https://bots.example.org"
* }
* ]
* }
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
data class WellKnown(
@Json(name = "m.homeserver")
var homeServer: WellKnownBaseConfig? = null,
@Json(name = "m.identity_server")
var identityServer: WellKnownBaseConfig? = null,
@Json(name = "m.integrations")
var integrations: Map<String, @JvmSuppressWildcards Any>? = null
) {
/**
* Returns the list of integration managers proposed
*/
fun getIntegrationManagers(): List<WellKnownManagerConfig> {
val managers = ArrayList<WellKnownManagerConfig>()
integrations?.get("managers")?.let {
(it as? ArrayList<*>)?.let { configs ->
configs.forEach { config ->
(config as? Map<*, *>)?.let { map ->
val apiUrl = map["api_url"] as? String
val uiUrl = map["ui_url"] as? String ?: apiUrl
if (apiUrl != null
&& apiUrl.startsWith("https://")
&& uiUrl!!.startsWith("https://")) {
managers.add(WellKnownManagerConfig(
apiUrl = apiUrl,
uiUrl = uiUrl
))
}
}
}
}
}
return managers
}
}

View file

@ -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.matrix.android.api.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
* <pre>
* {
* "base_url": "https://vector.im"
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
data class WellKnownBaseConfig(
@Json(name = "base_url")
val baseURL: String? = null
)

View file

@ -0,0 +1,21 @@
/*
* 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.data
data class WellKnownManagerConfig(
val apiUrl : String,
val uiUrl: String
)

View file

@ -0,0 +1,48 @@
/*
* 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.login
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
interface LoginWizard {
/**
* @param login the login field
* @param password the password field
* @param deviceName the initial device name
* @param callback the matrix callback on which you'll receive the result of authentication.
* @return return a [Cancelable]
*/
fun login(login: String,
password: String,
deviceName: String,
callback: MatrixCallback<Session>): Cancelable
/**
* Reset user password
*/
fun resetPassword(email: String,
newPassword: String,
callback: MatrixCallback<Unit>): Cancelable
/**
* Confirm the new password, once the user has checked his email
*/
fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable
}

View file

@ -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.matrix.android.api.auth.registration
sealed class RegisterThreePid {
data class Email(val email: String) : RegisterThreePid()
data class Msisdn(val msisdn: String, val countryCode: String) : RegisterThreePid()
}

View file

@ -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.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<Stage>,
val completedStages: List<Stage>
)

View file

@ -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.api.auth.registration
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface RegistrationWizard {
fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable
fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback<RegistrationResult>): Cancelable
fun performReCaptcha(response: String, callback: MatrixCallback<RegistrationResult>): Cancelable
fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable
fun dummy(callback: MatrixCallback<RegistrationResult>): Cancelable
fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable
fun sendAgainThreePid(callback: MatrixCallback<RegistrationResult>): Cancelable
fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable
fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback<RegistrationResult>): Cancelable
val currentThreePid: String?
// True when login and password has been sent with success to the homeserver
val isRegistrationStarted: Boolean
}

View file

@ -0,0 +1,44 @@
/*
* 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
sealed class Stage(open val mandatory: Boolean) {
// 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) : Stage(mandatory)
// m.login.msisdn
data class Msisdn(override val mandatory: Boolean) : Stage(mandatory)
// m.login.token
// m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username
// and a password, the dummy stage has to be done
data class Dummy(override val mandatory: Boolean) : Stage(mandatory)
// Undocumented yet: m.login.terms
data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory)
// For unknown stages
data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory)
}
typealias TermPolicies = Map<*, *>

View file

@ -34,6 +34,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
// When server send an error, but it cannot be interpreted as a MatrixError
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody))

View file

@ -31,7 +31,9 @@ data class MatrixError(
@Json(name = "consent_uri") val consentUri: String? = null,
// RESOURCE_LIMIT_EXCEEDED data
@Json(name = "limit_type") val limitType: String? = null,
@Json(name = "admin_contact") val adminUri: String? = null) {
@Json(name = "admin_contact") val adminUri: String? = null,
// For LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) {
companion object {
const val FORBIDDEN = "M_FORBIDDEN"

View file

@ -29,3 +29,5 @@ interface Cancelable {
// no-op
}
}
object NoOpCancellable : Cancelable

View file

@ -17,20 +17,47 @@
package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.Versions
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.login.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.auth.registration.*
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.*
/**
* The login REST API.
*/
internal interface AuthAPI {
/**
* Get the version information of the homeserver
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions")
fun versions(): Call<Versions>
/**
* Register to the homeserver
* Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
fun register(@Body registrationParams: RegistrationParams): Call<Credentials>
/**
* Add 3Pid during registration
* Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928
* https://github.com/matrix-org/matrix-doc/pull/2290
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/{threePid}/requestToken")
fun add3Pid(@Path("threePid") threePid: String, @Body params: AddThreePidRegistrationParams): Call<AddThreePidRegistrationResponse>
/**
* Validate 3pid
*/
@POST
fun validate3Pid(@Url url: String, @Body params: ValidationCodeBody): Call<SuccessResult>
/**
* Get the supported login flow
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login
@ -47,4 +74,16 @@ internal interface AuthAPI {
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>
/**
* Ask the homeserver to reset the password associated with the provided email.
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken")
fun resetPassword(@Body params: AddThreePidRegistrationParams): Call<AddThreePidRegistrationResponse>
/**
* Ask the homeserver to reset the password with the provided new password once the email is validated.
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed): Call<Unit>
}

View file

@ -20,8 +20,10 @@ import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.internal.auth.db.AuthRealmMigration
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthDatabase
@ -50,7 +52,8 @@ internal abstract class AuthModule {
}
.name("matrix-sdk-auth.realm")
.modules(AuthRealmModule())
.deleteRealmIfMigrationNeeded()
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
.migration(AuthRealmMigration())
.build()
}
}
@ -59,5 +62,11 @@ internal abstract class AuthModule {
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
@Binds
abstract fun bindAuthenticator(authenticator: DefaultAuthenticator): Authenticator
abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore
@Binds
abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService
@Binds
abstract fun bindSessionCreator(sessionCreator: DefaultSessionCreator): SessionCreator
}

View file

@ -0,0 +1,205 @@
/*
* 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
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.*
import im.vector.matrix.android.api.auth.login.LoginWizard
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.SessionManager
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.toCancelable
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import javax.inject.Inject
internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
) : AuthenticationService {
private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData()
private var currentLoginWizard: LoginWizard? = null
private var currentRegistrationWizard: RegistrationWizard? = null
override fun hasAuthenticatedSessions(): Boolean {
return sessionParamsStore.getLast() != null
}
override fun getLastAuthenticatedSession(): Session? {
val sessionParams = sessionParamsStore.getLast()
return sessionParams?.let {
sessionManager.getOrCreateSession(it)
}
}
override fun getSession(sessionParams: SessionParams): Session? {
return sessionManager.getOrCreateSession(sessionParams)
}
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable {
pendingSessionData = null
return GlobalScope.launch(coroutineDispatchers.main) {
pendingSessionStore.delete()
val result = runCatching {
getLoginFlowInternal(homeServerConnectionConfig)
}
result.fold(
{
if (it is LoginFlowResult.Success) {
// The homeserver exists and up to date, keep the config
pendingSessionData = PendingSessionData(homeServerConnectionConfig)
.also { data -> pendingSessionStore.savePendingSessionData(data) }
}
callback.onSuccess(it)
},
{
callback.onFailure(it)
}
)
}
.toCancelable()
}
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
// First check the homeserver version
val versions = executeRequest<Versions> {
apiCall = authAPI.versions()
}
if (versions.isSupportedBySdk()) {
// Get the login flow
val loginFlowResponse = executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows()
}
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk())
} else {
// Not supported
LoginFlowResult.OutdatedHomeserver
}
}
override fun getRegistrationWizard(): RegistrationWizard {
return currentRegistrationWizard
?: let {
pendingSessionData?.homeServerConnectionConfig?.let {
DefaultRegistrationWizard(
okHttpClient,
retrofitFactory,
coroutineDispatchers,
sessionCreator,
pendingSessionStore
).also {
currentRegistrationWizard = it
}
} ?: error("Please call getLoginFlow() with success first")
}
}
override val isRegistrationStarted: Boolean
get() = currentRegistrationWizard?.isRegistrationStarted == true
override fun getLoginWizard(): LoginWizard {
return currentLoginWizard
?: let {
pendingSessionData?.homeServerConnectionConfig?.let {
DefaultLoginWizard(
okHttpClient,
retrofitFactory,
coroutineDispatchers,
sessionCreator,
pendingSessionStore
).also {
currentLoginWizard = it
}
} ?: error("Please call getLoginFlow() with success first")
}
}
override fun cancelPendingLoginOrRegistration() {
currentLoginWizard = null
currentRegistrationWizard = null
// Keep only the home sever config
// Update the local pendingSessionData synchronously
pendingSessionData = pendingSessionData?.homeServerConnectionConfig
?.let { PendingSessionData(it) }
.also {
GlobalScope.launch(coroutineDispatchers.main) {
if (it == null) {
// Should not happen
pendingSessionStore.delete()
} else {
pendingSessionStore.savePendingSessionData(it)
}
}
}
}
override fun reset() {
currentLoginWizard = null
currentRegistrationWizard = null
pendingSessionData = null
GlobalScope.launch(coroutineDispatchers.main) {
pendingSessionStore.delete()
}
}
override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
credentials: Credentials,
callback: MatrixCallback<Session>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
createSessionFromSso(credentials, homeServerConnectionConfig)
}
}
private suspend fun createSessionFromSso(credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) {
sessionCreator.createSession(credentials, homeServerConnectionConfig)
}
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)
}
}

View file

@ -1,138 +0,0 @@
/*
* 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
import android.util.Patterns
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.SessionManager
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.data.ThreePidMedium
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
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 kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import javax.inject.Inject
internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager
) : Authenticator {
override fun hasAuthenticatedSessions(): Boolean {
return sessionParamsStore.getLast() != null
}
override fun getLastAuthenticatedSession(): Session? {
val sessionParams = sessionParamsStore.getLast()
return sessionParams?.let {
sessionManager.getOrCreateSession(it)
}
}
override fun getSession(sessionParams: SessionParams): Session? {
return sessionManager.getOrCreateSession(sessionParams)
}
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val result = runCatching {
getLoginFlowInternal(homeServerConnectionConfig)
}
result.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
login: String,
password: String,
callback: MatrixCallback<Session>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching {
authenticate(homeServerConnectionConfig, login, password)
}
sessionOrFailure.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows()
}
}
private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
login: String,
password: String) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, "Mobile")
} else {
PasswordLoginParams.userIdentifier(login, password, "Mobile")
}
val credentials = executeRequest<Credentials> {
apiCall = authAPI.login(loginParams)
}
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
sessionManager.getOrCreateSession(sessionParams)
}
override fun createSessionFromSso(credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig,
callback: MatrixCallback<Session>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching {
createSessionFromSso(credentials, homeServerConnectionConfig)
}
sessionOrFailure.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun createSessionFromSso(credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) {
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
sessionManager.getOrCreateSession(sessionParams)
}
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)
}
}

View file

@ -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.internal.auth
import im.vector.matrix.android.internal.auth.db.PendingSessionData
/**
* Store for elements when doing login or registration
*/
internal interface PendingSessionStore {
suspend fun savePendingSessionData(pendingSessionData: PendingSessionData)
fun getPendingSessionData(): PendingSessionData?
suspend fun delete()
}

View file

@ -0,0 +1,68 @@
/*
* 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
import android.net.Uri
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.SessionManager
import timber.log.Timber
import javax.inject.Inject
internal interface SessionCreator {
suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
}
internal class DefaultSessionCreator @Inject constructor(
private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager,
private val pendingSessionStore: PendingSessionStore
) : SessionCreator {
/**
* Credentials can affect the homeServerConnectionConfig, override home server url and/or
* identity server url if provided in the credentials
*/
override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
// We can cleanup the pending session params
pendingSessionStore.delete()
val sessionParams = SessionParams(
credentials = credentials,
homeServerConnectionConfig = homeServerConnectionConfig.copy(
homeServerUri = credentials.wellKnown?.homeServer?.baseURL
// remove trailing "/"
?.trim { it == '/' }
?.takeIf { it.isNotBlank() }
?.also { Timber.d("Overriding homeserver url to $it") }
?.let { Uri.parse(it) }
?: homeServerConnectionConfig.homeServerUri,
identityServerUri = credentials.wellKnown?.identityServer?.baseURL
// remove trailing "/"
?.trim { it == '/' }
?.takeIf { it.isNotBlank() }
?.also { Timber.d("Overriding identity server url to $it") }
?.let { Uri.parse(it) }
?: homeServerConnectionConfig.identityServerUri
))
sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams)
}
}

View file

@ -30,12 +30,4 @@ data class InteractiveAuthenticationFlow(
@Json(name = "stages")
val stages: List<String>? = null
) {
companion object {
// Possible values for type
const val TYPE_LOGIN_SSO = "m.login.sso"
const val TYPE_LOGIN_TOKEN = "m.login.token"
const val TYPE_LOGIN_PASSWORD = "m.login.password"
}
}
)

View file

@ -25,4 +25,7 @@ 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"
const val TOKEN = "m.login.token"
const val SSO = "m.login.sso"
}

View file

@ -19,34 +19,46 @@ package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Ref:
* - https://matrix.org/docs/spec/client_server/r0.5.0#password-based
* - https://matrix.org/docs/spec/client_server/r0.5.0#identifier-types
*/
@JsonClass(generateAdapter = true)
internal data class PasswordLoginParams(@Json(name = "identifier") val identifier: Map<String, String>,
internal data class PasswordLoginParams(
@Json(name = "identifier") val identifier: Map<String, String>,
@Json(name = "password") val password: String,
@Json(name = "type") override val type: String,
@Json(name = "initial_device_display_name") val deviceDisplayName: String?,
@Json(name = "device_id") val deviceId: String?) : LoginParams {
companion object {
private const val IDENTIFIER_KEY_TYPE = "type"
val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
private const val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
private const val IDENTIFIER_KEY_USER = "user"
val IDENTIFIER_KEY_TYPE = "type"
val IDENTIFIER_KEY_MEDIUM = "medium"
val IDENTIFIER_KEY_ADDRESS = "address"
val IDENTIFIER_KEY_USER = "user"
val IDENTIFIER_KEY_COUNTRY = "country"
val IDENTIFIER_KEY_NUMBER = "number"
private const val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
private const val IDENTIFIER_KEY_MEDIUM = "medium"
private const val IDENTIFIER_KEY_ADDRESS = "address"
private const val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
private const val IDENTIFIER_KEY_COUNTRY = "country"
private const val IDENTIFIER_KEY_PHONE = "phone"
fun userIdentifier(user: String,
password: String,
deviceDisplayName: String? = null,
deviceId: String? = null): PasswordLoginParams {
val identifier = HashMap<String, String>()
identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_USER
identifier[IDENTIFIER_KEY_USER] = user
return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
return PasswordLoginParams(
mapOf(
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER,
IDENTIFIER_KEY_USER to user
),
password,
LoginFlowTypes.PASSWORD,
deviceDisplayName,
deviceId)
}
fun thirdPartyIdentifier(medium: String,
@ -54,11 +66,33 @@ internal data class PasswordLoginParams(@Json(name = "identifier") val identifie
password: String,
deviceDisplayName: String? = null,
deviceId: String? = null): PasswordLoginParams {
val identifier = HashMap<String, String>()
identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_THIRD_PARTY
identifier[IDENTIFIER_KEY_MEDIUM] = medium
identifier[IDENTIFIER_KEY_ADDRESS] = address
return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
return PasswordLoginParams(
mapOf(
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY,
IDENTIFIER_KEY_MEDIUM to medium,
IDENTIFIER_KEY_ADDRESS to address
),
password,
LoginFlowTypes.PASSWORD,
deviceDisplayName,
deviceId)
}
fun phoneIdentifier(country: String,
phone: String,
password: String,
deviceDisplayName: String? = null,
deviceId: String? = null): PasswordLoginParams {
return PasswordLoginParams(
mapOf(
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE,
IDENTIFIER_KEY_COUNTRY to country,
IDENTIFIER_KEY_PHONE to phone
),
password,
LoginFlowTypes.PASSWORD,
deviceDisplayName,
deviceId)
}
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.db
import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
internal class AuthRealmMigration : RealmMigration {
companion object {
// Current schema version
const val SCHEMA_VERSION = 1L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
if (oldVersion <= 0) {
Timber.d("Step 0 -> 1")
Timber.d("Create PendingSessionEntity")
realm.schema.create("PendingSessionEntity")
.addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
.setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
.addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
.addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
.setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
.addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
.addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
}
}
}

View file

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

View file

@ -0,0 +1,50 @@
/*
* 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.db
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
import im.vector.matrix.android.internal.auth.registration.ThreePidData
import java.util.*
/**
* This class holds all pending data when creating a session, either by login or by register
*/
internal data class PendingSessionData(
val homeServerConnectionConfig: HomeServerConnectionConfig,
/* ==========================================================================================
* Common
* ========================================================================================== */
val clientSecret: String = UUID.randomUUID().toString(),
val sendAttempt: Int = 0,
/* ==========================================================================================
* For login
* ========================================================================================== */
val resetPasswordData: ResetPasswordData? = null,
/* ==========================================================================================
* For register
* ========================================================================================== */
val currentSession: String? = null,
val isRegistrationStarted: Boolean = false,
val currentThreePidData: ThreePidData? = null
)

View file

@ -0,0 +1,29 @@
/*
* 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.db
import io.realm.RealmObject
internal open class PendingSessionEntity(
var homeServerConnectionConfigJson: String = "",
var clientSecret: String = "",
var sendAttempt: Int = 0,
var resetPasswordDataJson: String? = null,
var currentSession: String? = null,
var isRegistrationStarted: Boolean = false,
var currentThreePidDataJson: String? = null
) : RealmObject()

View file

@ -0,0 +1,69 @@
/*
* 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.db
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
import im.vector.matrix.android.internal.auth.registration.ThreePidData
import javax.inject.Inject
internal class PendingSessionMapper @Inject constructor(moshi: Moshi) {
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
private val resetPasswordDataAdapter = moshi.adapter(ResetPasswordData::class.java)
private val threePidDataAdapter = moshi.adapter(ThreePidData::class.java)
fun map(entity: PendingSessionEntity?): PendingSessionData? {
if (entity == null) {
return null
}
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson)!!
val resetPasswordData = entity.resetPasswordDataJson?.let { resetPasswordDataAdapter.fromJson(it) }
val threePidData = entity.currentThreePidDataJson?.let { threePidDataAdapter.fromJson(it) }
return PendingSessionData(
homeServerConnectionConfig = homeServerConnectionConfig,
clientSecret = entity.clientSecret,
sendAttempt = entity.sendAttempt,
resetPasswordData = resetPasswordData,
currentSession = entity.currentSession,
isRegistrationStarted = entity.isRegistrationStarted,
currentThreePidData = threePidData)
}
fun map(sessionData: PendingSessionData?): PendingSessionEntity? {
if (sessionData == null) {
return null
}
val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionData.homeServerConnectionConfig)
val resetPasswordDataJson = resetPasswordDataAdapter.toJson(sessionData.resetPasswordData)
val currentThreePidDataJson = threePidDataAdapter.toJson(sessionData.currentThreePidData)
return PendingSessionEntity(
homeServerConnectionConfigJson = homeServerConnectionConfigJson,
clientSecret = sessionData.clientSecret,
sendAttempt = sessionData.sendAttempt,
resetPasswordDataJson = resetPasswordDataJson,
currentSession = sessionData.currentSession,
isRegistrationStarted = sessionData.isRegistrationStarted,
currentThreePidDataJson = currentThreePidDataJson
)
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.db
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.di.AuthDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject
internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper,
@AuthDatabase
private val realmConfiguration: RealmConfiguration
) : PendingSessionStore {
override suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) {
awaitTransaction(realmConfiguration) { realm ->
val entity = mapper.map(pendingSessionData)
if (entity != null) {
realm.where(PendingSessionEntity::class.java)
.findAll()
.deleteAllFromRealm()
realm.insert(entity)
}
}
}
override fun getPendingSessionData(): PendingSessionData? {
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(PendingSessionEntity::class.java)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
}
}
override suspend fun delete() {
awaitTransaction(realmConfiguration) {
it.where(PendingSessionEntity::class.java)
.findAll()
.deleteAllFromRealm()
}
}
}

View file

@ -30,36 +30,33 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
) : SessionParamsStore {
override fun getLast(): SessionParams? {
val realm = Realm.getInstance(realmConfiguration)
val sessionParams = realm
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
.findAll()
.map { mapper.map(it) }
.lastOrNull()
realm.close()
return sessionParams
}
}
override fun get(userId: String): SessionParams? {
val realm = Realm.getInstance(realmConfiguration)
val sessionParams = realm
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, userId)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
realm.close()
return sessionParams
}
}
override fun getAll(): List<SessionParams> {
val realm = Realm.getInstance(realmConfiguration)
val sessionParams = realm
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
.findAll()
.mapNotNull { mapper.map(it) }
realm.close()
return sessionParams
}
}
override suspend fun save(sessionParams: SessionParams) {

View file

@ -0,0 +1,130 @@
/*
* 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.login
import android.util.Patterns
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
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.auth.AuthAPI
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
import im.vector.matrix.android.internal.auth.registration.RegisterAddThreePidTask
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
internal class DefaultLoginWizard(
okHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
) : LoginWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
private val authAPI = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString())
.create(AuthAPI::class.java)
override fun login(login: String,
password: String,
deviceName: String,
callback: MatrixCallback<Session>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
loginInternal(login, password, deviceName)
}
}
private suspend fun loginInternal(login: String,
password: String,
deviceName: String) = withContext(coroutineDispatchers.computation) {
val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName)
} else {
PasswordLoginParams.userIdentifier(login, password, deviceName)
}
val credentials = executeRequest<Credentials> {
apiCall = authAPI.login(loginParams)
}
sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
}
override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
resetPasswordInternal(email, newPassword)
}
}
private suspend fun resetPasswordInternal(email: String, newPassword: String) {
val param = RegisterAddThreePidTask.Params(
RegisterThreePid.Email(email),
pendingSessionData.clientSecret,
pendingSessionData.sendAttempt
)
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
.also { pendingSessionStore.savePendingSessionData(it) }
val result = executeRequest<AddThreePidRegistrationResponse> {
apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param))
}
pendingSessionData = pendingSessionData.copy(resetPasswordData = ResetPasswordData(newPassword, result))
.also { pendingSessionStore.savePendingSessionData(it) }
}
override fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable {
val safeResetPasswordData = pendingSessionData.resetPasswordData ?: run {
callback.onFailure(IllegalStateException("developer error, no reset password in progress"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
resetPasswordMailConfirmedInternal(safeResetPasswordData)
}
}
private suspend fun resetPasswordMailConfirmedInternal(resetPasswordData: ResetPasswordData) {
val param = ResetPasswordMailConfirmed.create(
pendingSessionData.clientSecret,
resetPasswordData.addThreePidRegistrationResponse.sid,
resetPasswordData.newPassword
)
executeRequest<Unit> {
apiCall = authAPI.resetPasswordMailConfirmed(param)
}
// Set to null?
// resetPasswordData = null
}
}

View file

@ -0,0 +1,29 @@
/*
* 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.login
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
/**
* Container to store the data when a reset password is in the email validation step
*/
@JsonClass(generateAdapter = true)
internal data class ResetPasswordData(
val newPassword: String,
val addThreePidRegistrationResponse: AddThreePidRegistrationResponse
)

View file

@ -0,0 +1,45 @@
/*
* 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.login
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.registration.AuthParams
/**
* Class to pass parameters to reset the password once a email has been validated.
*/
@JsonClass(generateAdapter = true)
internal data class ResetPasswordMailConfirmed(
// authentication parameters
@Json(name = "auth")
val auth: AuthParams? = null,
// the new password
@Json(name = "new_password")
val newPassword: String? = null
) {
companion object {
fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed {
return ResetPasswordMailConfirmed(
auth = AuthParams.createForResetPassword(clientSecret, sid),
newPassword = newPassword
)
}
}
}

View file

@ -0,0 +1,101 @@
/*
* 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 com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
/**
* Add a three Pid during authentication
*/
@JsonClass(generateAdapter = true)
internal data class AddThreePidRegistrationParams(
/**
* Required. A unique string generated by the client, and used to identify the validation attempt.
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed 255 characters and it must not be empty.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The server will only send an email if the send_attempt is a number greater than the most recent one which it has seen,
* scoped to that email + client_secret pair. This is to avoid repeatedly sending the same email in the case of request retries between
* the POSTing user and the identity server. The client should increment this value if they desire a new email (e.g. a reminder) to be sent.
* If they do not, the server should respond with success but not resend the email.
*/
@Json(name = "send_attempt")
val sendAttempt: Int,
/**
* Optional. When the validation is completed, the identity server will redirect the user to this URL. This option is ignored when
* submitting 3PID validation information through a POST request.
*/
@Json(name = "next_link")
val nextLink: String? = null,
/**
* Required. The hostname of the identity server to communicate with. May optionally include a port.
* This parameter is ignored when the homeserver handles 3PID verification.
*/
@Json(name = "id_server")
val id_server: String? = null,
/* ==========================================================================================
* For emails
* ========================================================================================== */
/**
* Required. The email address to validate.
*/
@Json(name = "email")
val email: String? = null,
/* ==========================================================================================
* For Msisdn
* ========================================================================================== */
/**
* Required. The two-letter uppercase ISO country code that the number in phone_number should be parsed as if it were dialled from.
*/
@Json(name = "country")
val countryCode: String? = null,
/**
* Required. The phone number to validate.
*/
@Json(name = "phone_number")
val msisdn: String? = null
) {
companion object {
fun from(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationParams {
return when (params.threePid) {
is RegisterThreePid.Email -> AddThreePidRegistrationParams(
email = params.threePid.email,
clientSecret = params.clientSecret,
sendAttempt = params.sendAttempt
)
is RegisterThreePid.Msisdn -> AddThreePidRegistrationParams(
msisdn = params.threePid.msisdn,
countryCode = params.threePid.countryCode,
clientSecret = params.clientSecret,
sendAttempt = params.sendAttempt
)
}
}
}
}

View file

@ -0,0 +1,54 @@
/*
* 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 com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddThreePidRegistrationResponse(
/**
* Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-].
* Their length must not exceed 255 characters and they must not be empty.
*/
@Json(name = "sid")
val sid: String,
/**
* An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity
* Service API's POST /validate/email/submitToken endpoint. The homeserver must send this token to the user (if applicable),
* who should then be prompted to provide it to the client.
*
* If this field is not present, the client can assume that verification will happen without the client's involvement provided
* the homeserver advertises this specification version in the /versions response (ie: r0.5.0).
*/
@Json(name = "submit_url")
val submitUrl: String? = null,
/* ==========================================================================================
* It seems that the homeserver is sending more data, we may need it
* ========================================================================================== */
@Json(name = "msisdn")
val msisdn: String? = null,
@Json(name = "intl_fmt")
val formattedMsisdn: String? = null,
@Json(name = "success")
val success: Boolean? = null
)

View file

@ -0,0 +1,102 @@
/*
* 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
/**
* Open class, parent to all possible authentication parameters
*/
@JsonClass(generateAdapter = true)
internal data class AuthParams(
@Json(name = "type")
val type: String,
/**
* Note: session can be null for reset password request
*/
@Json(name = "session")
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
)
}
/**
* Note that there is a bug in Synapse (I have to investigate where), but if we pass LoginFlowTypes.MSISDN,
* the homeserver answer with the login flow with MatrixError fields and not with a simple MatrixError 401.
*/
fun createForMsisdnIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams {
return AuthParams(
type = LoginFlowTypes.MSISDN,
session = session,
threePidCredentials = threePidCredentials
)
}
fun createForResetPassword(clientSecret: String, sid: String): AuthParams {
return AuthParams(
type = LoginFlowTypes.EMAIL_IDENTITY,
session = null,
threePidCredentials = ThreePidCredentials(
clientSecret = clientSecret,
sid = sid
)
)
}
}
}
@JsonClass(generateAdapter = true)
data class ThreePidCredentials(
@Json(name = "client_secret")
val clientSecret: String? = null,
@Json(name = "id_server")
val idServer: String? = null,
@Json(name = "sid")
val sid: String? = null
)

View file

@ -0,0 +1,246 @@
/*
* 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 dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
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.failure.Failure.RegistrationFlowError
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import okhttp3.OkHttpClient
/**
* This class execute the registration request and is responsible to keep the session of interactive authentication
*/
internal class DefaultRegistrationWizard(
private val okHttpClient: Lazy<OkHttpClient>,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
) : RegistrationWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
private val authAPI = buildAuthAPI()
private val registerTask = DefaultRegisterTask(authAPI)
private val registerAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI)
private val validateCodeTask = DefaultValidateCodeTask(authAPI)
override val currentThreePid: String?
get() {
return when (val threePid = pendingSessionData.currentThreePidData?.threePid) {
is RegisterThreePid.Email -> threePid.email
is RegisterThreePid.Msisdn -> {
// Take formatted msisdn if provided by the server
pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn
}
null -> null
}
}
override val isRegistrationStarted: Boolean
get() = pendingSessionData.isRegistrationStarted
override fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable {
val params = RegistrationParams()
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
}
}
override fun createAccount(userName: String,
password: String,
initialDeviceDisplayName: String?,
callback: MatrixCallback<RegistrationResult>): Cancelable {
val params = RegistrationParams(
username = userName,
password = password,
initialDeviceDisplayName = initialDeviceDisplayName
)
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
.also {
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
.also { pendingSessionStore.savePendingSessionData(it) }
}
}
}
override fun performReCaptcha(response: String, callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeSession = pendingSessionData.currentSession ?: run {
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
}
}
override fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeSession = pendingSessionData.currentSession ?: run {
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
}
}
override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
.also { pendingSessionStore.savePendingSessionData(it) }
sendThreePid(threePid)
}
}
override fun sendAgainThreePid(callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid ?: run {
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
sendThreePid(safeCurrentThreePid)
}
}
private suspend fun sendThreePid(threePid: RegisterThreePid): RegistrationResult {
val safeSession = pendingSessionData.currentSession ?: throw IllegalStateException("developer error, call createAccount() method first")
val response = registerAddThreePidTask.execute(
RegisterAddThreePidTask.Params(
threePid,
pendingSessionData.clientSecret,
pendingSessionData.sendAttempt))
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
.also { pendingSessionStore.savePendingSessionData(it) }
val params = RegistrationParams(
auth = if (threePid is RegisterThreePid.Email) {
AuthParams.createForEmailIdentity(safeSession,
ThreePidCredentials(
clientSecret = pendingSessionData.clientSecret,
sid = response.sid
)
)
} else {
AuthParams.createForMsisdnIdentity(safeSession,
ThreePidCredentials(
clientSecret = pendingSessionData.clientSecret,
sid = response.sid
)
)
}
)
// Store data
pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))
.also { pendingSessionStore.savePendingSessionData(it) }
// and send the sid a first time
return performRegistrationRequest(params)
}
override fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeParam = pendingSessionData.currentThreePidData?.registrationParams ?: run {
callback.onFailure(IllegalStateException("developer error, no pending three pid"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(safeParam, delayMillis)
}
}
override fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
validateThreePid(code)
}
}
private suspend fun validateThreePid(code: String): RegistrationResult {
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
?: throw IllegalStateException("developer error, no pending three pid")
val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code")
val validationBody = ValidationCodeBody(
clientSecret = pendingSessionData.clientSecret,
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
code = code
)
val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
if (validationResponse.success == true) {
// The entered code is correct
// Same than validate email
return performRegistrationRequest(registrationParams, 3_000)
} else {
// The code is not correct
throw Failure.SuccessError
}
}
override fun dummy(callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeSession = pendingSessionData.currentSession ?: run {
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
performRegistrationRequest(params)
}
}
private suspend fun performRegistrationRequest(registrationParams: RegistrationParams,
delayMillis: Long = 0): RegistrationResult {
delay(delayMillis)
val credentials = try {
registerTask.execute(RegisterTask.Params(registrationParams))
} catch (exception: Throwable) {
if (exception is RegistrationFlowError) {
pendingSessionData = pendingSessionData.copy(currentSession = exception.registrationFlowResponse.session)
.also { pendingSessionStore.savePendingSessionData(it) }
return RegistrationResult.FlowResponse(exception.registrationFlowResponse.toFlowResult())
} else {
throw exception
}
}
val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
return RegistrationResult.Success(session)
}
private fun buildAuthAPI(): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)
}
}

View file

@ -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

View file

@ -0,0 +1,47 @@
/*
* 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.registration.RegisterThreePid
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
internal interface RegisterAddThreePidTask : Task<RegisterAddThreePidTask.Params, AddThreePidRegistrationResponse> {
data class Params(
val threePid: RegisterThreePid,
val clientSecret: String,
val sendAttempt: Int
)
}
internal class DefaultRegisterAddThreePidTask(private val authAPI: AuthAPI)
: RegisterAddThreePidTask {
override suspend fun execute(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationResponse {
return executeRequest {
apiCall = authAPI.add3Pid(params.threePid.toPath(), AddThreePidRegistrationParams.from(params))
}
}
private fun RegisterThreePid.toPath(): String {
return when (this) {
is RegisterThreePid.Email -> "email"
is RegisterThreePid.Msisdn -> "msisdn"
}
}
}

View file

@ -0,0 +1,62 @@
/*
* 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
internal interface RegisterTask : Task<RegisterTask.Params, Credentials> {
data class Params(
val registrationParams: RegistrationParams
)
}
internal class DefaultRegisterTask(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
}
}
}
}

View file

@ -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(
@ -50,4 +54,46 @@ data class RegistrationFlowResponse(
*/
@Json(name = "params")
var params: JsonDict? = null
/**
* WARNING,
* The two MatrixError fields "errcode" and "error" can also be present here in case of error when validating a stage,
* But in this case Moshi will be able to parse the result as a MatrixError, see [RetrofitExtensions.toFailure]
* Ex: when polling for "m.login.msisdn" validation
*/
)
/**
* Convert to something easier to handle on client side
*/
fun RegistrationFlowResponse.toFlowResult(): FlowResult {
// Get all the returned stages
val allFlowTypes = mutableSetOf<String>()
val missingStage = mutableListOf<Stage>()
val completedStage = mutableListOf<Stage>()
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(isMandatory)
LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap<String, String>())
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)
}

View file

@ -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)
internal 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
)

View file

@ -0,0 +1,26 @@
/*
* 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 com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SuccessResult(
@Json(name = "success")
val success: Boolean?
)

View file

@ -0,0 +1,54 @@
/*
* 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 com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
/**
* Container to store the data when a three pid is in validation step
*/
@JsonClass(generateAdapter = true)
internal data class ThreePidData(
val email: String,
val msisdn: String,
val country: String,
val addThreePidRegistrationResponse: AddThreePidRegistrationResponse,
val registrationParams: RegistrationParams
) {
val threePid: RegisterThreePid
get() {
return if (email.isNotBlank()) {
RegisterThreePid.Email(email)
} else {
RegisterThreePid.Msisdn(msisdn, country)
}
}
companion object {
fun from(threePid: RegisterThreePid,
addThreePidRegistrationResponse: AddThreePidRegistrationResponse,
registrationParams: RegistrationParams): ThreePidData {
return when (threePid) {
is RegisterThreePid.Email ->
ThreePidData(threePid.email, "", "", addThreePidRegistrationResponse, registrationParams)
is RegisterThreePid.Msisdn ->
ThreePidData("", threePid.msisdn, threePid.countryCode, addThreePidRegistrationResponse, registrationParams)
}
}
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.internal.auth.AuthAPI
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
internal interface ValidateCodeTask : Task<ValidateCodeTask.Params, SuccessResult> {
data class Params(
val url: String,
val body: ValidationCodeBody
)
}
internal class DefaultValidateCodeTask(private val authAPI: AuthAPI)
: ValidateCodeTask {
override suspend fun execute(params: ValidateCodeTask.Params): SuccessResult {
return executeRequest {
apiCall = authAPI.validate3Pid(params.url, params.body)
}
}
}

View file

@ -0,0 +1,35 @@
/*
* 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 com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This object is used to send a code received by SMS to validate Msisdn ownership
*/
@JsonClass(generateAdapter = true)
data class ValidationCodeBody(
@Json(name = "client_secret")
val clientSecret: String,
@Json(name = "sid")
val sid: String,
@Json(name = "token")
val code: String
)

View file

@ -22,7 +22,7 @@ import com.squareup.moshi.Moshi
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.AuthenticationService
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.auth.SessionParamsStore
@ -44,7 +44,7 @@ internal interface MatrixComponent {
@Unauthenticated
fun okHttpClient(): OkHttpClient
fun authenticator(): Authenticator
fun authenticationService(): AuthenticationService
fun context(): Context

View file

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.network
internal object NetworkConstants {
private const val URI_API_PREFIX_PATH = "_matrix/client"
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"

View file

@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressServi
import im.vector.matrix.android.internal.session.filter.FilterRepository
import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabilitiesTask
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.session.user.UserStore
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
@ -41,7 +42,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
private val sessionParamsStore: SessionParamsStore,
private val initialSyncProgressService: DefaultInitialSyncProgressService,
private val syncTokenStore: SyncTokenStore,
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
private val userStore: UserStore
) : SyncTask {
override suspend fun execute(params: SyncTask.Params) {
@ -60,6 +62,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
val isInitialSync = token == null
if (isInitialSync) {
// We might want to get the user information in parallel too
userStore.createOrUpdate(userId)
initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
}

View file

@ -53,4 +53,7 @@ internal abstract class UserModule {
@Binds
abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask
@Binds
abstract fun bindUserStore(userStore: RealmUserStore): UserStore
}

View file

@ -0,0 +1,36 @@
/*
* 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.session.user
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.util.awaitTransaction
import javax.inject.Inject
internal interface UserStore {
suspend fun createOrUpdate(userId: String, displayName: String? = null, avatarUrl: String? = null)
}
internal class RealmUserStore @Inject constructor(private val monarchy: Monarchy) : UserStore {
override suspend fun createOrUpdate(userId: String, displayName: String?, avatarUrl: String?) {
monarchy.awaitTransaction {
val userEntity = UserEntity(userId, displayName ?: "", avatarUrl ?: "")
it.insertOrUpdate(userEntity)
}
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.task
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.toCancelable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
internal fun <T> CoroutineScope.launchToCallback(
context: CoroutineContext = EmptyCoroutineContext,
callback: MatrixCallback<T>,
block: suspend () -> T
): Cancelable = launch(context, CoroutineStart.DEFAULT) {
val result = runCatching {
block()
}
result.foldToCallback(callback)
}.toCancelable()

View file

@ -20,8 +20,8 @@ import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.MatrixScope
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.toCancelable
import kotlinx.coroutines.*
import timber.log.Timber
import javax.inject.Inject
@ -34,7 +34,8 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers
private val executorScope = CoroutineScope(SupervisorJob())
fun <PARAMS, RESULT> execute(task: ConfigurableTask<PARAMS, RESULT>): Cancelable {
val job = executorScope.launch(task.callbackThread.toDispatcher()) {
return executorScope
.launch(task.callbackThread.toDispatcher()) {
val resultOrFailure = runCatching {
withContext(task.executionThread.toDispatcher()) {
Timber.v("Enqueue task $task")
@ -54,7 +55,7 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers
}
.foldToCallback(task.callback)
}
return CancelableCoroutine(job)
.toCancelable()
}
fun cancelAll() = executorScope.coroutineContext.cancelChildren()

View file

@ -19,7 +19,14 @@ package im.vector.matrix.android.internal.util
import im.vector.matrix.android.api.util.Cancelable
import kotlinx.coroutines.Job
internal class CancelableCoroutine(private val job: Job) : Cancelable {
internal fun Job.toCancelable(): Cancelable {
return CancelableCoroutine(this)
}
/**
* Private, use the extension above
*/
private class CancelableCoroutine(private val job: Job) : Cancelable {
override fun cancel() {
if (!job.isCancelled) {

View file

@ -225,6 +225,7 @@ dependencies {
def glide_version = '4.10.0'
def moshi_version = '1.8.0'
def daggerVersion = '2.24'
def autofill_version = "1.0.0-rc01"
implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx")
@ -256,6 +257,9 @@ dependencies {
// Debug
implementation 'com.facebook.stetho:stetho:1.5.1'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
// rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
@ -290,6 +294,7 @@ dependencies {
implementation "io.noties.markwon:html:$markwon_version"
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version"
// Passphrase strength helper
implementation 'com.nulab-inc:zxcvbn:1.2.7'

View file

@ -33,7 +33,9 @@
</activity-alias>
<activity android:name=".features.home.HomeActivity" />
<activity android:name=".features.login.LoginActivity" />
<activity
android:name=".features.login.LoginActivity"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".features.media.ImageMediaViewerActivity" />
<activity
android:name=".features.rageshake.BugReportActivity"

View file

@ -0,0 +1 @@
javascript:window.matrixLogin.onLogin = function(response) { sendObjectMessage({ 'action': 'onLogin', 'credentials': response }); };

View file

@ -0,0 +1 @@
javascript:window.matrixRegistration.onRegistered = function(homeserverUrl, userId, accessToken) { sendObjectMessage({ 'action': 'onRegistered', 'homeServer': homeserverUrl, 'userId': userId, 'accessToken': accessToken }); }

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
var verifyCallback = function(response) {
var iframe = document.createElement('iframe');
iframe.setAttribute('src', 'js:' + JSON.stringify({'action': 'verifyCallback', 'response': response}));
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
};
var onloadCallback = function() {
grecaptcha.render('recaptcha_widget', { 'sitekey' : '%s', 'callback': verifyCallback });
};
</script>
</head>
<body>
<div id="recaptcha_widget"></div>
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
</body>
</html>

View file

@ -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;};

View file

@ -36,7 +36,7 @@ import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.glide.GlideImageLoader
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.DaggerVectorComponent
import im.vector.riotx.core.di.HasVectorInjector
@ -63,7 +63,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
lateinit var appContext: Context
// font thread handler
@Inject lateinit var authenticator: Authenticator
@Inject lateinit var authenticationService: AuthenticationService
@Inject lateinit var vectorConfiguration: VectorConfiguration
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
@ -115,8 +115,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
emojiCompatWrapper.init(fontRequest)
notificationUtils.createNotificationChannels()
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener, sessionListener)
}

View file

@ -17,7 +17,7 @@
package im.vector.riotx.core.di
import arrow.core.Option
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
@ -27,7 +27,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator,
class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService,
private val sessionObservableStore: ActiveSessionDataSource,
private val keyRequestHandler: KeyRequestHandler,
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
@ -64,7 +64,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
// TODO: Stop sync ?
// fun switchToSession(sessionParams: SessionParams) {
// val newActiveSession = authenticator.getSession(sessionParams)
// val newActiveSession = authenticationService.getSession(sessionParams)
// activeSession.set(newActiveSession)
// }
}

View file

@ -35,8 +35,8 @@ 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.LoginSsoFallbackFragment
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
@ -117,8 +117,63 @@ interface FragmentModule {
@Binds
@IntoMap
@FragmentKey(LoginSsoFallbackFragment::class)
fun bindLoginSsoFallbackFragment(fragment: LoginSsoFallbackFragment): Fragment
@FragmentKey(LoginCaptchaFragment::class)
fun bindLoginCaptchaFragment(fragment: LoginCaptchaFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginTermsFragment::class)
fun bindLoginTermsFragment(fragment: LoginTermsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginServerUrlFormFragment::class)
fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginResetPasswordMailConfirmationFragment::class)
fun bindLoginResetPasswordMailConfirmationFragment(fragment: LoginResetPasswordMailConfirmationFragment): 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)
fun bindLoginWebFragment(fragment: LoginWebFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginGenericTextInputFormFragment::class)
fun bindLoginGenericTextInputFormFragment(fragment: LoginGenericTextInputFormFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginWaitForEmailFragment::class)
fun bindLoginWaitForEmailFragment(fragment: LoginWaitForEmailFragment): Fragment
@Binds
@IntoMap

View file

@ -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

View file

@ -21,13 +21,14 @@ import android.content.res.Resources
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.AuthenticationService
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionDataSource
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
@ -69,6 +70,8 @@ interface VectorComponent {
fun resources(): Resources
fun assetReader(): AssetReader
fun dimensionConverter(): DimensionConverter
fun vectorConfiguration(): VectorConfiguration
@ -97,7 +100,7 @@ interface VectorComponent {
fun incomingKeyRequestHandler(): KeyRequestHandler
fun authenticator(): Authenticator
fun authenticationService(): AuthenticationService
fun bugReporter(): BugReporter

View file

@ -24,7 +24,7 @@ import dagger.Binds
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.AuthenticationService
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.features.navigation.DefaultNavigator
import im.vector.riotx.features.navigation.Navigator
@ -64,8 +64,8 @@ abstract class VectorModule {
@Provides
@JvmStatic
fun providesAuthenticator(matrix: Matrix): Authenticator {
return matrix.authenticator()
fun providesAuthenticationService(matrix: Matrix): AuthenticationService {
return matrix.authenticationService()
}
}

View file

@ -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
}

View file

@ -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) {
@ -34,23 +35,61 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
return when (throwable) {
null -> null
is Failure.NetworkConnection -> {
if (throwable.ioException is SocketTimeoutException) {
when {
throwable.ioException is SocketTimeoutException ->
stringProvider.getString(R.string.error_network_timeout)
} else {
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 -> {
if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) {
when {
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.code == MatrixError.FORBIDDEN
&& 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)
}
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 -> {
limitExceededError(throwable.error)
}
throwable.error.code == MatrixError.THREEPID_NOT_FOUND -> {
stringProvider.getString(R.string.login_reset_password_error_not_found)
}
else -> {
throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() }
}
}
}
else -> throwable.localizedMessage
}
?: stringProvider.getString(R.string.unknown_error)
}
private fun limitExceededError(error: MatrixError): String {
val delay = error.retryAfterMillis
return if (delay == null) {
stringProvider.getString(R.string.login_error_limit_exceeded)
} else {
// Ensure at least 1 second
val delaySeconds = delay.toInt() / 1000 + 1
stringProvider.getQuantityString(R.plurals.login_error_limit_exceeded_retry_after, delaySeconds, delaySeconds)
}
}
}

View file

@ -0,0 +1,26 @@
/*
* 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.error
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import javax.net.ssl.HttpsURLConnection
fun Throwable.is401(): Boolean {
return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& this.error.code == MatrixError.UNAUTHORIZED)
}

View file

@ -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 <T : Fragment> VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
fun <T : Fragment> VectorBaseActivity.addFragmentToBackstack(frameId: Int,
fragmentClass: Class<T>,
params: Parcelable? = null,
tag: String? = null,
option: ((FragmentTransaction) -> Unit)? = null) {
supportFragmentManager.commitTransaction {
option?.invoke(this)
replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag)
}
}

View file

@ -17,6 +17,7 @@
package im.vector.riotx.core.extensions
import android.os.Bundle
import android.util.Patterns
import androidx.fragment.app.Fragment
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
@ -27,3 +28,8 @@ inline fun <T> T.ooi(block: (T) -> Unit): T = also(block)
* Apply argument to a Fragment
*/
fun <T : Fragment> T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bundle().apply(block) }
/**
* Check if a CharSequence is an email
*/
fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()

View file

@ -79,3 +79,6 @@ fun <T : Fragment> VectorBaseFragment.addChildFragmentToBackstack(frameId: Int,
replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag)
}
}
// Define a missing constant
const val POP_BACK_STACK_EXCLUSIVE = 0

View file

@ -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
}

View file

@ -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<VectorBaseFragment>().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
}
}

View file

@ -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<String, String?>()
/**
* 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()
}
}

View file

@ -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<TextInputLayout> {
val res = ArrayList<TextInputLayout>()
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<TextInputLayout>) {
textInputLayouts.forEach {
it.editText?.addTextChangedListener(object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) {
// Reset the error
it.error = null
}
})
}
}

View file

@ -21,9 +21,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
@ -56,8 +54,6 @@ class MainActivity : VectorBaseActivity() {
}
}
@Inject lateinit var matrix: Matrix
@Inject lateinit var authenticator: Authenticator
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter

View file

@ -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
}

View file

@ -80,5 +80,5 @@ object ServerUrlsRepository {
/**
* Return default home server url from resources
*/
fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.default_hs_server_url)
fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.matrix_org_server_url)
}

View file

@ -0,0 +1,149 @@
/*
* 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.Build
import android.os.Bundle
import android.view.View
import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog
import androidx.transition.TransitionInflater
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
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.platform.OnBackPressed
import im.vector.riotx.core.platform.VectorBaseFragment
import javax.net.ssl.HttpsURLConnection
/**
* Parent Fragment for all the login/registration screens
*/
abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
protected val loginViewModel: LoginViewModel by activityViewModel()
protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
private var isResetPasswordStarted = false
// Due to async, we keep a boolean to avoid displaying twice the cancellation dialog
private var displayCancelDialog = true
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)
}
}
@CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java)
loginViewModel.viewEvents
.observe()
.subscribe {
handleLoginViewEvents(it)
}
.disposeOnDestroyView()
}
private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
when (loginViewEvents) {
is LoginViewEvents.Error -> showError(loginViewEvents.throwable)
else ->
// This is handled by the Activity
Unit
}
}
private fun showError(throwable: Throwable) {
when (throwable) {
is Failure.ServerError -> {
if (throwable.error.code == MatrixError.FORBIDDEN
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.login_registration_disabled))
.setPositiveButton(R.string.ok, null)
.show()
} else {
onError(throwable)
}
}
else -> onError(throwable)
}
}
abstract fun onError(throwable: Throwable)
override fun onBackPressed(toolbarButton: Boolean): Boolean {
return when {
displayCancelDialog && loginViewModel.isRegistrationStarted -> {
// 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.yes) { _, _ ->
displayCancelDialog = false
vectorBaseActivity.onBackPressed()
}
.setNegativeButton(R.string.no, null)
.show()
true
}
displayCancelDialog && isResetPasswordStarted -> {
// Ask for confirmation before cancelling the reset password
AlertDialog.Builder(requireActivity())
.setTitle(R.string.login_reset_password_cancel_confirmation_title)
.setMessage(R.string.login_reset_password_cancel_confirmation_content)
.setPositiveButton(R.string.yes) { _, _ ->
displayCancelDialog = false
vectorBaseActivity.onBackPressed()
}
.setNegativeButton(R.string.no, null)
.show()
true
}
else -> {
resetViewModel()
// Do not consume the Back event
false
}
}
}
final override fun invalidate() = withState(loginViewModel) { state ->
// True when email is sent with success to the homeserver
isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not()
updateWithState(state)
}
open fun updateWithState(state: LoginViewState) {
// No op by default
}
// Reset any modification on the loginViewModel by the current fragment
abstract fun resetViewModel()
}

View file

@ -0,0 +1,20 @@
/*
* 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
// TODO Check the link with Nad
const val MODULAR_LINK = "https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication"

View file

@ -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 im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import timber.log.Timber
import javax.inject.Inject
class HomeServerConnectionConfigFactory @Inject constructor() {
fun create(url: String?): HomeServerConnectionConfig? {
if (url == null) {
return null
}
return try {
HomeServerConnectionConfig.Builder()
.withHomeServerUri(url)
.build()
} catch (t: Throwable) {
Timber.e(t)
null
}
}
}

View file

@ -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
)

View file

@ -17,12 +17,42 @@
package im.vector.riotx.features.login
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
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 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 UpdateSignMode(val signMode: SignMode) : 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()
object ResetPasswordMailConfirmed : LoginAction()
// Login or Register, depending on the signMode
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : LoginAction()
// Register actions
open class RegisterAction : LoginAction()
data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction()
object SendAgainThreePid : RegisterAction()
// TODO Confirm Email (from link in the email, open in the phone, intercepted by RiotX)
data class ValidateThreePid(val code: String) : RegisterAction()
data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction()
object StopEmailValidationCheck : RegisterAction()
data class CaptchaDone(val captchaResponse: String) : RegisterAction()
object AcceptTerms : RegisterAction()
object RegisterDummy : RegisterAction()
// Reset actions
open class ResetAction : LoginAction()
object ResetHomeServerType : ResetAction()
object ResetHomeServerUrl : ResetAction()
object ResetSignMode : ResetAction()
object ResetLogin : ResetAction()
object ResetResetPassword : ResetAction()
}

View file

@ -18,28 +18,41 @@ package im.vector.riotx.features.login
import android.content.Context
import android.content.Intent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.Success
import androidx.fragment.app.FragmentTransaction
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.POP_BACK_STACK_EXCLUSIVE
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.ToolbarConfigurable
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
class LoginActivity : VectorBaseActivity() {
// Supported navigation actions for this Activity
sealed class Navigation {
object OpenSsoLoginFallback : Navigation()
object GoBack : Navigation()
}
/**
* The LoginActivity manages the fragment navigation and also display the loading View
*/
class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
private val loginViewModel: LoginViewModel by viewModel()
private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
@ -47,42 +60,290 @@ class LoginActivity : VectorBaseActivity() {
injector.inject(this)
}
override fun getLayoutRes() = R.layout.activity_simple
private val enterAnim = R.anim.enter_fade_in
private val exitAnim = R.anim.exit_fade_out
private val popEnterAnim = R.anim.no_anim
private val popExitAnim = R.anim.exit_fade_out
private val topFragment: Fragment?
get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer)
private val commonOption: (FragmentTransaction) -> Unit = { ft ->
// Find the loginLogo on the current Fragment, this should not return null
(topFragment?.view as? ViewGroup)
// Find findViewById does not work, I do not know why
// findViewById<View?>(R.id.loginLogo)
?.children
?.first { it.id == R.id.loginLogo }
?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}
override fun getLayoutRes() = R.layout.activity_login
override fun initUiAndData() {
if (isFirstCreation()) {
addFragment(R.id.simpleFragmentContainer, LoginFragment::class.java)
addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java)
}
// Get config extra
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
if (loginConfig != null && isFirstCreation()) {
// TODO Check this
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 {
handleLoginNavigation(it)
}
.disposeOnDestroy()
loginViewModel
.subscribe(this) {
updateWithState(it)
}
.disposeOnDestroy()
loginViewModel.viewEvents
.observe()
.subscribe {
handleLoginViewEvents(it)
}
.disposeOnDestroy()
}
private fun handleLoginNavigation(loginNavigation: LoginNavigation) {
// Assigning to dummy make sure we do not forget a case
@Suppress("UNUSED_VARIABLE")
val dummy = when (loginNavigation) {
is LoginNavigation.OpenServerSelection ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerSelectionFragment::class.java,
option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO Disabled because it provokes a flickering
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
})
is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone()
is LoginNavigation.OnSignModeSelected -> onSignModeSelected()
is LoginNavigation.OnLoginFlowRetrieved ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginSignUpSignInSelectionFragment::class.java,
option = commonOption)
is LoginNavigation.OnWebLoginError -> onWebLoginError(loginNavigation)
is LoginNavigation.OnForgetPasswordClicked ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginResetPasswordFragment::class.java,
option = commonOption)
is LoginNavigation.OnResetPasswordSendThreePidDone -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginResetPasswordMailConfirmationFragment::class.java,
option = commonOption)
}
is LoginNavigation.OnResetPasswordMailConfirmationSuccess -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginResetPasswordSuccessFragment::class.java,
option = commonOption)
}
is LoginNavigation.OnResetPasswordMailConfirmationSuccessDone -> {
// Go back to the login fragment
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
}
is LoginNavigation.OnSendEmailSuccess ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWaitForEmailFragment::class.java,
LoginWaitForEmailFragmentArgument(loginNavigation.email),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is LoginNavigation.OnSendMsisdnSuccess ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginNavigation.msisdn),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
}
}
loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) {
if (it is Success) {
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.isSupported() }) {
// Display a popup to propose use web fallback
onRegistrationStageNotSupported()
} else {
if (loginViewEvents.isRegistrationStarted) {
// Go on with registration flow
handleRegistrationNavigation(loginViewEvents.flowResult)
} else {
// First ask for login and password
// 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,
option = commonOption
)
}
}
}
is LoginViewEvents.OutdatedHomeserver ->
AlertDialog.Builder(this)
.setTitle(R.string.login_error_outdated_homeserver_title)
.setMessage(R.string.login_error_outdated_homeserver_content)
.setPositiveButton(R.string.ok, null)
.show()
is LoginViewEvents.Error ->
// This is handled by the Fragments
Unit
}
}
private fun updateWithState(loginViewState: LoginViewState) {
if (loginViewState.isUserLogged()) {
val intent = HomeActivity.newIntent(this)
startActivity(intent)
finish()
return
}
// Loading
loginLoading.isVisible = loginViewState.isLoading()
}
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, onWebLoginError.description, onWebLoginError.errorCode))
.setPositiveButton(R.string.ok, null)
.show()
}
private fun onServerSelectionDone() = withState(loginViewModel) { state ->
when (state.serverType) {
ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow
ServerType.Modular,
ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerUrlFormFragment::class.java,
option = commonOption)
}
}
private fun onSignModeSelected() = withState(loginViewModel) { state ->
when (state.signMode) {
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
SignMode.SignUp -> {
// This is managed by the LoginViewEvents
}
SignMode.SignIn -> {
// It depends on the LoginMode
when (state.loginMode) {
LoginMode.Unknown -> error("Developer error")
LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment::class.java,
tag = FRAGMENT_LOGIN_TAG,
option = commonOption)
LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWebFragment::class.java,
option = commonOption)
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
}
}
}
}
override fun onResume() {
super.onResume()
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,
option = commonOption)
}
.setNegativeButton(R.string.no, null)
.show()
}
showDisclaimerDialog(this)
private fun onLoginModeNotSupported(supportedTypes: List<String>) {
AlertDialog.Builder(this)
.setTitle(R.string.app_name)
.setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
.setPositiveButton(R.string.yes) { _, _ ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWebFragment::class.java,
option = commonOption)
}
.setNegativeButton(R.string.no, null)
.show()
}
private fun handleRegistrationNavigation(flowResult: FlowResult) {
// Complete all mandatory stages first
val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }
if (mandatoryStage != null) {
doStage(mandatoryStage)
} else {
// Consider optional stages
val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy }
if (optionalStage == null) {
// Should not happen...
} else {
doStage(optionalStage)
}
}
}
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),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginTermsFragment::class.java,
LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
else -> Unit // Should not happen
}
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
companion object {
private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {

View file

@ -0,0 +1,193 @@
/*
* 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.annotation.SuppressLint
import android.content.DialogInterface
import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Build
import android.os.Parcelable
import android.view.KeyEvent
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
import im.vector.riotx.core.error.ErrorFormatter
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
/**
* In this screen, the user is asked to confirm he is not a robot
*/
class LoginCaptchaFragment @Inject constructor(
private val assetReader: AssetReader,
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_captcha
private val params: LoginCaptchaFragmentArgument by args()
private var isWebViewLoaded = false
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView(state: LoginViewState) {
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 = state.homeServerUrl ?: error("missing url of homeserver")
loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null)
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)
// Hide loader
loginCaptchaProgress.isVisible = false
}
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 javascriptResponse: JavascriptResponse? = null
try {
// URL decode
json = URLDecoder.decode(json, "UTF-8")
javascriptResponse = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java).fromJson(json)
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading(): failed")
}
val response = javascriptResponse?.response
if (javascriptResponse?.action == "verifyCallback" && response != null) {
loginViewModel.handle(LoginAction.CaptchaDone(response))
}
}
return true
}
}
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}
override fun updateWithState(state: LoginViewState) {
if (!isWebViewLoaded) {
setupWebView(state)
isWebViewLoaded = true
}
}
}

View file

@ -16,34 +16,38 @@
package im.vector.riotx.features.login
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.autofill.HintConstants
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.airbnb.mvrx.*
import com.jakewharton.rxbinding3.view.focusChanges
import butterknife.OnClick
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.jakewharton.rxbinding3.widget.textChanges
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.extensions.setTextWithColoredPart
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard
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
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
/**
* 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, 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() : VectorBaseFragment() {
private val viewModel: LoginViewModel by activityViewModel()
class LoginFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
private var passwordShown = false
@ -52,69 +56,101 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupNotice()
setupAuthButton()
setupSubmitButton()
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()))
private fun setupAutoFill(state: LoginViewState) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> {
loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
}
SignMode.SignIn -> {
loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
}
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() {
val login = loginField.text?.trim().toString()
val password = passwordField.text?.trim().toString()
@OnClick(R.id.loginSubmit)
fun submit() {
cleanupUi()
viewModel.handle(LoginAction.Login(login, password))
val login = loginField.text.toString()
val password = passwordField.text.toString()
loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device)))
}
private fun setupAuthButton() {
private fun cleanupUi() {
loginSubmit.hideKeyboard()
loginFieldTil.error = null
passwordFieldTil.error = null
}
private fun setupUi(state: LoginViewState) {
val resId = when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> R.string.login_signup_to
SignMode.SignIn -> R.string.login_connect_to
}
when (state.serverType) {
ServerType.MatrixOrg -> {
loginServerIcon.isVisible = true
loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
loginTitle.text = getString(resId, state.homeServerUrlSimple)
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(resId, "TODO")
loginNotice.text = getString(R.string.login_server_modular_text)
}
ServerType.Other -> {
loginServerIcon.isVisible = false
loginTitle.text = getString(resId, state.homeServerUrlSimple)
loginNotice.text = getString(R.string.login_server_other_text)
}
}
}
private fun setupButtons(state: LoginViewState) {
forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn
loginSubmit.text = getString(when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> R.string.login_signup_submit
SignMode.SignIn -> R.string.login_signin
})
}
private fun setupSubmitButton() {
Observable
.combineLatest(
loginField.textChanges().map { it.trim().isNotEmpty() },
passwordField.textChanges().map { it.trim().isNotEmpty() },
homeServerField.textChanges().map { it.trim().isNotEmpty() },
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
BiFunction<Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty
}
)
.subscribeBy { authenticateButton.isEnabled = it }
.subscribeBy {
loginFieldTil.error = null
passwordFieldTil.error = null
loginSubmit.isEnabled = it
}
.disposeOnDestroyView()
authenticateButton.setOnClickListener { authenticate() }
authenticateButtonSso.setOnClickListener { openSso() }
}
private fun openSso() {
viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback))
@OnClick(R.id.forgetPasswordButton)
fun forgetPasswordClicked() {
loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked)
}
private fun setupPasswordReveal() {
@ -141,73 +177,47 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() {
}
}
override fun invalidate() = withState(viewModel) { state ->
TransitionManager.beginDelayedTransition(login_fragment)
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}
when (state.asyncHomeServerLoginFlowRequest) {
is Incomplete -> {
progressBar.isVisible = true
touchArea.isVisible = true
loginField.isVisible = false
passwordContainer.isVisible = false
authenticateButton.isVisible = false
authenticateButtonSso.isVisible = false
passwordShown = false
renderPasswordField()
override fun onError(throwable: Throwable) {
loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
}
is Fail -> {
progressBar.isVisible = false
touchArea.isVisible = false
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 -> {
progressBar.isVisible = false
touchArea.isVisible = false
when (state.asyncHomeServerLoginFlowRequest()) {
LoginMode.Password -> {
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()
}
}
LoginMode.Sso -> {
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()
}
}
}
}
override fun updateWithState(state: LoginViewState) {
setupUi(state)
setupAutoFill(state)
setupButtons(state)
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()
val error = state.asyncLoginAction.error
if (error is Failure.ServerError
&& error.error.code == MatrixError.FORBIDDEN
&& error.error.message.isEmpty()) {
// Login with email, but email unknown
loginFieldTil.error = getString(R.string.login_login_with_email_error)
} else {
// Trick to display the error without text.
loginFieldTil.error = " "
passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncLoginAction.error)
}
}
// Success is handled by the LoginActivity
is Success -> Unit
}
when (state.asyncRegistration) {
is Loading -> {
// Ensure password is hidden
passwordShown = false
renderPasswordField()
}
// Success is handled by the LoginActivity
is Success -> Unit

View file

@ -0,0 +1,252 @@
/*
* 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.Build
import android.os.Bundle
import android.os.Parcelable
import android.text.InputType
import android.view.View
import androidx.autofill.HintConstants
import androidx.core.view.isVisible
import butterknife.OnClick
import com.airbnb.mvrx.args
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.api.failure.Failure
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.error.is401
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.isEmail
import im.vector.riotx.core.extensions.setTextOrHide
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_login_generic_text_input_form.*
import javax.inject.Inject
enum class TextInputFormFragmentMode {
SetEmail,
SetMsisdn,
ConfirmMsisdn
}
@Parcelize
data class LoginGenericTextInputFormFragmentArgument(
val mode: TextInputFormFragmentMode,
val mandatory: Boolean,
val extra: String = ""
) : Parcelable
/**
* In this screen, the user is asked for a text input
*/
class LoginGenericTextInputFormFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : 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()
setupTil()
setupAutoFill()
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
loginGenericTextInputFormTextInput.setAutofillHints(
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS
TextInputFormFragmentMode.SetMsisdn -> HintConstants.AUTOFILL_HINT_PHONE_NUMBER
TextInputFormFragmentMode.ConfirmMsisdn -> HintConstants.AUTOFILL_HINT_SMS_OTP
}
)
}
}
private fun setupTil() {
loginGenericTextInputFormTextInput.textChanges()
.subscribe {
loginGenericTextInputFormTil.error = null
}
.disposeOnDestroyView()
}
private fun setupUi() {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> {
loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title)
loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice)
loginGenericTextInputFormNotice2.setTextOrHide(null)
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.SetMsisdn -> {
loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title)
loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice)
loginGenericTextInputFormNotice2.setTextOrHide(getString(R.string.login_set_msisdn_notice2))
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.ConfirmMsisdn -> {
loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title)
loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice, params.extra)
loginGenericTextInputFormNotice2.setTextOrHide(null)
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() {
when (params.mode) {
TextInputFormFragmentMode.ConfirmMsisdn -> {
loginViewModel.handle(LoginAction.SendAgainThreePid)
}
else -> {
// Should not happen, button is not displayed
}
}
}
@OnClick(R.id.loginGenericTextInputFormSubmit)
fun submit() {
cleanupUi()
val text = loginGenericTextInputFormTextInput.text.toString()
if (text.isEmpty()) {
// Perform dummy action
loginViewModel.handle(LoginAction.RegisterDummy)
} else {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> {
loginViewModel.handle(LoginAction.AddThreePid(RegisterThreePid.Email(text)))
}
TextInputFormFragmentMode.SetMsisdn -> {
getCountryCodeOrShowError(text)?.let { countryCode ->
loginViewModel.handle(LoginAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode)))
}
}
TextInputFormFragmentMode.ConfirmMsisdn -> {
loginViewModel.handle(LoginAction.ValidateThreePid(text))
}
}
}
}
private fun cleanupUi() {
loginGenericTextInputFormSubmit.hideKeyboard()
loginGenericTextInputFormSubmit.error = null
}
private fun getCountryCodeOrShowError(text: String): String? {
// We expect an international format for the moment (see https://github.com/vector-im/riotX-android/issues/693)
if (text.startsWith("+")) {
try {
val phoneNumber = PhoneNumberUtil.getInstance().parse(text, null)
return PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
} catch (e: NumberParseException) {
loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_other)
}
} else {
loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_not_international)
}
// Error
return null
}
private fun setupSubmitButton() {
loginGenericTextInputFormSubmit.isEnabled = false
loginGenericTextInputFormTextInput.textChanges()
.subscribe {
loginGenericTextInputFormSubmit.isEnabled = isInputValid(it)
}
.disposeOnDestroyView()
}
private fun isInputValid(input: CharSequence): Boolean {
return if (input.isEmpty() && !params.mandatory) {
true
} else {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> {
input.isEmail()
}
TextInputFormFragmentMode.SetMsisdn -> {
input.isNotBlank()
}
TextInputFormFragmentMode.ConfirmMsisdn -> {
input.isNotBlank()
}
}
}
}
override fun onError(throwable: Throwable) {
when (params.mode) {
TextInputFormFragmentMode.SetEmail -> {
if (throwable.is401()) {
// This is normal use case, we go to the mail waiting screen
loginSharedActionViewModel.post(LoginNavigation.OnSendEmailSuccess(loginViewModel.currentThreePid ?: ""))
} else {
loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
}
}
TextInputFormFragmentMode.SetMsisdn -> {
if (throwable.is401()) {
// This is normal use case, we go to the enter code screen
loginSharedActionViewModel.post(LoginNavigation.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: ""))
} else {
loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
}
}
TextInputFormFragmentMode.ConfirmMsisdn -> {
when {
throwable is Failure.SuccessError ->
// The entered code is not correct
loginGenericTextInputFormTil.error = getString(R.string.login_validation_code_is_not_correct)
throwable.is401() ->
// It can happen if user request again the 3pid
Unit
else ->
loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
}
}
}
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}
}

View file

@ -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 LoginMode {
Unknown,
Password,
Sso,
Unsupported
}

View file

@ -0,0 +1,36 @@
/*
* 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 LoginActivity
sealed class LoginNavigation : VectorSharedAction {
object OpenServerSelection : LoginNavigation()
object OnServerSelectionDone : LoginNavigation()
object OnLoginFlowRetrieved : LoginNavigation()
object OnSignModeSelected : LoginNavigation()
object OnForgetPasswordClicked : LoginNavigation()
object OnResetPasswordSendThreePidDone : LoginNavigation()
object OnResetPasswordMailConfirmationSuccess : LoginNavigation()
object OnResetPasswordMailConfirmationSuccessDone : LoginNavigation()
data class OnSendEmailSuccess(val email: String) : LoginNavigation()
data class OnSendMsisdnSuccess(val msisdn: String) : LoginNavigation()
data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation()
}

View file

@ -0,0 +1,166 @@
/*
* 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.appcompat.app.AlertDialog
import butterknife.OnClick
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.isEmail
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_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
// Show warning only once
private var showWarning = true
override fun getLayoutResId() = R.layout.fragment_login_reset_password
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
setupPasswordReveal()
}
private fun setupUi(state: LoginViewState) {
resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlSimple)
}
private fun setupSubmitButton() {
Observable
.combineLatest(
resetPasswordEmail.textChanges().map { it.isEmail() },
passwordField.textChanges().map { it.isNotEmpty() },
BiFunction<Boolean, Boolean, Boolean> { isEmail, isPasswordNotEmpty ->
isEmail && isPasswordNotEmpty
}
)
.subscribeBy {
resetPasswordEmailTil.error = null
passwordFieldTil.error = null
resetPasswordSubmit.isEnabled = it
}
.disposeOnDestroyView()
}
@OnClick(R.id.resetPasswordSubmit)
fun submit() {
cleanupUi()
if (showWarning) {
showWarning = false
// Display a warning as Riot-Web does first
AlertDialog.Builder(requireActivity())
.setTitle(R.string.login_reset_password_warning_title)
.setMessage(R.string.login_reset_password_warning_content)
.setPositiveButton(R.string.login_reset_password_warning_submit) { _, _ ->
doSubmit()
}
.setNegativeButton(R.string.cancel, null)
.show()
} else {
doSubmit()
}
}
private fun doSubmit() {
val email = resetPasswordEmail.text.toString()
val password = passwordField.text.toString()
loginViewModel.handle(LoginAction.ResetPassword(email, password))
}
private fun cleanupUi() {
resetPasswordSubmit.hideKeyboard()
resetPasswordEmailTil.error = null
passwordFieldTil.error = null
}
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 onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun updateWithState(state: LoginViewState) {
setupUi(state)
when (state.asyncResetPassword) {
is Loading -> {
// Ensure new password is hidden
passwordShown = false
renderPasswordField()
}
is Fail -> {
resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error)
}
is Success -> {
loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSendThreePidDone)
}
}
}
}

View file

@ -0,0 +1,82 @@
/*
* 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.appcompat.app.AlertDialog
import butterknife.OnClick
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.error.is401
import kotlinx.android.synthetic.main.fragment_login_reset_password_mail_confirmation.*
import javax.inject.Inject
/**
* In this screen, the user is asked to check his email and to click on a button once it's done
*/
class LoginResetPasswordMailConfirmationFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_reset_password_mail_confirmation
private fun setupUi(state: LoginViewState) {
resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetPasswordEmail)
}
@OnClick(R.id.resetPasswordMailConfirmationSubmit)
fun submit() {
loginViewModel.handle(LoginAction.ResetPasswordMailConfirmed)
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetResetPassword)
}
override fun updateWithState(state: LoginViewState) {
setupUi(state)
when (state.asyncResetMailConfirmed) {
is Fail -> {
// Link in email not yet clicked ?
val message = if (state.asyncResetMailConfirmed.error.is401()) {
getString(R.string.auth_reset_password_error_unauthorized)
} else {
errorFormatter.toHumanReadable(state.asyncResetMailConfirmed.error)
}
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
is Success -> {
loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccess)
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more