mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Merge pull request #689 from vector-im/feature/signin_signup
Login and Registration
This commit is contained in:
commit
3f4f7457c7
@ -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>
|
@ -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
260
docs/signin.md
Normal 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
579
docs/signup.md
Normal 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"
|
||||
]
|
||||
}
|
||||
```
|
@ -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()
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -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>
|
||||
)
|
@ -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
|
||||
}
|
@ -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<*, *>
|
@ -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))
|
||||
|
||||
|
@ -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"
|
||||
|
@ -29,3 +29,5 @@ interface Cancelable {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
object NoOpCancellable : Cancelable
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import io.realm.annotations.RealmModule
|
||||
*/
|
||||
@RealmModule(library = true,
|
||||
classes = [
|
||||
SessionParamsEntity::class
|
||||
SessionParamsEntity::class,
|
||||
PendingSessionEntity::class
|
||||
])
|
||||
internal class AuthRealmModule
|
||||
|
@ -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
|
||||
)
|
@ -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()
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
@ -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?
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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
|
||||
|
||||
|
@ -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/"
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -53,4 +53,7 @@ internal abstract class UserModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUserStore(userStore: RealmUserStore): UserStore
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
|
@ -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"
|
||||
|
1
vector/src/main/assets/onLogin.js
Normal file
1
vector/src/main/assets/onLogin.js
Normal file
@ -0,0 +1 @@
|
||||
javascript:window.matrixLogin.onLogin = function(response) { sendObjectMessage({ 'action': 'onLogin', 'credentials': response }); };
|
1
vector/src/main/assets/onRegistered.js
Normal file
1
vector/src/main/assets/onRegistered.js
Normal file
@ -0,0 +1 @@
|
||||
javascript:window.matrixRegistration.onRegistered = function(homeserverUrl, userId, accessToken) { sendObjectMessage({ 'action': 'onRegistered', 'homeServer': homeserverUrl, 'userId': userId, 'accessToken': accessToken }); }
|
22
vector/src/main/assets/reCaptchaPage.html
Normal file
22
vector/src/main/assets/reCaptchaPage.html
Normal 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>
|
1
vector/src/main/assets/sendObject.js
Normal file
1
vector/src/main/assets/sendObject.js
Normal 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;};
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
// }
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
55
vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt
Normal file
55
vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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"
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user