mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +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>
|
<words>
|
||||||
<w>backstack</w>
|
<w>backstack</w>
|
||||||
<w>bytearray</w>
|
<w>bytearray</w>
|
||||||
|
<w>checkables</w>
|
||||||
<w>ciphertext</w>
|
<w>ciphertext</w>
|
||||||
|
<w>coroutine</w>
|
||||||
<w>decryptor</w>
|
<w>decryptor</w>
|
||||||
<w>emoji</w>
|
<w>emoji</w>
|
||||||
<w>emojis</w>
|
<w>emojis</w>
|
||||||
@ -12,8 +14,11 @@
|
|||||||
<w>linkified</w>
|
<w>linkified</w>
|
||||||
<w>linkify</w>
|
<w>linkify</w>
|
||||||
<w>megolm</w>
|
<w>megolm</w>
|
||||||
|
<w>msisdn</w>
|
||||||
<w>pbkdf</w>
|
<w>pbkdf</w>
|
||||||
<w>pkcs</w>
|
<w>pkcs</w>
|
||||||
|
<w>signin</w>
|
||||||
|
<w>signup</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
</component>
|
</component>
|
@ -2,7 +2,8 @@ Changes in RiotX 0.9.0 (2019-XX-XX)
|
|||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
-
|
- Account creation. It's now possible to create account on any homeserver with RiotX (#34)
|
||||||
|
- Iteration of the login flow (#613)
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- Send mention Pills from composer
|
- 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 androidx.test.rule.GrantPermissionRule
|
||||||
import im.vector.matrix.android.InstrumentedTest
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
import im.vector.matrix.android.OkReplayRuleChainNoActivity
|
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 okreplay.*
|
||||||
import org.junit.ClassRule
|
import org.junit.ClassRule
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@ -29,9 +29,9 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
internal class AuthenticatorTest : InstrumentedTest {
|
internal class AuthenticationServiceTest : InstrumentedTest {
|
||||||
|
|
||||||
lateinit var authenticator: Authenticator
|
lateinit var authenticationService: AuthenticationService
|
||||||
lateinit var okReplayInterceptor: OkReplayInterceptor
|
lateinit var okReplayInterceptor: OkReplayInterceptor
|
||||||
|
|
||||||
private val okReplayConfig = OkReplayConfig.Builder()
|
private val okReplayConfig = OkReplayConfig.Builder()
|
@ -22,7 +22,7 @@ import androidx.work.Configuration
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.BuildConfig
|
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.SessionManager
|
||||||
import im.vector.matrix.android.internal.di.DaggerMatrixComponent
|
import im.vector.matrix.android.internal.di.DaggerMatrixComponent
|
||||||
import im.vector.matrix.android.internal.network.UserAgentHolder
|
import im.vector.matrix.android.internal.network.UserAgentHolder
|
||||||
@ -46,7 +46,7 @@ data class MatrixConfiguration(
|
|||||||
*/
|
*/
|
||||||
class Matrix private constructor(context: Context, matrixConfiguration: 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 userAgentHolder: UserAgentHolder
|
||||||
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
|
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
|
||||||
@Inject internal lateinit var olmManager: OlmManager
|
@Inject internal lateinit var olmManager: OlmManager
|
||||||
@ -64,8 +64,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
|||||||
|
|
||||||
fun getUserAgent() = userAgentHolder.userAgent
|
fun getUserAgent() = userAgentHolder.userAgent
|
||||||
|
|
||||||
fun authenticator(): Authenticator {
|
fun authenticationService(): AuthenticationService {
|
||||||
return authenticator
|
return authenticationService
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
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.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.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.session.Session
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
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
|
* Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first.
|
||||||
* @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]
|
|
||||||
*/
|
*/
|
||||||
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].
|
* Check if there is an authenticated [Session].
|
||||||
@ -67,5 +86,7 @@ interface Authenticator {
|
|||||||
/**
|
/**
|
||||||
* Create a session after a SSO successful login
|
* 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 = "home_server") val homeServer: String,
|
||||||
@Json(name = "access_token") val accessToken: String,
|
@Json(name = "access_token") val accessToken: String,
|
||||||
@Json(name = "refresh_token") val refreshToken: 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.
|
* 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.
|
* You should use the [Builder] to create one.
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@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 Cancelled(val throwable: Throwable? = null) : Failure(throwable)
|
||||||
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
|
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
|
||||||
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
|
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
|
// 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))
|
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,
|
@Json(name = "consent_uri") val consentUri: String? = null,
|
||||||
// RESOURCE_LIMIT_EXCEEDED data
|
// RESOURCE_LIMIT_EXCEEDED data
|
||||||
@Json(name = "limit_type") val limitType: String? = null,
|
@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 {
|
companion object {
|
||||||
const val FORBIDDEN = "M_FORBIDDEN"
|
const val FORBIDDEN = "M_FORBIDDEN"
|
||||||
|
@ -29,3 +29,5 @@ interface Cancelable {
|
|||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object NoOpCancellable : Cancelable
|
||||||
|
@ -17,20 +17,47 @@
|
|||||||
package im.vector.matrix.android.internal.auth
|
package im.vector.matrix.android.internal.auth
|
||||||
|
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
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.LoginFlowResponse
|
||||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
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 im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.*
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.Headers
|
|
||||||
import retrofit2.http.POST
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The login REST API.
|
* The login REST API.
|
||||||
*/
|
*/
|
||||||
internal interface AuthAPI {
|
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
|
* Get the supported login flow
|
||||||
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login
|
* 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")
|
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||||
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>
|
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.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
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.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.auth.db.RealmSessionParamsStore
|
||||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||||
import im.vector.matrix.android.internal.di.AuthDatabase
|
import im.vector.matrix.android.internal.di.AuthDatabase
|
||||||
@ -50,7 +52,8 @@ internal abstract class AuthModule {
|
|||||||
}
|
}
|
||||||
.name("matrix-sdk-auth.realm")
|
.name("matrix-sdk-auth.realm")
|
||||||
.modules(AuthRealmModule())
|
.modules(AuthRealmModule())
|
||||||
.deleteRealmIfMigrationNeeded()
|
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
|
||||||
|
.migration(AuthRealmMigration())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,5 +62,11 @@ internal abstract class AuthModule {
|
|||||||
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
|
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
|
||||||
|
|
||||||
@Binds
|
@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")
|
@Json(name = "stages")
|
||||||
val stages: List<String>? = null
|
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 MSISDN = "m.login.msisdn"
|
||||||
const val RECAPTCHA = "m.login.recaptcha"
|
const val RECAPTCHA = "m.login.recaptcha"
|
||||||
const val DUMMY = "m.login.dummy"
|
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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
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)
|
@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 = "password") val password: String,
|
||||||
@Json(name = "type") override val type: String,
|
@Json(name = "type") override val type: String,
|
||||||
@Json(name = "initial_device_display_name") val deviceDisplayName: String?,
|
@Json(name = "initial_device_display_name") val deviceDisplayName: String?,
|
||||||
@Json(name = "device_id") val deviceId: String?) : LoginParams {
|
@Json(name = "device_id") val deviceId: String?) : LoginParams {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val IDENTIFIER_KEY_TYPE = "type"
|
||||||
|
|
||||||
val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
|
private const val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
|
||||||
val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
|
private const val IDENTIFIER_KEY_USER = "user"
|
||||||
val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
|
|
||||||
|
|
||||||
val IDENTIFIER_KEY_TYPE = "type"
|
private const val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
|
||||||
val IDENTIFIER_KEY_MEDIUM = "medium"
|
private const val IDENTIFIER_KEY_MEDIUM = "medium"
|
||||||
val IDENTIFIER_KEY_ADDRESS = "address"
|
private const val IDENTIFIER_KEY_ADDRESS = "address"
|
||||||
val IDENTIFIER_KEY_USER = "user"
|
|
||||||
val IDENTIFIER_KEY_COUNTRY = "country"
|
private const val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
|
||||||
val IDENTIFIER_KEY_NUMBER = "number"
|
private const val IDENTIFIER_KEY_COUNTRY = "country"
|
||||||
|
private const val IDENTIFIER_KEY_PHONE = "phone"
|
||||||
|
|
||||||
fun userIdentifier(user: String,
|
fun userIdentifier(user: String,
|
||||||
password: String,
|
password: String,
|
||||||
deviceDisplayName: String? = null,
|
deviceDisplayName: String? = null,
|
||||||
deviceId: String? = null): PasswordLoginParams {
|
deviceId: String? = null): PasswordLoginParams {
|
||||||
val identifier = HashMap<String, String>()
|
return PasswordLoginParams(
|
||||||
identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_USER
|
mapOf(
|
||||||
identifier[IDENTIFIER_KEY_USER] = user
|
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER,
|
||||||
return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
|
IDENTIFIER_KEY_USER to user
|
||||||
|
),
|
||||||
|
password,
|
||||||
|
LoginFlowTypes.PASSWORD,
|
||||||
|
deviceDisplayName,
|
||||||
|
deviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun thirdPartyIdentifier(medium: String,
|
fun thirdPartyIdentifier(medium: String,
|
||||||
@ -54,11 +66,33 @@ internal data class PasswordLoginParams(@Json(name = "identifier") val identifie
|
|||||||
password: String,
|
password: String,
|
||||||
deviceDisplayName: String? = null,
|
deviceDisplayName: String? = null,
|
||||||
deviceId: String? = null): PasswordLoginParams {
|
deviceId: String? = null): PasswordLoginParams {
|
||||||
val identifier = HashMap<String, String>()
|
return PasswordLoginParams(
|
||||||
identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_THIRD_PARTY
|
mapOf(
|
||||||
identifier[IDENTIFIER_KEY_MEDIUM] = medium
|
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY,
|
||||||
identifier[IDENTIFIER_KEY_ADDRESS] = address
|
IDENTIFIER_KEY_MEDIUM to medium,
|
||||||
return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
|
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,
|
@RealmModule(library = true,
|
||||||
classes = [
|
classes = [
|
||||||
SessionParamsEntity::class
|
SessionParamsEntity::class,
|
||||||
|
PendingSessionEntity::class
|
||||||
])
|
])
|
||||||
internal class AuthRealmModule
|
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 {
|
) : SessionParamsStore {
|
||||||
|
|
||||||
override fun getLast(): SessionParams? {
|
override fun getLast(): SessionParams? {
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
val sessionParams = realm
|
realm
|
||||||
.where(SessionParamsEntity::class.java)
|
.where(SessionParamsEntity::class.java)
|
||||||
.findAll()
|
.findAll()
|
||||||
.map { mapper.map(it) }
|
.map { mapper.map(it) }
|
||||||
.lastOrNull()
|
.lastOrNull()
|
||||||
realm.close()
|
}
|
||||||
return sessionParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(userId: String): SessionParams? {
|
override fun get(userId: String): SessionParams? {
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
val sessionParams = realm
|
realm
|
||||||
.where(SessionParamsEntity::class.java)
|
.where(SessionParamsEntity::class.java)
|
||||||
.equalTo(SessionParamsEntityFields.USER_ID, userId)
|
.equalTo(SessionParamsEntityFields.USER_ID, userId)
|
||||||
.findAll()
|
.findAll()
|
||||||
.map { mapper.map(it) }
|
.map { mapper.map(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
realm.close()
|
}
|
||||||
return sessionParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAll(): List<SessionParams> {
|
override fun getAll(): List<SessionParams> {
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
val sessionParams = realm
|
realm
|
||||||
.where(SessionParamsEntity::class.java)
|
.where(SessionParamsEntity::class.java)
|
||||||
.findAll()
|
.findAll()
|
||||||
.mapNotNull { mapper.map(it) }
|
.mapNotNull { mapper.map(it) }
|
||||||
realm.close()
|
}
|
||||||
return sessionParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun save(sessionParams: 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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
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.api.util.JsonDict
|
||||||
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
|
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
|
||||||
|
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class RegistrationFlowResponse(
|
data class RegistrationFlowResponse(
|
||||||
@ -50,4 +54,46 @@ data class RegistrationFlowResponse(
|
|||||||
*/
|
*/
|
||||||
@Json(name = "params")
|
@Json(name = "params")
|
||||||
var params: JsonDict? = null
|
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.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import im.vector.matrix.android.api.Matrix
|
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.SessionManager
|
||||||
import im.vector.matrix.android.internal.auth.AuthModule
|
import im.vector.matrix.android.internal.auth.AuthModule
|
||||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||||
@ -44,7 +44,7 @@ internal interface MatrixComponent {
|
|||||||
@Unauthenticated
|
@Unauthenticated
|
||||||
fun okHttpClient(): OkHttpClient
|
fun okHttpClient(): OkHttpClient
|
||||||
|
|
||||||
fun authenticator(): Authenticator
|
fun authenticationService(): AuthenticationService
|
||||||
|
|
||||||
fun context(): Context
|
fun context(): Context
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.network
|
|||||||
internal object NetworkConstants {
|
internal object NetworkConstants {
|
||||||
|
|
||||||
private const val URI_API_PREFIX_PATH = "_matrix/client"
|
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_R0 = "$URI_API_PREFIX_PATH/r0/"
|
||||||
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
|
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.filter.FilterRepository
|
||||||
import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabilitiesTask
|
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.sync.model.SyncResponse
|
||||||
|
import im.vector.matrix.android.internal.session.user.UserStore
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -41,7 +42,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
|
|||||||
private val sessionParamsStore: SessionParamsStore,
|
private val sessionParamsStore: SessionParamsStore,
|
||||||
private val initialSyncProgressService: DefaultInitialSyncProgressService,
|
private val initialSyncProgressService: DefaultInitialSyncProgressService,
|
||||||
private val syncTokenStore: SyncTokenStore,
|
private val syncTokenStore: SyncTokenStore,
|
||||||
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask
|
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
|
||||||
|
private val userStore: UserStore
|
||||||
) : SyncTask {
|
) : SyncTask {
|
||||||
|
|
||||||
override suspend fun execute(params: SyncTask.Params) {
|
override suspend fun execute(params: SyncTask.Params) {
|
||||||
@ -60,6 +62,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
|
|||||||
|
|
||||||
val isInitialSync = token == null
|
val isInitialSync = token == null
|
||||||
if (isInitialSync) {
|
if (isInitialSync) {
|
||||||
|
// We might want to get the user information in parallel too
|
||||||
|
userStore.createOrUpdate(userId)
|
||||||
initialSyncProgressService.endAll()
|
initialSyncProgressService.endAll()
|
||||||
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
|
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
|
||||||
}
|
}
|
||||||
|
@ -53,4 +53,7 @@ internal abstract class UserModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask
|
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.di.MatrixScope
|
||||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
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.MatrixCoroutineDispatchers
|
||||||
|
import im.vector.matrix.android.internal.util.toCancelable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -34,7 +34,8 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers
|
|||||||
private val executorScope = CoroutineScope(SupervisorJob())
|
private val executorScope = CoroutineScope(SupervisorJob())
|
||||||
|
|
||||||
fun <PARAMS, RESULT> execute(task: ConfigurableTask<PARAMS, RESULT>): Cancelable {
|
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 {
|
val resultOrFailure = runCatching {
|
||||||
withContext(task.executionThread.toDispatcher()) {
|
withContext(task.executionThread.toDispatcher()) {
|
||||||
Timber.v("Enqueue task $task")
|
Timber.v("Enqueue task $task")
|
||||||
@ -54,7 +55,7 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers
|
|||||||
}
|
}
|
||||||
.foldToCallback(task.callback)
|
.foldToCallback(task.callback)
|
||||||
}
|
}
|
||||||
return CancelableCoroutine(job)
|
.toCancelable()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelAll() = executorScope.coroutineContext.cancelChildren()
|
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 im.vector.matrix.android.api.util.Cancelable
|
||||||
import kotlinx.coroutines.Job
|
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() {
|
override fun cancel() {
|
||||||
if (!job.isCancelled) {
|
if (!job.isCancelled) {
|
||||||
|
@ -225,6 +225,7 @@ dependencies {
|
|||||||
def glide_version = '4.10.0'
|
def glide_version = '4.10.0'
|
||||||
def moshi_version = '1.8.0'
|
def moshi_version = '1.8.0'
|
||||||
def daggerVersion = '2.24'
|
def daggerVersion = '2.24'
|
||||||
|
def autofill_version = "1.0.0-rc01"
|
||||||
|
|
||||||
implementation project(":matrix-sdk-android")
|
implementation project(":matrix-sdk-android")
|
||||||
implementation project(":matrix-sdk-android-rx")
|
implementation project(":matrix-sdk-android-rx")
|
||||||
@ -256,6 +257,9 @@ dependencies {
|
|||||||
// Debug
|
// Debug
|
||||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||||
|
|
||||||
|
// Phone number https://github.com/google/libphonenumber
|
||||||
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
||||||
|
|
||||||
// rx
|
// rx
|
||||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||||
@ -290,6 +294,7 @@ dependencies {
|
|||||||
implementation "io.noties.markwon:html:$markwon_version"
|
implementation "io.noties.markwon:html:$markwon_version"
|
||||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||||
implementation 'com.google.android:flexbox:1.1.1'
|
implementation 'com.google.android:flexbox:1.1.1'
|
||||||
|
implementation "androidx.autofill:autofill:$autofill_version"
|
||||||
|
|
||||||
// Passphrase strength helper
|
// Passphrase strength helper
|
||||||
implementation 'com.nulab-inc:zxcvbn:1.2.7'
|
implementation 'com.nulab-inc:zxcvbn:1.2.7'
|
||||||
|
@ -33,7 +33,9 @@
|
|||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity android:name=".features.home.HomeActivity" />
|
<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.media.ImageMediaViewerActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.rageshake.BugReportActivity"
|
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 com.github.piasy.biv.loader.glide.GlideImageLoader
|
||||||
import im.vector.matrix.android.api.Matrix
|
import im.vector.matrix.android.api.Matrix
|
||||||
import im.vector.matrix.android.api.MatrixConfiguration
|
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.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.di.DaggerVectorComponent
|
import im.vector.riotx.core.di.DaggerVectorComponent
|
||||||
import im.vector.riotx.core.di.HasVectorInjector
|
import im.vector.riotx.core.di.HasVectorInjector
|
||||||
@ -63,7 +63,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||||||
|
|
||||||
lateinit var appContext: Context
|
lateinit var appContext: Context
|
||||||
// font thread handler
|
// font thread handler
|
||||||
@Inject lateinit var authenticator: Authenticator
|
@Inject lateinit var authenticationService: AuthenticationService
|
||||||
@Inject lateinit var vectorConfiguration: VectorConfiguration
|
@Inject lateinit var vectorConfiguration: VectorConfiguration
|
||||||
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
|
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
|
||||||
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
|
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
|
||||||
@ -115,8 +115,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||||||
emojiCompatWrapper.init(fontRequest)
|
emojiCompatWrapper.init(fontRequest)
|
||||||
|
|
||||||
notificationUtils.createNotificationChannels()
|
notificationUtils.createNotificationChannels()
|
||||||
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
|
if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
|
||||||
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
|
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
|
||||||
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
||||||
lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener, sessionListener)
|
lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener, sessionListener)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
package im.vector.riotx.core.di
|
package im.vector.riotx.core.di
|
||||||
|
|
||||||
import arrow.core.Option
|
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.matrix.android.api.session.Session
|
||||||
import im.vector.riotx.ActiveSessionDataSource
|
import im.vector.riotx.ActiveSessionDataSource
|
||||||
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
||||||
@ -27,7 +27,7 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator,
|
class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService,
|
||||||
private val sessionObservableStore: ActiveSessionDataSource,
|
private val sessionObservableStore: ActiveSessionDataSource,
|
||||||
private val keyRequestHandler: KeyRequestHandler,
|
private val keyRequestHandler: KeyRequestHandler,
|
||||||
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
|
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
|
||||||
@ -64,7 +64,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
|
|||||||
|
|
||||||
// TODO: Stop sync ?
|
// TODO: Stop sync ?
|
||||||
// fun switchToSession(sessionParams: SessionParams) {
|
// fun switchToSession(sessionParams: SessionParams) {
|
||||||
// val newActiveSession = authenticator.getSession(sessionParams)
|
// val newActiveSession = authenticationService.getSession(sessionParams)
|
||||||
// activeSession.set(newActiveSession)
|
// 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.group.GroupListFragment
|
||||||
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
|
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
|
||||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||||
import im.vector.riotx.features.login.LoginFragment
|
import im.vector.riotx.features.login.*
|
||||||
import im.vector.riotx.features.login.LoginSsoFallbackFragment
|
import im.vector.riotx.features.login.terms.LoginTermsFragment
|
||||||
import im.vector.riotx.features.reactions.EmojiSearchResultFragment
|
import im.vector.riotx.features.reactions.EmojiSearchResultFragment
|
||||||
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
|
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
|
||||||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
|
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
|
||||||
@ -117,8 +117,63 @@ interface FragmentModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(LoginSsoFallbackFragment::class)
|
@FragmentKey(LoginCaptchaFragment::class)
|
||||||
fun bindLoginSsoFallbackFragment(fragment: LoginSsoFallbackFragment): Fragment
|
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
|
@Binds
|
||||||
@IntoMap
|
@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.edithistory.ViewEditHistoryBottomSheet
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
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.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.RoomListModule
|
||||||
|
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet
|
||||||
import im.vector.riotx.features.invite.VectorInviteView
|
import im.vector.riotx.features.invite.VectorInviteView
|
||||||
import im.vector.riotx.features.link.LinkHandlerActivity
|
import im.vector.riotx.features.link.LinkHandlerActivity
|
||||||
import im.vector.riotx.features.login.LoginActivity
|
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.reactions.widget.ReactionButton
|
||||||
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
|
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
|
||||||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
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.share.IncomingShareActivity
|
||||||
import im.vector.riotx.features.ui.UiStateRepository
|
import im.vector.riotx.features.ui.UiStateRepository
|
||||||
|
|
||||||
|
@ -21,13 +21,14 @@ import android.content.res.Resources
|
|||||||
import dagger.BindsInstance
|
import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import im.vector.matrix.android.api.Matrix
|
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.matrix.android.api.session.Session
|
||||||
import im.vector.riotx.ActiveSessionDataSource
|
import im.vector.riotx.ActiveSessionDataSource
|
||||||
import im.vector.riotx.EmojiCompatFontProvider
|
import im.vector.riotx.EmojiCompatFontProvider
|
||||||
import im.vector.riotx.EmojiCompatWrapper
|
import im.vector.riotx.EmojiCompatWrapper
|
||||||
import im.vector.riotx.VectorApplication
|
import im.vector.riotx.VectorApplication
|
||||||
import im.vector.riotx.core.pushers.PushersManager
|
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.core.utils.DimensionConverter
|
||||||
import im.vector.riotx.features.configuration.VectorConfiguration
|
import im.vector.riotx.features.configuration.VectorConfiguration
|
||||||
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
||||||
@ -69,6 +70,8 @@ interface VectorComponent {
|
|||||||
|
|
||||||
fun resources(): Resources
|
fun resources(): Resources
|
||||||
|
|
||||||
|
fun assetReader(): AssetReader
|
||||||
|
|
||||||
fun dimensionConverter(): DimensionConverter
|
fun dimensionConverter(): DimensionConverter
|
||||||
|
|
||||||
fun vectorConfiguration(): VectorConfiguration
|
fun vectorConfiguration(): VectorConfiguration
|
||||||
@ -97,7 +100,7 @@ interface VectorComponent {
|
|||||||
|
|
||||||
fun incomingKeyRequestHandler(): KeyRequestHandler
|
fun incomingKeyRequestHandler(): KeyRequestHandler
|
||||||
|
|
||||||
fun authenticator(): Authenticator
|
fun authenticationService(): AuthenticationService
|
||||||
|
|
||||||
fun bugReporter(): BugReporter
|
fun bugReporter(): BugReporter
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ import dagger.Binds
|
|||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import im.vector.matrix.android.api.Matrix
|
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.matrix.android.api.session.Session
|
||||||
import im.vector.riotx.features.navigation.DefaultNavigator
|
import im.vector.riotx.features.navigation.DefaultNavigator
|
||||||
import im.vector.riotx.features.navigation.Navigator
|
import im.vector.riotx.features.navigation.Navigator
|
||||||
@ -64,8 +64,8 @@ abstract class VectorModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun providesAuthenticator(matrix: Matrix): Authenticator {
|
fun providesAuthenticationService(matrix: Matrix): AuthenticationService {
|
||||||
return matrix.authenticator()
|
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.createdirect.CreateDirectRoomSharedActionViewModel
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel
|
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.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
|
||||||
|
import im.vector.riotx.features.login.LoginSharedActionViewModel
|
||||||
import im.vector.riotx.features.reactions.EmojiChooserViewModel
|
import im.vector.riotx.features.reactions.EmojiChooserViewModel
|
||||||
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
|
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
|
||||||
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
||||||
@ -112,4 +113,9 @@ interface ViewModelModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(RoomDirectorySharedActionViewModel::class)
|
@ViewModelKey(RoomDirectorySharedActionViewModel::class)
|
||||||
fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel
|
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.R
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
import java.net.UnknownHostException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) {
|
class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) {
|
||||||
@ -34,23 +35,61 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
|
|||||||
return when (throwable) {
|
return when (throwable) {
|
||||||
null -> null
|
null -> null
|
||||||
is Failure.NetworkConnection -> {
|
is Failure.NetworkConnection -> {
|
||||||
if (throwable.ioException is SocketTimeoutException) {
|
when {
|
||||||
|
throwable.ioException is SocketTimeoutException ->
|
||||||
stringProvider.getString(R.string.error_network_timeout)
|
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)
|
stringProvider.getString(R.string.error_no_network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Failure.ServerError -> {
|
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
|
// Special case for terms and conditions
|
||||||
stringProvider.getString(R.string.error_terms_not_accepted)
|
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.message.takeIf { it.isNotEmpty() }
|
||||||
?: throwable.error.code.takeIf { it.isNotEmpty() }
|
?: throwable.error.code.takeIf { it.isNotEmpty() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else -> throwable.localizedMessage
|
else -> throwable.localizedMessage
|
||||||
}
|
}
|
||||||
?: stringProvider.getString(R.string.unknown_error)
|
?: 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 android.os.Parcelable
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
|
|
||||||
fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) {
|
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) }
|
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 {
|
supportFragmentManager.commitTransaction {
|
||||||
|
option?.invoke(this)
|
||||||
replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag)
|
replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotx.core.extensions
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Patterns
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
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
|
* Apply argument to a Fragment
|
||||||
*/
|
*/
|
||||||
fun <T : Fragment> T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bundle().apply(block) }
|
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)
|
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.
|
* Returns true, if the on back pressed event has been handled by this Fragment.
|
||||||
* Otherwise return false
|
* 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 {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
if (item.itemId == android.R.id.home) {
|
if (item.itemId == android.R.id.home) {
|
||||||
onBackPressed()
|
onBackPressed(true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,20 +286,24 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
|
onBackPressed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onBackPressed(fromToolbar: Boolean) {
|
||||||
|
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager, fromToolbar)
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
|
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager, fromToolbar: Boolean): Boolean {
|
||||||
val reverseOrder = fm.fragments.filter { it is VectorBaseFragment }.reversed()
|
val reverseOrder = fm.fragments.filterIsInstance<VectorBaseFragment>().reversed()
|
||||||
for (f in reverseOrder) {
|
for (f in reverseOrder) {
|
||||||
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
|
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager, fromToolbar)
|
||||||
if (handledByChildFragments) {
|
if (handledByChildFragments) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (f is OnBackPressed && f.onBackPressed()) {
|
if (f is OnBackPressed && f.onBackPressed(fromToolbar)) {
|
||||||
return true
|
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 android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.bumptech.glide.Glide
|
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.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.Authenticator
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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 sessionHolder: ActiveSessionHolder
|
||||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ class RoomListFragment @Inject constructor(
|
|||||||
stateView.state = StateView.State.Error(message)
|
stateView.state = StateView.State.Error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean {
|
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||||
if (createChatFabMenu.onBackPressed()) {
|
if (createChatFabMenu.onBackPressed()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -80,5 +80,5 @@ object ServerUrlsRepository {
|
|||||||
/**
|
/**
|
||||||
* Return default home server url from resources
|
* 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
|
package im.vector.riotx.features.login
|
||||||
|
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
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
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class LoginAction : VectorViewModelAction {
|
sealed class LoginAction : VectorViewModelAction {
|
||||||
|
data class UpdateServerType(val serverType: ServerType) : LoginAction()
|
||||||
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
|
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
|
||||||
data class Login(val login: String, val password: String) : LoginAction()
|
data class UpdateSignMode(val signMode: SignMode) : LoginAction()
|
||||||
data class SsoLoginSuccess(val credentials: Credentials) : LoginAction()
|
data class WebLoginSuccess(val credentials: Credentials) : LoginAction()
|
||||||
data class NavigateTo(val target: LoginActivity.Navigation) : LoginAction()
|
|
||||||
data class InitWith(val loginConfig: LoginConfig) : 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.Context
|
||||||
import android.content.Intent
|
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 androidx.fragment.app.FragmentManager
|
||||||
import com.airbnb.mvrx.Success
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import com.airbnb.mvrx.viewModel
|
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.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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.addFragment
|
||||||
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
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.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
|
||||||
import im.vector.riotx.features.home.HomeActivity
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LoginActivity : VectorBaseActivity() {
|
/**
|
||||||
|
* The LoginActivity manages the fragment navigation and also display the loading View
|
||||||
// Supported navigation actions for this Activity
|
*/
|
||||||
sealed class Navigation {
|
class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
object OpenSsoLoginFallback : Navigation()
|
|
||||||
object GoBack : Navigation()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val loginViewModel: LoginViewModel by viewModel()
|
private val loginViewModel: LoginViewModel by viewModel()
|
||||||
|
private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
|
||||||
|
|
||||||
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
|
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
|
||||||
|
|
||||||
@ -47,42 +60,290 @@ class LoginActivity : VectorBaseActivity() {
|
|||||||
injector.inject(this)
|
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() {
|
override fun initUiAndData() {
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
addFragment(R.id.simpleFragmentContainer, LoginFragment::class.java)
|
addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get config extra
|
// Get config extra
|
||||||
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
|
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
|
||||||
if (loginConfig != null && isFirstCreation()) {
|
if (loginConfig != null && isFirstCreation()) {
|
||||||
|
// TODO Check this
|
||||||
loginViewModel.handle(LoginAction.InitWith(loginConfig))
|
loginViewModel.handle(LoginAction.InitWith(loginConfig))
|
||||||
}
|
}
|
||||||
|
|
||||||
loginViewModel.navigationLiveData.observeEvent(this) {
|
loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java)
|
||||||
when (it) {
|
loginSharedActionViewModel.observe()
|
||||||
is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java)
|
.subscribe {
|
||||||
is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
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) {
|
private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
|
||||||
if (it is Success) {
|
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)
|
val intent = HomeActivity.newIntent(this)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finish()
|
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() {
|
private fun onRegistrationStageNotSupported() {
|
||||||
super.onResume()
|
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 {
|
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"
|
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
|
||||||
|
|
||||||
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
|
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
|
package im.vector.riotx.features.login
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import androidx.autofill.HintConstants
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.transition.TransitionManager
|
import butterknife.OnClick
|
||||||
import com.airbnb.mvrx.*
|
import com.airbnb.mvrx.Fail
|
||||||
import com.jakewharton.rxbinding3.view.focusChanges
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
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.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.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.Observable
|
||||||
import io.reactivex.functions.Function3
|
import io.reactivex.functions.BiFunction
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
import kotlinx.android.synthetic.main.fragment_login.*
|
import kotlinx.android.synthetic.main.fragment_login.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What can be improved:
|
* In this screen, in signin mode:
|
||||||
* - 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
|
* - 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() {
|
class LoginFragment @Inject constructor(
|
||||||
|
private val errorFormatter: ErrorFormatter
|
||||||
private val viewModel: LoginViewModel by activityViewModel()
|
) : AbstractLoginFragment() {
|
||||||
|
|
||||||
private var passwordShown = false
|
private var passwordShown = false
|
||||||
|
|
||||||
@ -52,69 +56,101 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
setupNotice()
|
setupSubmitButton()
|
||||||
setupAuthButton()
|
|
||||||
setupPasswordReveal()
|
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()
|
private fun setupAutoFill(state: LoginViewState) {
|
||||||
if (initHsUrl != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
homeServerField.setText(initHsUrl)
|
when (state.signMode) {
|
||||||
} else {
|
SignMode.Unknown -> error("developer error")
|
||||||
homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext()))
|
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() {
|
@OnClick(R.id.loginSubmit)
|
||||||
val login = loginField.text?.trim().toString()
|
fun submit() {
|
||||||
val password = passwordField.text?.trim().toString()
|
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
|
Observable
|
||||||
.combineLatest(
|
.combineLatest(
|
||||||
loginField.textChanges().map { it.trim().isNotEmpty() },
|
loginField.textChanges().map { it.trim().isNotEmpty() },
|
||||||
passwordField.textChanges().map { it.trim().isNotEmpty() },
|
passwordField.textChanges().map { it.trim().isNotEmpty() },
|
||||||
homeServerField.textChanges().map { it.trim().isNotEmpty() },
|
BiFunction<Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty ->
|
||||||
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
|
isLoginNotEmpty && isPasswordNotEmpty
|
||||||
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.subscribeBy { authenticateButton.isEnabled = it }
|
.subscribeBy {
|
||||||
|
loginFieldTil.error = null
|
||||||
|
passwordFieldTil.error = null
|
||||||
|
loginSubmit.isEnabled = it
|
||||||
|
}
|
||||||
.disposeOnDestroyView()
|
.disposeOnDestroyView()
|
||||||
authenticateButton.setOnClickListener { authenticate() }
|
|
||||||
|
|
||||||
authenticateButtonSso.setOnClickListener { openSso() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openSso() {
|
@OnClick(R.id.forgetPasswordButton)
|
||||||
viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback))
|
fun forgetPasswordClicked() {
|
||||||
|
loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPasswordReveal() {
|
private fun setupPasswordReveal() {
|
||||||
@ -141,73 +177,47 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun resetViewModel() {
|
||||||
TransitionManager.beginDelayedTransition(login_fragment)
|
loginViewModel.handle(LoginAction.ResetLogin)
|
||||||
|
}
|
||||||
|
|
||||||
when (state.asyncHomeServerLoginFlowRequest) {
|
override fun onError(throwable: Throwable) {
|
||||||
is Incomplete -> {
|
loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||||
progressBar.isVisible = true
|
|
||||||
touchArea.isVisible = true
|
|
||||||
loginField.isVisible = false
|
|
||||||
passwordContainer.isVisible = false
|
|
||||||
authenticateButton.isVisible = false
|
|
||||||
authenticateButtonSso.isVisible = false
|
|
||||||
passwordShown = false
|
|
||||||
renderPasswordField()
|
|
||||||
}
|
}
|
||||||
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()) {
|
override fun updateWithState(state: LoginViewState) {
|
||||||
LoginMode.Password -> {
|
setupUi(state)
|
||||||
loginField.isVisible = true
|
setupAutoFill(state)
|
||||||
passwordContainer.isVisible = true
|
setupButtons(state)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (state.asyncLoginAction) {
|
when (state.asyncLoginAction) {
|
||||||
is Loading -> {
|
is Loading -> {
|
||||||
progressBar.isVisible = true
|
// Ensure password is hidden
|
||||||
touchArea.isVisible = true
|
|
||||||
|
|
||||||
passwordShown = false
|
passwordShown = false
|
||||||
renderPasswordField()
|
renderPasswordField()
|
||||||
}
|
}
|
||||||
is Fail -> {
|
is Fail -> {
|
||||||
progressBar.isVisible = false
|
val error = state.asyncLoginAction.error
|
||||||
touchArea.isVisible = false
|
if (error is Failure.ServerError
|
||||||
Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show()
|
&& 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
|
// Success is handled by the LoginActivity
|
||||||
is Success -> Unit
|
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