Merge pull request #689 from vector-im/feature/signin_signup
Login and Registration
This commit is contained in:
		
						commit
						3f4f7457c7
					
				
							
								
								
									
										5
									
								
								.idea/dictionaries/bmarty.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								.idea/dictionaries/bmarty.xml
									
									
									
										generated
									
									
									
								
							| @ -3,7 +3,9 @@ | ||||
|     <words> | ||||
|       <w>backstack</w> | ||||
|       <w>bytearray</w> | ||||
|       <w>checkables</w> | ||||
|       <w>ciphertext</w> | ||||
|       <w>coroutine</w> | ||||
|       <w>decryptor</w> | ||||
|       <w>emoji</w> | ||||
|       <w>emojis</w> | ||||
| @ -12,8 +14,11 @@ | ||||
|       <w>linkified</w> | ||||
|       <w>linkify</w> | ||||
|       <w>megolm</w> | ||||
|       <w>msisdn</w> | ||||
|       <w>pbkdf</w> | ||||
|       <w>pkcs</w> | ||||
|       <w>signin</w> | ||||
|       <w>signup</w> | ||||
|     </words> | ||||
|   </dictionary> | ||||
| </component> | ||||
| @ -2,7 +2,8 @@ Changes in RiotX 0.9.0 (2019-XX-XX) | ||||
| =================================================== | ||||
| 
 | ||||
| Features ✨: | ||||
|  - | ||||
|  - Account creation. It's now possible to create account on any homeserver with RiotX (#34) | ||||
|  - Iteration of the login flow (#613) | ||||
| 
 | ||||
| Improvements 🙌: | ||||
|  - Send mention Pills from composer | ||||
|  | ||||
							
								
								
									
										260
									
								
								docs/signin.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								docs/signin.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,260 @@ | ||||
| # Sign in to a homeserver | ||||
| 
 | ||||
| This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver. | ||||
| 
 | ||||
| ## Sign up flows | ||||
| 
 | ||||
| ### Get the flow | ||||
| 
 | ||||
| Client request the sign-in flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`) | ||||
| 
 | ||||
| > curl -X GET 'https://matrix.org/_matrix/client/r0/login' | ||||
| 
 | ||||
| 200 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "flows": [ | ||||
|     { | ||||
|       "type": "m.login.password" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Login with username | ||||
| 
 | ||||
| The user is able to connect using `m.login.password` | ||||
| 
 | ||||
| > curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "identifier": { | ||||
|     "type": "m.id.user", | ||||
|     "user": "alice" | ||||
|   }, | ||||
|   "password": "weak_password", | ||||
|   "type": "m.login.password", | ||||
|   "initial_device_display_name": "Portable" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### Incorrect password | ||||
| 
 | ||||
| 403 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "errcode": "M_FORBIDDEN", | ||||
|   "error": "Invalid password" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### Correct password: | ||||
| 
 | ||||
| We get credential (200) | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "user_id": "@benoit0816:matrix.org", | ||||
|   "access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg", | ||||
|   "home_server": "matrix.org", | ||||
|   "device_id": "GTVREDALBF", | ||||
|   "well_known": { | ||||
|     "m.homeserver": { | ||||
|       "base_url": "https:\/\/matrix.org\/" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Login with email | ||||
| 
 | ||||
| If the user has associated an email with its account, he can signin using the email. | ||||
| 
 | ||||
| > curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@yopmail.com"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "identifier": { | ||||
|     "type": "m.id.thirdparty", | ||||
|     "medium": "email", | ||||
|     "address": "alice@yopmail.com" | ||||
|   }, | ||||
|   "password": "weak_password", | ||||
|   "type": "m.login.password", | ||||
|   "initial_device_display_name": "Portable" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### Unknown email | ||||
| 
 | ||||
| 403 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "errcode": "M_FORBIDDEN", | ||||
|   "error": "" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### Known email, wrong password | ||||
| 
 | ||||
| 403 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "errcode": "M_FORBIDDEN", | ||||
|   "error": "Invalid password" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ##### Known email, correct password | ||||
| 
 | ||||
| We get the credentials (200) | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "user_id": "@alice:matrix.org", | ||||
|   "access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg", | ||||
|   "home_server": "matrix.org", | ||||
|   "device_id": "WBSREDASND", | ||||
|   "well_known": { | ||||
|     "m.homeserver": { | ||||
|       "base_url": "https:\/\/matrix.org\/" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Login with Msisdn | ||||
| 
 | ||||
| Not supported yet in RiotX | ||||
| 
 | ||||
| ### Login with SSO | ||||
| 
 | ||||
| > curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login' | ||||
| 
 | ||||
| 200 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "flows": [ | ||||
|     { | ||||
|       "type": "m.login.sso" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| In this case, the user can click on "Sign in with SSO" and the web screen will be displayed on the page `https://homeserver.with.sso/_matrix/static/client/login/` and the credentials will be passed back to the native code through the JS bridge | ||||
| 
 | ||||
| ## Reset password | ||||
| 
 | ||||
| Ref: `https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-password-email-requesttoken` | ||||
| 
 | ||||
| When the user has forgotten his password, he can reset it by providing an email and a new password. | ||||
| 
 | ||||
| Here is the flow: | ||||
| 
 | ||||
| ### Send email | ||||
| 
 | ||||
| User is asked to enter the email linked to his account and a new password. | ||||
| We display a warning regarding e2e. | ||||
| 
 | ||||
| At the first step, we do not send the password, only the email and a client secret, generated by the application | ||||
| 
 | ||||
| > curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "client_secret": "6c57f284-85e2-421b-8270-fb1795a120a7", | ||||
|   "send_attempt": 0, | ||||
|   "email": "user@domain.com" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### When the email is not known | ||||
| 
 | ||||
| We get a 400 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "errcode": "M_THREEPID_NOT_FOUND", | ||||
|   "error": "Email not found" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### When the email is known | ||||
| 
 | ||||
| We get a 200 with a `sid` | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "sid": "tQNbrREDACTEDldA" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Then the user is asked to click on the link in the email he just received, and to confirm when it's done. | ||||
| 
 | ||||
| During this step, the new password is sent to the homeserver. | ||||
| 
 | ||||
| If the user confirms before the link is clicked, we get an error: | ||||
| 
 | ||||
| > curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "auth": { | ||||
|     "type": "m.login.email.identity", | ||||
|     "threepid_creds": { | ||||
|       "client_secret": "6c57f284-85e2-421b-8270-fb1795a120a7", | ||||
|       "sid": "tQNbrREDACTEDldA" | ||||
|     } | ||||
|   }, | ||||
|   "new_password": "weak_password" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 401 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "errcode": "M_UNAUTHORIZED", | ||||
|   "error": "" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### User clicks on the link | ||||
| 
 | ||||
| The link has the form: | ||||
| 
 | ||||
| https://matrix.org/_matrix/client/unstable/password_reset/email/submit_token?token=fzZLBlcqhTKeaFQFSRbsQnQCkzbwtGAD&client_secret=6c57f284-85e2-421b-8270-fb1795a120a7&sid=tQNbrREDACTEDldA | ||||
| 
 | ||||
| It contains the client secret, a token and the sid | ||||
| 
 | ||||
| When the user click the link, if validate his ownership and the new password can now be ent by the application (on user demand): | ||||
| 
 | ||||
| > curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "auth": { | ||||
|     "type": "m.login.email.identity", | ||||
|     "threepid_creds": { | ||||
|       "client_secret": "6c57f284-85e2-421b-8270-fb1795a120a7", | ||||
|       "sid": "tQNbrREDACTEDldA" | ||||
|     } | ||||
|   }, | ||||
|   "new_password": "weak_password" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 200 | ||||
| 
 | ||||
| ```json | ||||
| {} | ||||
| ``` | ||||
| 
 | ||||
| The password has been changed, and all the existing token are invalidated. User can now login with the new password. | ||||
							
								
								
									
										579
									
								
								docs/signup.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										579
									
								
								docs/signup.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,579 @@ | ||||
| # Sign up to a homeserver | ||||
| 
 | ||||
| This document describes the flow of registration to a homeserver. Examples come from the `matrix.org` homeserver. | ||||
| 
 | ||||
| *Ref*: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management | ||||
| 
 | ||||
| ## Sign up flows | ||||
| 
 | ||||
| ### First step | ||||
| 
 | ||||
| Client request the sign-up flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`) | ||||
| 
 | ||||
| > curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| We get the flows with a 401, which also means the the registration is possible on this homeserver. | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "session": "vwehdKMtkRedactedAMwgCACZ", | ||||
|   "flows": [ | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.dummy" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.email.identity" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "params": { | ||||
|     "m.login.recaptcha": { | ||||
|       "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" | ||||
|     }, | ||||
|     "m.login.terms": { | ||||
|       "policies": { | ||||
|         "privacy_policy": { | ||||
|           "version": "1.0", | ||||
|           "en": { | ||||
|             "name": "Terms and Conditions", | ||||
|             "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| If the registration is not possible, we get a 403 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "errcode": "M_FORBIDDEN", | ||||
|   "error": "Registration is disabled" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Step 1: entering user name and password | ||||
| 
 | ||||
| The app is displaying a form to enter username and password. | ||||
| 
 | ||||
| > curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "initial_device_display_name": "Mobile device", | ||||
|   "username": "alice", | ||||
|   "password": "weak_password" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 401. Note that the `session` value has changed (because we did not provide the previous value in the request body), but it's ok, we will use the new value for the next steps. | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|   "flows": [ | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.dummy" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.email.identity" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "params": { | ||||
|     "m.login.recaptcha": { | ||||
|       "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" | ||||
|     }, | ||||
|     "m.login.terms": { | ||||
|       "policies": { | ||||
|         "privacy_policy": { | ||||
|           "version": "1.0", | ||||
|           "en": { | ||||
|             "name": "Terms and Conditions", | ||||
|             "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| #### If username already exists | ||||
| 
 | ||||
| We get a 400: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "errcode": "M_USER_IN_USE", | ||||
|   "error": "User ID already taken." | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Step 2: entering email | ||||
| 
 | ||||
| User is proposed to enter an email. We skip this step. | ||||
| 
 | ||||
| > curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "auth": { | ||||
|     "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|     "type": "m.login.dummy" | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 401 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|   "flows": [ | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.dummy" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.email.identity" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "params": { | ||||
|     "m.login.recaptcha": { | ||||
|       "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" | ||||
|     }, | ||||
|     "m.login.terms": { | ||||
|       "policies": { | ||||
|         "privacy_policy": { | ||||
|           "version": "1.0", | ||||
|           "en": { | ||||
|             "name": "Terms and Conditions", | ||||
|             "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "completed": [ | ||||
|     "m.login.dummy" | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Step 2 bis: we enter an email | ||||
| 
 | ||||
| We request a token to the homeserver. The `client_secret` is generated by the application | ||||
| 
 | ||||
| > curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@yopmail.com","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", | ||||
|   "email": "alice@yopmail.com", | ||||
|   "send_attempt": 0 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 200 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "sid": "qlBCREDACTEDEtgxD" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| And | ||||
| 
 | ||||
| > curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "auth": { | ||||
|     "threepid_creds": { | ||||
|       "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", | ||||
|       "sid": "qlBCREDACTEDEtgxD" | ||||
|     }, | ||||
|     "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|     "type": "m.login.email.identity" | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| We get 401 since the email is not validated yet: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "errcode": "M_UNAUTHORIZED", | ||||
|   "error": "" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| The app is now polling on  | ||||
| 
 | ||||
| > curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "auth": { | ||||
|     "threepid_creds": { | ||||
|       "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", | ||||
|       "sid": "qlBCREDACTEDEtgxD" | ||||
|     }, | ||||
|     "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|     "type": "m.login.email.identity" | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| We click on the link received by email `https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD` which contains: | ||||
| - A `token` vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ | ||||
| - The `client_secret`: 53e679ea-oRED-ACTED-92b8-3012c49c6cfa | ||||
| - A `sid`: qlBCREDACTEDEtgxD | ||||
| 
 | ||||
| Once the link is clicked, the registration request (polling) returns a 401 with the following content: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|   "flows": [ | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.dummy" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.email.identity" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "params": { | ||||
|     "m.login.recaptcha": { | ||||
|       "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" | ||||
|     }, | ||||
|     "m.login.terms": { | ||||
|       "policies": { | ||||
|         "privacy_policy": { | ||||
|           "version": "1.0", | ||||
|           "en": { | ||||
|             "name": "Terms and Conditions", | ||||
|             "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "completed": [ | ||||
|     "m.login.email.identity" | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Step 3: Accepting T&C | ||||
| 
 | ||||
| User is proposed to accept T&C and he accepts them | ||||
| 
 | ||||
| > curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"}}' 'https://matrix.org/_matrix/client/r0/register' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "auth": { | ||||
|     "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|     "type": "m.login.terms" | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 401 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|   "flows": [ | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.dummy" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.email.identity" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "params": { | ||||
|     "m.login.recaptcha": { | ||||
|       "public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb" | ||||
|     }, | ||||
|     "m.login.terms": { | ||||
|       "policies": { | ||||
|         "privacy_policy": { | ||||
|           "version": "1.0", | ||||
|           "en": { | ||||
|             "name": "Terms and Conditions", | ||||
|             "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "completed": [ | ||||
|     "m.login.dummy", | ||||
|     "m.login.terms" | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Step 4: Captcha | ||||
| 
 | ||||
| User is proposed to prove he is not a robot and he does it: | ||||
| 
 | ||||
| > curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' 'https://matrix.org/_matrix/client/r0/register' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "auth": { | ||||
|     "response": "03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q", | ||||
|     "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|     "type": "m.login.recaptcha" | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 200 | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "user_id": "@alice:matrix.org", | ||||
|   "home_server": "matrix.org", | ||||
|   "access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmcKMoREDACTEDo50aWZpZXIga2V5CjAwMTBjaWQgZ2VuID0gMQowMDI5Y2lkIHVzZXJfaWQgPSBAYmVub2l0eHh4eDptYXRoREDACTEDoCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNHVSVm00aVFDaWlKdoREDACTEDoJmc2lnbmF0dXJlIOmHnTLRfxiPjhrWhS-dThUX-qAzZktfRThzH1YyAsxaCg", | ||||
|   "device_id": "FLBAREDAJZ" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| The account is created! | ||||
| 
 | ||||
| ### Step 5: MSISDN | ||||
| 
 | ||||
| Some homeservers may require the user to enter MSISDN. | ||||
| 
 | ||||
| On matrix.org, it's not required, and not even optional, but it's still possible for the app to add a MSISDN during the registration. | ||||
| 
 | ||||
| The user enter a phone number and select a country, the `client_secret` is generated by the application | ||||
| 
 | ||||
| > curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}'  'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7", | ||||
|   "send_attempt": 1, | ||||
|   "country": "FR", | ||||
|   "phone_number": "+33611223344" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| If the msisdn is already associated to another account, you will received an error: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "errcode": "M_THREEPID_IN_USE", | ||||
|   "error": "Phone number is already in use" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| If it is not the case, the homeserver send the SMS and returns some data, especially a `sid` and a `submit_url`: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "msisdn": "33611223344", | ||||
|   "intl_fmt": "+336 11 22 33 44", | ||||
|   "success": true, | ||||
|   "sid": "1678881798", | ||||
|   "submit_url": "https:\/\/matrix.org\/_matrix\/client\/unstable\/add_threepid\/msisdn\/submit_token" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| When you execute the register request, with the received `sid`, you get an error since the MSISDN is not validated yet: | ||||
| 
 | ||||
| > curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register' | ||||
| 
 | ||||
| 
 | ||||
| ```json | ||||
|   "auth": { | ||||
|     "type": "m.login.msisdn", | ||||
|     "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|     "threepid_creds": { | ||||
|       "client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7", | ||||
|       "sid": "1678881798" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| There is an issue on Synapse, which return a 401, it sends too much data along with the classical MatrixError fields: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|   "flows": [ | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.dummy" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.email.identity" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "params": { | ||||
|     "m.login.recaptcha": { | ||||
|       "public_key": "6LcgI54UAAAAABGdGmruw6DdOocFpYVdjYBRe4zb" | ||||
|     }, | ||||
|     "m.login.terms": { | ||||
|       "policies": { | ||||
|         "privacy_policy": { | ||||
|           "version": "1.0", | ||||
|           "en": { | ||||
|             "name": "Terms and Conditions", | ||||
|             "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "completed": [], | ||||
|   "error": "", | ||||
|   "errcode": "M_UNAUTHORIZED" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| The user receive the SMS, he can enter the SMS code in the app, which is sent using the "submit_url" received ie the response of the `requestToken` request: | ||||
| 
 | ||||
| > curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7", | ||||
|   "sid": "1678881798", | ||||
|   "token": "123456" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| If the code is not correct, we get a 200 with: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "success": false | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| And if the code is correct we get a 200 with: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "success": true | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| We can now execute the registration request, to the homeserver | ||||
| 
 | ||||
| > curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register' | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "auth": { | ||||
|     "type": "m.login.msisdn", | ||||
|     "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|     "threepid_creds": { | ||||
|       "client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7", | ||||
|       "sid": "1678881798" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Now the homeserver consider that the `m.login.msisdn` step is completed (401): | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "session": "xptUYoREDACTEDogOWAGVnbJQ", | ||||
|   "flows": [ | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.dummy" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "stages": [ | ||||
|         "m.login.recaptcha", | ||||
|         "m.login.terms", | ||||
|         "m.login.email.identity" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "params": { | ||||
|     "m.login.recaptcha": { | ||||
|       "public_key": "6LcgI54UAAAAABGdGmruw6DdOocFpYVdjYBRe4zb" | ||||
|     }, | ||||
|     "m.login.terms": { | ||||
|       "policies": { | ||||
|         "privacy_policy": { | ||||
|           "version": "1.0", | ||||
|           "en": { | ||||
|             "name": "Terms and Conditions", | ||||
|             "url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "completed": [ | ||||
|     "m.login.msisdn" | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
| @ -21,7 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 | ||||
| import androidx.test.rule.GrantPermissionRule | ||||
| import im.vector.matrix.android.InstrumentedTest | ||||
| import im.vector.matrix.android.OkReplayRuleChainNoActivity | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.matrix.android.api.auth.AuthenticationService | ||||
| import okreplay.* | ||||
| import org.junit.ClassRule | ||||
| import org.junit.Rule | ||||
| @ -29,9 +29,9 @@ import org.junit.Test | ||||
| import org.junit.runner.RunWith | ||||
| 
 | ||||
| @RunWith(AndroidJUnit4::class) | ||||
| internal class AuthenticatorTest : InstrumentedTest { | ||||
| internal class AuthenticationServiceTest : InstrumentedTest { | ||||
| 
 | ||||
|     lateinit var authenticator: Authenticator | ||||
|     lateinit var authenticationService: AuthenticationService | ||||
|     lateinit var okReplayInterceptor: OkReplayInterceptor | ||||
| 
 | ||||
|     private val okReplayConfig = OkReplayConfig.Builder() | ||||
| @ -22,7 +22,7 @@ import androidx.work.Configuration | ||||
| import androidx.work.WorkManager | ||||
| import com.zhuinden.monarchy.Monarchy | ||||
| import im.vector.matrix.android.BuildConfig | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.matrix.android.api.auth.AuthenticationService | ||||
| import im.vector.matrix.android.internal.SessionManager | ||||
| import im.vector.matrix.android.internal.di.DaggerMatrixComponent | ||||
| import im.vector.matrix.android.internal.network.UserAgentHolder | ||||
| @ -46,7 +46,7 @@ data class MatrixConfiguration( | ||||
|  */ | ||||
| class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) { | ||||
| 
 | ||||
|     @Inject internal lateinit var authenticator: Authenticator | ||||
|     @Inject internal lateinit var authenticationService: AuthenticationService | ||||
|     @Inject internal lateinit var userAgentHolder: UserAgentHolder | ||||
|     @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver | ||||
|     @Inject internal lateinit var olmManager: OlmManager | ||||
| @ -64,8 +64,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo | ||||
| 
 | ||||
|     fun getUserAgent() = userAgentHolder.userAgent | ||||
| 
 | ||||
|     fun authenticator(): Authenticator { | ||||
|         return authenticator | ||||
|     fun authenticationService(): AuthenticationService { | ||||
|         return authenticationService | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|  | ||||
| @ -19,29 +19,48 @@ package im.vector.matrix.android.api.auth | ||||
| import im.vector.matrix.android.api.MatrixCallback | ||||
| import im.vector.matrix.android.api.auth.data.Credentials | ||||
| import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig | ||||
| import im.vector.matrix.android.api.auth.data.LoginFlowResult | ||||
| import im.vector.matrix.android.api.auth.data.SessionParams | ||||
| import im.vector.matrix.android.api.auth.login.LoginWizard | ||||
| import im.vector.matrix.android.api.auth.registration.RegistrationWizard | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| import im.vector.matrix.android.api.util.Cancelable | ||||
| import im.vector.matrix.android.internal.auth.data.LoginFlowResponse | ||||
| 
 | ||||
| /** | ||||
|  * This interface defines methods to authenticate to a matrix server. | ||||
|  * This interface defines methods to authenticate or to create an account to a matrix server. | ||||
|  */ | ||||
| interface Authenticator { | ||||
| interface AuthenticationService { | ||||
| 
 | ||||
|     /** | ||||
|      * Request the supported login flows for this homeserver | ||||
|      * Request the supported login flows for this homeserver. | ||||
|      * This is the first method to call to be able to get a wizard to login or the create an account | ||||
|      */ | ||||
|     fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable | ||||
|     fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable | ||||
| 
 | ||||
|     /** | ||||
|      * @param homeServerConnectionConfig this param is used to configure the Homeserver | ||||
|      * @param login the login field | ||||
|      * @param password the password field | ||||
|      * @param callback  the matrix callback on which you'll receive the result of authentication. | ||||
|      * @return return a [Cancelable] | ||||
|      * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. | ||||
|      */ | ||||
|     fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String, callback: MatrixCallback<Session>): Cancelable | ||||
|     fun getLoginWizard(): LoginWizard | ||||
| 
 | ||||
|     /** | ||||
|      * Return a RegistrationWizard, to create an matrix account on the homeserver. The login flow has to be retrieved first. | ||||
|      */ | ||||
|     fun getRegistrationWizard(): RegistrationWizard | ||||
| 
 | ||||
|     /** | ||||
|      * True when login and password has been sent with success to the homeserver | ||||
|      */ | ||||
|     val isRegistrationStarted: Boolean | ||||
| 
 | ||||
|     /** | ||||
|      * Cancel pending login or pending registration | ||||
|      */ | ||||
|     fun cancelPendingLoginOrRegistration() | ||||
| 
 | ||||
|     /** | ||||
|      * Reset all pending settings, including current HomeServerConnectionConfig | ||||
|      */ | ||||
|     fun reset() | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there is an authenticated [Session]. | ||||
| @ -67,5 +86,7 @@ interface Authenticator { | ||||
|     /** | ||||
|      * Create a session after a SSO successful login | ||||
|      */ | ||||
|     fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<Session>): Cancelable | ||||
|     fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, | ||||
|                              credentials: Credentials, | ||||
|                              callback: MatrixCallback<Session>): Cancelable | ||||
| } | ||||
| @ -30,4 +30,7 @@ data class Credentials( | ||||
|         @Json(name = "home_server") val homeServer: String, | ||||
|         @Json(name = "access_token") val accessToken: String, | ||||
|         @Json(name = "refresh_token") val refreshToken: String?, | ||||
|         @Json(name = "device_id") val deviceId: String?) | ||||
|         @Json(name = "device_id") val deviceId: String?, | ||||
|         // Optional data that may contain info to override home server and/or identity server | ||||
|         @Json(name = "well_known") val wellKnown: WellKnown? = null | ||||
| ) | ||||
|  | ||||
| @ -25,7 +25,7 @@ import okhttp3.TlsVersion | ||||
| 
 | ||||
| /** | ||||
|  * This data class holds how to connect to a specific Homeserver. | ||||
|  * It's used with [im.vector.matrix.android.api.auth.Authenticator] class. | ||||
|  * It's used with [im.vector.matrix.android.api.auth.AuthenticationService] class. | ||||
|  * You should use the [Builder] to create one. | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
|  | ||||
| @ -0,0 +1,29 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.api.auth.data | ||||
| 
 | ||||
| import im.vector.matrix.android.internal.auth.data.LoginFlowResponse | ||||
| 
 | ||||
| // Either a LoginFlowResponse, or an error if the homeserver is outdated | ||||
| sealed class LoginFlowResult { | ||||
|     data class Success( | ||||
|             val loginFlowResponse: LoginFlowResponse, | ||||
|             val isLoginAndRegistrationSupported: Boolean | ||||
|     ) : LoginFlowResult() | ||||
| 
 | ||||
|     object OutdatedHomeserver : LoginFlowResult() | ||||
| } | ||||
| @ -0,0 +1,111 @@ | ||||
| /* | ||||
|  * Copyright 2018 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.api.auth.data | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| 
 | ||||
| /** | ||||
|  * Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions | ||||
|  * | ||||
|  * Ex: | ||||
|  * <pre> | ||||
|  *   { | ||||
|  *     "unstable_features": { | ||||
|  *       "m.lazy_load_members": true | ||||
|  *     }, | ||||
|  *     "versions": [ | ||||
|  *       "r0.0.1", | ||||
|  *       "r0.1.0", | ||||
|  *       "r0.2.0", | ||||
|  *       "r0.3.0" | ||||
|  *     ] | ||||
|  *   } | ||||
|  * </pre> | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| data class Versions( | ||||
|         @Json(name = "versions") | ||||
|         val supportedVersions: List<String>? = null, | ||||
| 
 | ||||
|         @Json(name = "unstable_features") | ||||
|         val unstableFeatures: Map<String, Boolean>? = null | ||||
| ) | ||||
| 
 | ||||
| // MatrixClientServerAPIVersion | ||||
| private const val r0_0_1 = "r0.0.1" | ||||
| private const val r0_1_0 = "r0.1.0" | ||||
| private const val r0_2_0 = "r0.2.0" | ||||
| private const val r0_3_0 = "r0.3.0" | ||||
| private const val r0_4_0 = "r0.4.0" | ||||
| private const val r0_5_0 = "r0.5.0" | ||||
| private const val r0_6_0 = "r0.6.0" | ||||
| 
 | ||||
| // MatrixVersionsFeature | ||||
| private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members" | ||||
| private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server" | ||||
| private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token" | ||||
| private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind" | ||||
| 
 | ||||
| /** | ||||
|  * Return true if the SDK supports this homeserver version | ||||
|  */ | ||||
| fun Versions.isSupportedBySdk(): Boolean { | ||||
|     return supportLazyLoadMembers() | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Return true if the SDK supports this homeserver version for login and registration | ||||
|  */ | ||||
| fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { | ||||
|     return !doesServerRequireIdentityServerParam() | ||||
|             && doesServerAcceptIdentityAccessToken() | ||||
|             && doesServerSeparatesAddAndBind() | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Return true if the server support the lazy loading of room members | ||||
|  * | ||||
|  * @return true if the server support the lazy loading of room members | ||||
|  */ | ||||
| private fun Versions.supportLazyLoadMembers(): Boolean { | ||||
|     return supportedVersions?.contains(r0_5_0) == true | ||||
|             || unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Indicate if the `id_server` parameter is required when registering with an 3pid, | ||||
|  * adding a 3pid or resetting password. | ||||
|  */ | ||||
| private fun Versions.doesServerRequireIdentityServerParam(): Boolean { | ||||
|     if (supportedVersions?.contains(r0_6_0) == true) return false | ||||
|     return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Indicate if the `id_access_token` parameter can be safely passed to the homeserver. | ||||
|  * Some homeservers may trigger errors if they are not prepared for the new parameter. | ||||
|  */ | ||||
| private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean { | ||||
|     return supportedVersions?.contains(r0_6_0) == true | ||||
|             || unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false | ||||
| } | ||||
| 
 | ||||
| private fun Versions.doesServerSeparatesAddAndBind(): Boolean { | ||||
|     return supportedVersions?.contains(r0_6_0) == true | ||||
|             || unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false | ||||
| } | ||||
| @ -0,0 +1,82 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.api.auth.data | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| 
 | ||||
| /** | ||||
|  * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery | ||||
|  * <pre> | ||||
|  * { | ||||
|  *     "m.homeserver": { | ||||
|  *         "base_url": "https://matrix.org" | ||||
|  *     }, | ||||
|  *     "m.identity_server": { | ||||
|  *         "base_url": "https://vector.im" | ||||
|  *     } | ||||
|  *     "m.integrations": { | ||||
|  *          "managers": [ | ||||
|  *              { | ||||
|  *                  "api_url": "https://integrations.example.org", | ||||
|  *                  "ui_url": "https://integrations.example.org/ui" | ||||
|  *              }, | ||||
|  *              { | ||||
|  *                  "api_url": "https://bots.example.org" | ||||
|  *              } | ||||
|  *          ] | ||||
|  *    } | ||||
|  * } | ||||
|  * </pre> | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| data class WellKnown( | ||||
|         @Json(name = "m.homeserver") | ||||
|         var homeServer: WellKnownBaseConfig? = null, | ||||
| 
 | ||||
|         @Json(name = "m.identity_server") | ||||
|         var identityServer: WellKnownBaseConfig? = null, | ||||
| 
 | ||||
|         @Json(name = "m.integrations") | ||||
|         var integrations: Map<String, @JvmSuppressWildcards Any>? = null | ||||
| ) { | ||||
|     /** | ||||
|      * Returns the list of integration managers proposed | ||||
|      */ | ||||
|     fun getIntegrationManagers(): List<WellKnownManagerConfig> { | ||||
|         val managers = ArrayList<WellKnownManagerConfig>() | ||||
|         integrations?.get("managers")?.let { | ||||
|             (it as? ArrayList<*>)?.let { configs -> | ||||
|                 configs.forEach { config -> | ||||
|                     (config as? Map<*, *>)?.let { map -> | ||||
|                         val apiUrl = map["api_url"] as? String | ||||
|                         val uiUrl = map["ui_url"] as? String ?: apiUrl | ||||
|                         if (apiUrl != null | ||||
|                                 && apiUrl.startsWith("https://") | ||||
|                                 && uiUrl!!.startsWith("https://")) { | ||||
|                             managers.add(WellKnownManagerConfig( | ||||
|                                     apiUrl = apiUrl, | ||||
|                                     uiUrl = uiUrl | ||||
|                             )) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return managers | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,34 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.api.auth.data | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| 
 | ||||
| /** | ||||
|  * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery | ||||
|  * <pre> | ||||
|  * { | ||||
|  *     "base_url": "https://vector.im" | ||||
|  * } | ||||
|  * </pre> | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| data class WellKnownBaseConfig( | ||||
|         @Json(name = "base_url") | ||||
|         val baseURL: String? = null | ||||
| ) | ||||
| @ -0,0 +1,21 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package im.vector.matrix.android.api.auth.data | ||||
| 
 | ||||
| data class WellKnownManagerConfig( | ||||
|         val apiUrl : String, | ||||
|         val uiUrl: String | ||||
| ) | ||||
| @ -0,0 +1,48 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.api.auth.login | ||||
| 
 | ||||
| import im.vector.matrix.android.api.MatrixCallback | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| import im.vector.matrix.android.api.util.Cancelable | ||||
| 
 | ||||
| interface LoginWizard { | ||||
| 
 | ||||
|     /** | ||||
|      * @param login the login field | ||||
|      * @param password the password field | ||||
|      * @param deviceName the initial device name | ||||
|      * @param callback  the matrix callback on which you'll receive the result of authentication. | ||||
|      * @return return a [Cancelable] | ||||
|      */ | ||||
|     fun login(login: String, | ||||
|               password: String, | ||||
|               deviceName: String, | ||||
|               callback: MatrixCallback<Session>): Cancelable | ||||
| 
 | ||||
|     /** | ||||
|      * Reset user password | ||||
|      */ | ||||
|     fun resetPassword(email: String, | ||||
|                       newPassword: String, | ||||
|                       callback: MatrixCallback<Unit>): Cancelable | ||||
| 
 | ||||
|     /** | ||||
|      * Confirm the new password, once the user has checked his email | ||||
|      */ | ||||
|     fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable | ||||
| } | ||||
| @ -0,0 +1,22 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.api.auth.registration | ||||
| 
 | ||||
| sealed class RegisterThreePid { | ||||
|     data class Email(val email: String) : RegisterThreePid() | ||||
|     data class Msisdn(val msisdn: String, val countryCode: String) : RegisterThreePid() | ||||
| } | ||||
| @ -0,0 +1,30 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.api.auth.registration | ||||
| 
 | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| 
 | ||||
| // Either a session or an object containing data about registration stages | ||||
| sealed class RegistrationResult { | ||||
|     data class Success(val session: Session) : RegistrationResult() | ||||
|     data class FlowResponse(val flowResult: FlowResult) : RegistrationResult() | ||||
| } | ||||
| 
 | ||||
| data class FlowResult( | ||||
|         val missingStages: List<Stage>, | ||||
|         val completedStages: List<Stage> | ||||
| ) | ||||
| @ -0,0 +1,46 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.api.auth.registration | ||||
| 
 | ||||
| import im.vector.matrix.android.api.MatrixCallback | ||||
| import im.vector.matrix.android.api.util.Cancelable | ||||
| 
 | ||||
| interface RegistrationWizard { | ||||
| 
 | ||||
|     fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable | ||||
| 
 | ||||
|     fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback<RegistrationResult>): Cancelable | ||||
| 
 | ||||
|     fun performReCaptcha(response: String, callback: MatrixCallback<RegistrationResult>): Cancelable | ||||
| 
 | ||||
|     fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable | ||||
| 
 | ||||
|     fun dummy(callback: MatrixCallback<RegistrationResult>): Cancelable | ||||
| 
 | ||||
|     fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable | ||||
| 
 | ||||
|     fun sendAgainThreePid(callback: MatrixCallback<RegistrationResult>): Cancelable | ||||
| 
 | ||||
|     fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable | ||||
| 
 | ||||
|     fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback<RegistrationResult>): Cancelable | ||||
| 
 | ||||
|     val currentThreePid: String? | ||||
| 
 | ||||
|     // True when login and password has been sent with success to the homeserver | ||||
|     val isRegistrationStarted: Boolean | ||||
| } | ||||
| @ -0,0 +1,44 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.api.auth.registration | ||||
| 
 | ||||
| sealed class Stage(open val mandatory: Boolean) { | ||||
| 
 | ||||
|     // m.login.recaptcha | ||||
|     data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) | ||||
| 
 | ||||
|     // m.login.oauth2 | ||||
|     // m.login.email.identity | ||||
|     data class Email(override val mandatory: Boolean) : Stage(mandatory) | ||||
| 
 | ||||
|     // m.login.msisdn | ||||
|     data class Msisdn(override val mandatory: Boolean) : Stage(mandatory) | ||||
| 
 | ||||
|     // m.login.token | ||||
| 
 | ||||
|     // m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username | ||||
|     // and a password, the dummy stage has to be done | ||||
|     data class Dummy(override val mandatory: Boolean) : Stage(mandatory) | ||||
| 
 | ||||
|     // Undocumented yet: m.login.terms | ||||
|     data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) | ||||
| 
 | ||||
|     // For unknown stages | ||||
|     data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory) | ||||
| } | ||||
| 
 | ||||
| typealias TermPolicies = Map<*, *> | ||||
| @ -34,6 +34,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { | ||||
|     data class Cancelled(val throwable: Throwable? = null) : Failure(throwable) | ||||
|     data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException) | ||||
|     data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) | ||||
|     object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false"))) | ||||
|     // When server send an error, but it cannot be interpreted as a MatrixError | ||||
|     data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody)) | ||||
| 
 | ||||
|  | ||||
| @ -31,7 +31,9 @@ data class MatrixError( | ||||
|         @Json(name = "consent_uri") val consentUri: String? = null, | ||||
|         // RESOURCE_LIMIT_EXCEEDED data | ||||
|         @Json(name = "limit_type") val limitType: String? = null, | ||||
|         @Json(name = "admin_contact") val adminUri: String? = null) { | ||||
|         @Json(name = "admin_contact") val adminUri: String? = null, | ||||
|         // For LIMIT_EXCEEDED | ||||
|         @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) { | ||||
| 
 | ||||
|     companion object { | ||||
|         const val FORBIDDEN = "M_FORBIDDEN" | ||||
|  | ||||
| @ -29,3 +29,5 @@ interface Cancelable { | ||||
|         // no-op | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| object NoOpCancellable : Cancelable | ||||
|  | ||||
| @ -17,20 +17,47 @@ | ||||
| package im.vector.matrix.android.internal.auth | ||||
| 
 | ||||
| import im.vector.matrix.android.api.auth.data.Credentials | ||||
| import im.vector.matrix.android.api.auth.data.Versions | ||||
| import im.vector.matrix.android.internal.auth.data.LoginFlowResponse | ||||
| import im.vector.matrix.android.internal.auth.data.PasswordLoginParams | ||||
| import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed | ||||
| import im.vector.matrix.android.internal.auth.registration.* | ||||
| import im.vector.matrix.android.internal.network.NetworkConstants | ||||
| import retrofit2.Call | ||||
| import retrofit2.http.Body | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Headers | ||||
| import retrofit2.http.POST | ||||
| import retrofit2.http.* | ||||
| 
 | ||||
| /** | ||||
|  * The login REST API. | ||||
|  */ | ||||
| internal interface AuthAPI { | ||||
| 
 | ||||
|     /** | ||||
|      * Get the version information of the homeserver | ||||
|      */ | ||||
|     @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") | ||||
|     fun versions(): Call<Versions> | ||||
| 
 | ||||
|     /** | ||||
|      * Register to the homeserver | ||||
|      * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management | ||||
|      */ | ||||
|     @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") | ||||
|     fun register(@Body registrationParams: RegistrationParams): Call<Credentials> | ||||
| 
 | ||||
|     /** | ||||
|      * Add 3Pid during registration | ||||
|      * Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928 | ||||
|      * https://github.com/matrix-org/matrix-doc/pull/2290 | ||||
|      */ | ||||
|     @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/{threePid}/requestToken") | ||||
|     fun add3Pid(@Path("threePid") threePid: String, @Body params: AddThreePidRegistrationParams): Call<AddThreePidRegistrationResponse> | ||||
| 
 | ||||
|     /** | ||||
|      * Validate 3pid | ||||
|      */ | ||||
|     @POST | ||||
|     fun validate3Pid(@Url url: String, @Body params: ValidationCodeBody): Call<SuccessResult> | ||||
| 
 | ||||
|     /** | ||||
|      * Get the supported login flow | ||||
|      * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login | ||||
| @ -47,4 +74,16 @@ internal interface AuthAPI { | ||||
|     @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") | ||||
|     @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") | ||||
|     fun login(@Body loginParams: PasswordLoginParams): Call<Credentials> | ||||
| 
 | ||||
|     /** | ||||
|      * Ask the homeserver to reset the password associated with the provided email. | ||||
|      */ | ||||
|     @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken") | ||||
|     fun resetPassword(@Body params: AddThreePidRegistrationParams): Call<AddThreePidRegistrationResponse> | ||||
| 
 | ||||
|     /** | ||||
|      * Ask the homeserver to reset the password with the provided new password once the email is validated. | ||||
|      */ | ||||
|     @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") | ||||
|     fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed): Call<Unit> | ||||
| } | ||||
|  | ||||
| @ -20,8 +20,10 @@ import android.content.Context | ||||
| import dagger.Binds | ||||
| import dagger.Module | ||||
| import dagger.Provides | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.matrix.android.api.auth.AuthenticationService | ||||
| import im.vector.matrix.android.internal.auth.db.AuthRealmMigration | ||||
| import im.vector.matrix.android.internal.auth.db.AuthRealmModule | ||||
| import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore | ||||
| import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore | ||||
| import im.vector.matrix.android.internal.database.RealmKeysUtils | ||||
| import im.vector.matrix.android.internal.di.AuthDatabase | ||||
| @ -50,7 +52,8 @@ internal abstract class AuthModule { | ||||
|                     } | ||||
|                     .name("matrix-sdk-auth.realm") | ||||
|                     .modules(AuthRealmModule()) | ||||
|                     .deleteRealmIfMigrationNeeded() | ||||
|                     .schemaVersion(AuthRealmMigration.SCHEMA_VERSION) | ||||
|                     .migration(AuthRealmMigration()) | ||||
|                     .build() | ||||
|         } | ||||
|     } | ||||
| @ -59,5 +62,11 @@ internal abstract class AuthModule { | ||||
|     abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun bindAuthenticator(authenticator: DefaultAuthenticator): Authenticator | ||||
|     abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun bindSessionCreator(sessionCreator: DefaultSessionCreator): SessionCreator | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,205 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth | ||||
| 
 | ||||
| import dagger.Lazy | ||||
| import im.vector.matrix.android.api.MatrixCallback | ||||
| import im.vector.matrix.android.api.auth.AuthenticationService | ||||
| import im.vector.matrix.android.api.auth.data.* | ||||
| import im.vector.matrix.android.api.auth.login.LoginWizard | ||||
| import im.vector.matrix.android.api.auth.registration.RegistrationWizard | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| import im.vector.matrix.android.api.util.Cancelable | ||||
| import im.vector.matrix.android.internal.SessionManager | ||||
| import im.vector.matrix.android.internal.auth.data.LoginFlowResponse | ||||
| import im.vector.matrix.android.internal.auth.db.PendingSessionData | ||||
| import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard | ||||
| import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard | ||||
| import im.vector.matrix.android.internal.di.Unauthenticated | ||||
| import im.vector.matrix.android.internal.network.RetrofitFactory | ||||
| import im.vector.matrix.android.internal.network.executeRequest | ||||
| import im.vector.matrix.android.internal.task.launchToCallback | ||||
| import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers | ||||
| import im.vector.matrix.android.internal.util.toCancelable | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import okhttp3.OkHttpClient | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated | ||||
|                                                                 private val okHttpClient: Lazy<OkHttpClient>, | ||||
|                                                                 private val retrofitFactory: RetrofitFactory, | ||||
|                                                                 private val coroutineDispatchers: MatrixCoroutineDispatchers, | ||||
|                                                                 private val sessionParamsStore: SessionParamsStore, | ||||
|                                                                 private val sessionManager: SessionManager, | ||||
|                                                                 private val sessionCreator: SessionCreator, | ||||
|                                                                 private val pendingSessionStore: PendingSessionStore | ||||
| ) : AuthenticationService { | ||||
| 
 | ||||
|     private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() | ||||
| 
 | ||||
|     private var currentLoginWizard: LoginWizard? = null | ||||
|     private var currentRegistrationWizard: RegistrationWizard? = null | ||||
| 
 | ||||
|     override fun hasAuthenticatedSessions(): Boolean { | ||||
|         return sessionParamsStore.getLast() != null | ||||
|     } | ||||
| 
 | ||||
|     override fun getLastAuthenticatedSession(): Session? { | ||||
|         val sessionParams = sessionParamsStore.getLast() | ||||
|         return sessionParams?.let { | ||||
|             sessionManager.getOrCreateSession(it) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getSession(sessionParams: SessionParams): Session? { | ||||
|         return sessionManager.getOrCreateSession(sessionParams) | ||||
|     } | ||||
| 
 | ||||
|     override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable { | ||||
|         pendingSessionData = null | ||||
| 
 | ||||
|         return GlobalScope.launch(coroutineDispatchers.main) { | ||||
|             pendingSessionStore.delete() | ||||
| 
 | ||||
|             val result = runCatching { | ||||
|                 getLoginFlowInternal(homeServerConnectionConfig) | ||||
|             } | ||||
|             result.fold( | ||||
|                     { | ||||
|                         if (it is LoginFlowResult.Success) { | ||||
|                             // The homeserver exists and up to date, keep the config | ||||
|                             pendingSessionData = PendingSessionData(homeServerConnectionConfig) | ||||
|                                     .also { data -> pendingSessionStore.savePendingSessionData(data) } | ||||
|                         } | ||||
|                         callback.onSuccess(it) | ||||
|                     }, | ||||
|                     { | ||||
|                         callback.onFailure(it) | ||||
|                     } | ||||
|             ) | ||||
|         } | ||||
|                 .toCancelable() | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) { | ||||
|         val authAPI = buildAuthAPI(homeServerConnectionConfig) | ||||
| 
 | ||||
|         // First check the homeserver version | ||||
|         val versions = executeRequest<Versions> { | ||||
|             apiCall = authAPI.versions() | ||||
|         } | ||||
| 
 | ||||
|         if (versions.isSupportedBySdk()) { | ||||
|             // Get the login flow | ||||
|             val loginFlowResponse = executeRequest<LoginFlowResponse> { | ||||
|                 apiCall = authAPI.getLoginFlows() | ||||
|             } | ||||
|             LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk()) | ||||
|         } else { | ||||
|             // Not supported | ||||
|             LoginFlowResult.OutdatedHomeserver | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getRegistrationWizard(): RegistrationWizard { | ||||
|         return currentRegistrationWizard | ||||
|                 ?: let { | ||||
|                     pendingSessionData?.homeServerConnectionConfig?.let { | ||||
|                         DefaultRegistrationWizard( | ||||
|                                 okHttpClient, | ||||
|                                 retrofitFactory, | ||||
|                                 coroutineDispatchers, | ||||
|                                 sessionCreator, | ||||
|                                 pendingSessionStore | ||||
|                         ).also { | ||||
|                             currentRegistrationWizard = it | ||||
|                         } | ||||
|                     } ?: error("Please call getLoginFlow() with success first") | ||||
|                 } | ||||
|     } | ||||
| 
 | ||||
|     override val isRegistrationStarted: Boolean | ||||
|         get() = currentRegistrationWizard?.isRegistrationStarted == true | ||||
| 
 | ||||
|     override fun getLoginWizard(): LoginWizard { | ||||
|         return currentLoginWizard | ||||
|                 ?: let { | ||||
|                     pendingSessionData?.homeServerConnectionConfig?.let { | ||||
|                         DefaultLoginWizard( | ||||
|                                 okHttpClient, | ||||
|                                 retrofitFactory, | ||||
|                                 coroutineDispatchers, | ||||
|                                 sessionCreator, | ||||
|                                 pendingSessionStore | ||||
|                         ).also { | ||||
|                             currentLoginWizard = it | ||||
|                         } | ||||
|                     } ?: error("Please call getLoginFlow() with success first") | ||||
|                 } | ||||
|     } | ||||
| 
 | ||||
|     override fun cancelPendingLoginOrRegistration() { | ||||
|         currentLoginWizard = null | ||||
|         currentRegistrationWizard = null | ||||
| 
 | ||||
|         // Keep only the home sever config | ||||
|         // Update the local pendingSessionData synchronously | ||||
|         pendingSessionData = pendingSessionData?.homeServerConnectionConfig | ||||
|                 ?.let { PendingSessionData(it) } | ||||
|                 .also { | ||||
|                     GlobalScope.launch(coroutineDispatchers.main) { | ||||
|                         if (it == null) { | ||||
|                             // Should not happen | ||||
|                             pendingSessionStore.delete() | ||||
|                         } else { | ||||
|                             pendingSessionStore.savePendingSessionData(it) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|     } | ||||
| 
 | ||||
|     override fun reset() { | ||||
|         currentLoginWizard = null | ||||
|         currentRegistrationWizard = null | ||||
| 
 | ||||
|         pendingSessionData = null | ||||
| 
 | ||||
|         GlobalScope.launch(coroutineDispatchers.main) { | ||||
|             pendingSessionStore.delete() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, | ||||
|                                       credentials: Credentials, | ||||
|                                       callback: MatrixCallback<Session>): Cancelable { | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             createSessionFromSso(credentials, homeServerConnectionConfig) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun createSessionFromSso(credentials: Credentials, | ||||
|                                              homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { | ||||
|         sessionCreator.createSession(credentials, homeServerConnectionConfig) | ||||
|     } | ||||
| 
 | ||||
|     private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { | ||||
|         val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) | ||||
|         return retrofit.create(AuthAPI::class.java) | ||||
|     } | ||||
| } | ||||
| @ -1,138 +0,0 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth | ||||
| 
 | ||||
| import android.util.Patterns | ||||
| import dagger.Lazy | ||||
| import im.vector.matrix.android.api.MatrixCallback | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.matrix.android.api.auth.data.Credentials | ||||
| import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig | ||||
| import im.vector.matrix.android.api.auth.data.SessionParams | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| import im.vector.matrix.android.api.util.Cancelable | ||||
| import im.vector.matrix.android.internal.SessionManager | ||||
| import im.vector.matrix.android.internal.auth.data.LoginFlowResponse | ||||
| import im.vector.matrix.android.internal.auth.data.PasswordLoginParams | ||||
| import im.vector.matrix.android.internal.auth.data.ThreePidMedium | ||||
| import im.vector.matrix.android.internal.di.Unauthenticated | ||||
| import im.vector.matrix.android.internal.extensions.foldToCallback | ||||
| import im.vector.matrix.android.internal.network.RetrofitFactory | ||||
| import im.vector.matrix.android.internal.network.executeRequest | ||||
| import im.vector.matrix.android.internal.util.CancelableCoroutine | ||||
| import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import okhttp3.OkHttpClient | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| internal class DefaultAuthenticator @Inject constructor(@Unauthenticated | ||||
|                                                         private val okHttpClient: Lazy<OkHttpClient>, | ||||
|                                                         private val retrofitFactory: RetrofitFactory, | ||||
|                                                         private val coroutineDispatchers: MatrixCoroutineDispatchers, | ||||
|                                                         private val sessionParamsStore: SessionParamsStore, | ||||
|                                                         private val sessionManager: SessionManager | ||||
| ) : Authenticator { | ||||
| 
 | ||||
|     override fun hasAuthenticatedSessions(): Boolean { | ||||
|         return sessionParamsStore.getLast() != null | ||||
|     } | ||||
| 
 | ||||
|     override fun getLastAuthenticatedSession(): Session? { | ||||
|         val sessionParams = sessionParamsStore.getLast() | ||||
|         return sessionParams?.let { | ||||
|             sessionManager.getOrCreateSession(it) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getSession(sessionParams: SessionParams): Session? { | ||||
|         return sessionManager.getOrCreateSession(sessionParams) | ||||
|     } | ||||
| 
 | ||||
|     override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable { | ||||
|         val job = GlobalScope.launch(coroutineDispatchers.main) { | ||||
|             val result = runCatching { | ||||
|                 getLoginFlowInternal(homeServerConnectionConfig) | ||||
|             } | ||||
|             result.foldToCallback(callback) | ||||
|         } | ||||
|         return CancelableCoroutine(job) | ||||
|     } | ||||
| 
 | ||||
|     override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig, | ||||
|                               login: String, | ||||
|                               password: String, | ||||
|                               callback: MatrixCallback<Session>): Cancelable { | ||||
|         val job = GlobalScope.launch(coroutineDispatchers.main) { | ||||
|             val sessionOrFailure = runCatching { | ||||
|                 authenticate(homeServerConnectionConfig, login, password) | ||||
|             } | ||||
|             sessionOrFailure.foldToCallback(callback) | ||||
|         } | ||||
|         return CancelableCoroutine(job) | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) { | ||||
|         val authAPI = buildAuthAPI(homeServerConnectionConfig) | ||||
| 
 | ||||
|         executeRequest<LoginFlowResponse> { | ||||
|             apiCall = authAPI.getLoginFlows() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig, | ||||
|                                      login: String, | ||||
|                                      password: String) = withContext(coroutineDispatchers.io) { | ||||
|         val authAPI = buildAuthAPI(homeServerConnectionConfig) | ||||
|         val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) { | ||||
|             PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, "Mobile") | ||||
|         } else { | ||||
|             PasswordLoginParams.userIdentifier(login, password, "Mobile") | ||||
|         } | ||||
|         val credentials = executeRequest<Credentials> { | ||||
|             apiCall = authAPI.login(loginParams) | ||||
|         } | ||||
|         val sessionParams = SessionParams(credentials, homeServerConnectionConfig) | ||||
|         sessionParamsStore.save(sessionParams) | ||||
|         sessionManager.getOrCreateSession(sessionParams) | ||||
|     } | ||||
| 
 | ||||
|     override fun createSessionFromSso(credentials: Credentials, | ||||
|                                       homeServerConnectionConfig: HomeServerConnectionConfig, | ||||
|                                       callback: MatrixCallback<Session>): Cancelable { | ||||
|         val job = GlobalScope.launch(coroutineDispatchers.main) { | ||||
|             val sessionOrFailure = runCatching { | ||||
|                 createSessionFromSso(credentials, homeServerConnectionConfig) | ||||
|             } | ||||
|             sessionOrFailure.foldToCallback(callback) | ||||
|         } | ||||
|         return CancelableCoroutine(job) | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun createSessionFromSso(credentials: Credentials, | ||||
|                                              homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { | ||||
|         val sessionParams = SessionParams(credentials, homeServerConnectionConfig) | ||||
|         sessionParamsStore.save(sessionParams) | ||||
|         sessionManager.getOrCreateSession(sessionParams) | ||||
|     } | ||||
| 
 | ||||
|     private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { | ||||
|         val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) | ||||
|         return retrofit.create(AuthAPI::class.java) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,31 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth | ||||
| 
 | ||||
| import im.vector.matrix.android.internal.auth.db.PendingSessionData | ||||
| 
 | ||||
| /** | ||||
|  * Store for elements when doing login or registration | ||||
|  */ | ||||
| internal interface PendingSessionStore { | ||||
| 
 | ||||
|     suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) | ||||
| 
 | ||||
|     fun getPendingSessionData(): PendingSessionData? | ||||
| 
 | ||||
|     suspend fun delete() | ||||
| } | ||||
| @ -0,0 +1,68 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth | ||||
| 
 | ||||
| import android.net.Uri | ||||
| import im.vector.matrix.android.api.auth.data.Credentials | ||||
| import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig | ||||
| import im.vector.matrix.android.api.auth.data.SessionParams | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| import im.vector.matrix.android.internal.SessionManager | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| internal interface SessionCreator { | ||||
|     suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session | ||||
| } | ||||
| 
 | ||||
| internal class DefaultSessionCreator @Inject constructor( | ||||
|         private val sessionParamsStore: SessionParamsStore, | ||||
|         private val sessionManager: SessionManager, | ||||
|         private val pendingSessionStore: PendingSessionStore | ||||
| ) : SessionCreator { | ||||
| 
 | ||||
|     /** | ||||
|      * Credentials can affect the homeServerConnectionConfig, override home server url and/or | ||||
|      * identity server url if provided in the credentials | ||||
|      */ | ||||
|     override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session { | ||||
|         // We can cleanup the pending session params | ||||
|         pendingSessionStore.delete() | ||||
| 
 | ||||
|         val sessionParams = SessionParams( | ||||
|                 credentials = credentials, | ||||
|                 homeServerConnectionConfig = homeServerConnectionConfig.copy( | ||||
|                         homeServerUri = credentials.wellKnown?.homeServer?.baseURL | ||||
|                                 // remove trailing "/" | ||||
|                                 ?.trim { it == '/' } | ||||
|                                 ?.takeIf { it.isNotBlank() } | ||||
|                                 ?.also { Timber.d("Overriding homeserver url to $it") } | ||||
|                                 ?.let { Uri.parse(it) } | ||||
|                                 ?: homeServerConnectionConfig.homeServerUri, | ||||
|                         identityServerUri = credentials.wellKnown?.identityServer?.baseURL | ||||
|                                 // remove trailing "/" | ||||
|                                 ?.trim { it == '/' } | ||||
|                                 ?.takeIf { it.isNotBlank() } | ||||
|                                 ?.also { Timber.d("Overriding identity server url to $it") } | ||||
|                                 ?.let { Uri.parse(it) } | ||||
|                                 ?: homeServerConnectionConfig.identityServerUri | ||||
|                 )) | ||||
| 
 | ||||
|         sessionParamsStore.save(sessionParams) | ||||
|         return sessionManager.getOrCreateSession(sessionParams) | ||||
|     } | ||||
| } | ||||
| @ -30,12 +30,4 @@ data class InteractiveAuthenticationFlow( | ||||
| 
 | ||||
|         @Json(name = "stages") | ||||
|         val stages: List<String>? = null | ||||
| ) { | ||||
| 
 | ||||
|     companion object { | ||||
|         // Possible values for type | ||||
|         const val TYPE_LOGIN_SSO = "m.login.sso" | ||||
|         const val TYPE_LOGIN_TOKEN = "m.login.token" | ||||
|         const val TYPE_LOGIN_PASSWORD = "m.login.password" | ||||
|     } | ||||
| } | ||||
| ) | ||||
|  | ||||
| @ -25,4 +25,7 @@ object LoginFlowTypes { | ||||
|     const val MSISDN = "m.login.msisdn" | ||||
|     const val RECAPTCHA = "m.login.recaptcha" | ||||
|     const val DUMMY = "m.login.dummy" | ||||
|     const val TERMS = "m.login.terms" | ||||
|     const val TOKEN = "m.login.token" | ||||
|     const val SSO = "m.login.sso" | ||||
| } | ||||
|  | ||||
| @ -19,34 +19,46 @@ package im.vector.matrix.android.internal.auth.data | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| 
 | ||||
| /** | ||||
|  * Ref: | ||||
|  * - https://matrix.org/docs/spec/client_server/r0.5.0#password-based | ||||
|  * - https://matrix.org/docs/spec/client_server/r0.5.0#identifier-types | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| internal data class PasswordLoginParams(@Json(name = "identifier") val identifier: Map<String, String>, | ||||
|                                @Json(name = "password") val password: String, | ||||
|                                @Json(name = "type") override val type: String, | ||||
|                                @Json(name = "initial_device_display_name") val deviceDisplayName: String?, | ||||
|                                @Json(name = "device_id") val deviceId: String?) : LoginParams { | ||||
| internal data class PasswordLoginParams( | ||||
|         @Json(name = "identifier") val identifier: Map<String, String>, | ||||
|         @Json(name = "password") val password: String, | ||||
|         @Json(name = "type") override val type: String, | ||||
|         @Json(name = "initial_device_display_name") val deviceDisplayName: String?, | ||||
|         @Json(name = "device_id") val deviceId: String?) : LoginParams { | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val IDENTIFIER_KEY_TYPE = "type" | ||||
| 
 | ||||
|         val IDENTIFIER_KEY_TYPE_USER = "m.id.user" | ||||
|         val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty" | ||||
|         val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone" | ||||
|         private const val IDENTIFIER_KEY_TYPE_USER = "m.id.user" | ||||
|         private const val IDENTIFIER_KEY_USER = "user" | ||||
| 
 | ||||
|         val IDENTIFIER_KEY_TYPE = "type" | ||||
|         val IDENTIFIER_KEY_MEDIUM = "medium" | ||||
|         val IDENTIFIER_KEY_ADDRESS = "address" | ||||
|         val IDENTIFIER_KEY_USER = "user" | ||||
|         val IDENTIFIER_KEY_COUNTRY = "country" | ||||
|         val IDENTIFIER_KEY_NUMBER = "number" | ||||
|         private const val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty" | ||||
|         private const val IDENTIFIER_KEY_MEDIUM = "medium" | ||||
|         private const val IDENTIFIER_KEY_ADDRESS = "address" | ||||
| 
 | ||||
|         private const val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone" | ||||
|         private const val IDENTIFIER_KEY_COUNTRY = "country" | ||||
|         private const val IDENTIFIER_KEY_PHONE = "phone" | ||||
| 
 | ||||
|         fun userIdentifier(user: String, | ||||
|                            password: String, | ||||
|                            deviceDisplayName: String? = null, | ||||
|                            deviceId: String? = null): PasswordLoginParams { | ||||
|             val identifier = HashMap<String, String>() | ||||
|             identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_USER | ||||
|             identifier[IDENTIFIER_KEY_USER] = user | ||||
|             return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId) | ||||
|             return PasswordLoginParams( | ||||
|                     mapOf( | ||||
|                             IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER, | ||||
|                             IDENTIFIER_KEY_USER to user | ||||
|                     ), | ||||
|                     password, | ||||
|                     LoginFlowTypes.PASSWORD, | ||||
|                     deviceDisplayName, | ||||
|                     deviceId) | ||||
|         } | ||||
| 
 | ||||
|         fun thirdPartyIdentifier(medium: String, | ||||
| @ -54,11 +66,33 @@ internal data class PasswordLoginParams(@Json(name = "identifier") val identifie | ||||
|                                  password: String, | ||||
|                                  deviceDisplayName: String? = null, | ||||
|                                  deviceId: String? = null): PasswordLoginParams { | ||||
|             val identifier = HashMap<String, String>() | ||||
|             identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_THIRD_PARTY | ||||
|             identifier[IDENTIFIER_KEY_MEDIUM] = medium | ||||
|             identifier[IDENTIFIER_KEY_ADDRESS] = address | ||||
|             return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId) | ||||
|             return PasswordLoginParams( | ||||
|                     mapOf( | ||||
|                             IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY, | ||||
|                             IDENTIFIER_KEY_MEDIUM to medium, | ||||
|                             IDENTIFIER_KEY_ADDRESS to address | ||||
|                     ), | ||||
|                     password, | ||||
|                     LoginFlowTypes.PASSWORD, | ||||
|                     deviceDisplayName, | ||||
|                     deviceId) | ||||
|         } | ||||
| 
 | ||||
|         fun phoneIdentifier(country: String, | ||||
|                             phone: String, | ||||
|                             password: String, | ||||
|                             deviceDisplayName: String? = null, | ||||
|                             deviceId: String? = null): PasswordLoginParams { | ||||
|             return PasswordLoginParams( | ||||
|                     mapOf( | ||||
|                             IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE, | ||||
|                             IDENTIFIER_KEY_COUNTRY to country, | ||||
|                             IDENTIFIER_KEY_PHONE to phone | ||||
|                     ), | ||||
|                     password, | ||||
|                     LoginFlowTypes.PASSWORD, | ||||
|                     deviceDisplayName, | ||||
|                     deviceId) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,50 @@ | ||||
| /* | ||||
|  * Copyright 2018 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.db | ||||
| 
 | ||||
| import io.realm.DynamicRealm | ||||
| import io.realm.RealmMigration | ||||
| import timber.log.Timber | ||||
| 
 | ||||
| internal class AuthRealmMigration : RealmMigration { | ||||
| 
 | ||||
|     companion object { | ||||
|         // Current schema version | ||||
|         const val SCHEMA_VERSION = 1L | ||||
|     } | ||||
| 
 | ||||
|     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { | ||||
|         Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") | ||||
| 
 | ||||
|         if (oldVersion <= 0) { | ||||
|             Timber.d("Step 0 -> 1") | ||||
|             Timber.d("Create PendingSessionEntity") | ||||
| 
 | ||||
|             realm.schema.create("PendingSessionEntity") | ||||
|                     .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) | ||||
|                     .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) | ||||
|                     .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java) | ||||
|                     .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true) | ||||
|                     .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java) | ||||
|                     .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true) | ||||
|                     .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java) | ||||
|                     .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java) | ||||
|                     .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) | ||||
|                     .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -23,6 +23,7 @@ import io.realm.annotations.RealmModule | ||||
|  */ | ||||
| @RealmModule(library = true, | ||||
|         classes = [ | ||||
|             SessionParamsEntity::class | ||||
|             SessionParamsEntity::class, | ||||
|             PendingSessionEntity::class | ||||
|         ]) | ||||
| internal class AuthRealmModule | ||||
|  | ||||
| @ -0,0 +1,50 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.db | ||||
| 
 | ||||
| import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig | ||||
| import im.vector.matrix.android.internal.auth.login.ResetPasswordData | ||||
| import im.vector.matrix.android.internal.auth.registration.ThreePidData | ||||
| import java.util.* | ||||
| 
 | ||||
| /** | ||||
|  * This class holds all pending data when creating a session, either by login or by register | ||||
|  */ | ||||
| internal data class PendingSessionData( | ||||
|         val homeServerConnectionConfig: HomeServerConnectionConfig, | ||||
| 
 | ||||
|         /* ========================================================================================== | ||||
|          * Common | ||||
|          * ========================================================================================== */ | ||||
| 
 | ||||
|         val clientSecret: String = UUID.randomUUID().toString(), | ||||
|         val sendAttempt: Int = 0, | ||||
| 
 | ||||
|         /* ========================================================================================== | ||||
|          * For login | ||||
|          * ========================================================================================== */ | ||||
| 
 | ||||
|         val resetPasswordData: ResetPasswordData? = null, | ||||
| 
 | ||||
|         /* ========================================================================================== | ||||
|          * For register | ||||
|          * ========================================================================================== */ | ||||
| 
 | ||||
|         val currentSession: String? = null, | ||||
|         val isRegistrationStarted: Boolean = false, | ||||
|         val currentThreePidData: ThreePidData? = null | ||||
| ) | ||||
| @ -0,0 +1,29 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.db | ||||
| 
 | ||||
| import io.realm.RealmObject | ||||
| 
 | ||||
| internal open class PendingSessionEntity( | ||||
|         var homeServerConnectionConfigJson: String = "", | ||||
|         var clientSecret: String = "", | ||||
|         var sendAttempt: Int = 0, | ||||
|         var resetPasswordDataJson: String? = null, | ||||
|         var currentSession: String? = null, | ||||
|         var isRegistrationStarted: Boolean = false, | ||||
|         var currentThreePidDataJson: String? = null | ||||
| ) : RealmObject() | ||||
| @ -0,0 +1,69 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.db | ||||
| 
 | ||||
| import com.squareup.moshi.Moshi | ||||
| import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig | ||||
| import im.vector.matrix.android.internal.auth.login.ResetPasswordData | ||||
| import im.vector.matrix.android.internal.auth.registration.ThreePidData | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| internal class PendingSessionMapper @Inject constructor(moshi: Moshi) { | ||||
| 
 | ||||
|     private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java) | ||||
|     private val resetPasswordDataAdapter = moshi.adapter(ResetPasswordData::class.java) | ||||
|     private val threePidDataAdapter = moshi.adapter(ThreePidData::class.java) | ||||
| 
 | ||||
|     fun map(entity: PendingSessionEntity?): PendingSessionData? { | ||||
|         if (entity == null) { | ||||
|             return null | ||||
|         } | ||||
| 
 | ||||
|         val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson)!! | ||||
|         val resetPasswordData = entity.resetPasswordDataJson?.let { resetPasswordDataAdapter.fromJson(it) } | ||||
|         val threePidData = entity.currentThreePidDataJson?.let { threePidDataAdapter.fromJson(it) } | ||||
| 
 | ||||
|         return PendingSessionData( | ||||
|                 homeServerConnectionConfig = homeServerConnectionConfig, | ||||
|                 clientSecret = entity.clientSecret, | ||||
|                 sendAttempt = entity.sendAttempt, | ||||
|                 resetPasswordData = resetPasswordData, | ||||
|                 currentSession = entity.currentSession, | ||||
|                 isRegistrationStarted = entity.isRegistrationStarted, | ||||
|                 currentThreePidData = threePidData) | ||||
|     } | ||||
| 
 | ||||
|     fun map(sessionData: PendingSessionData?): PendingSessionEntity? { | ||||
|         if (sessionData == null) { | ||||
|             return null | ||||
|         } | ||||
| 
 | ||||
|         val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionData.homeServerConnectionConfig) | ||||
|         val resetPasswordDataJson = resetPasswordDataAdapter.toJson(sessionData.resetPasswordData) | ||||
|         val currentThreePidDataJson = threePidDataAdapter.toJson(sessionData.currentThreePidData) | ||||
| 
 | ||||
|         return PendingSessionEntity( | ||||
|                 homeServerConnectionConfigJson = homeServerConnectionConfigJson, | ||||
|                 clientSecret = sessionData.clientSecret, | ||||
|                 sendAttempt = sessionData.sendAttempt, | ||||
|                 resetPasswordDataJson = resetPasswordDataJson, | ||||
|                 currentSession = sessionData.currentSession, | ||||
|                 isRegistrationStarted = sessionData.isRegistrationStarted, | ||||
|                 currentThreePidDataJson = currentThreePidDataJson | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,61 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.db | ||||
| 
 | ||||
| import im.vector.matrix.android.internal.auth.PendingSessionStore | ||||
| import im.vector.matrix.android.internal.database.awaitTransaction | ||||
| import im.vector.matrix.android.internal.di.AuthDatabase | ||||
| import io.realm.Realm | ||||
| import io.realm.RealmConfiguration | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper, | ||||
|                                                             @AuthDatabase | ||||
|                                                             private val realmConfiguration: RealmConfiguration | ||||
| ) : PendingSessionStore { | ||||
| 
 | ||||
|     override suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) { | ||||
|         awaitTransaction(realmConfiguration) { realm -> | ||||
|             val entity = mapper.map(pendingSessionData) | ||||
|             if (entity != null) { | ||||
|                 realm.where(PendingSessionEntity::class.java) | ||||
|                         .findAll() | ||||
|                         .deleteAllFromRealm() | ||||
| 
 | ||||
|                 realm.insert(entity) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getPendingSessionData(): PendingSessionData? { | ||||
|         return Realm.getInstance(realmConfiguration).use { realm -> | ||||
|             realm | ||||
|                     .where(PendingSessionEntity::class.java) | ||||
|                     .findAll() | ||||
|                     .map { mapper.map(it) } | ||||
|                     .firstOrNull() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun delete() { | ||||
|         awaitTransaction(realmConfiguration) { | ||||
|             it.where(PendingSessionEntity::class.java) | ||||
|                     .findAll() | ||||
|                     .deleteAllFromRealm() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -30,36 +30,33 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S | ||||
| ) : SessionParamsStore { | ||||
| 
 | ||||
|     override fun getLast(): SessionParams? { | ||||
|         val realm = Realm.getInstance(realmConfiguration) | ||||
|         val sessionParams = realm | ||||
|                 .where(SessionParamsEntity::class.java) | ||||
|                 .findAll() | ||||
|                 .map { mapper.map(it) } | ||||
|                 .lastOrNull() | ||||
|         realm.close() | ||||
|         return sessionParams | ||||
|         return Realm.getInstance(realmConfiguration).use { realm -> | ||||
|             realm | ||||
|                     .where(SessionParamsEntity::class.java) | ||||
|                     .findAll() | ||||
|                     .map { mapper.map(it) } | ||||
|                     .lastOrNull() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun get(userId: String): SessionParams? { | ||||
|         val realm = Realm.getInstance(realmConfiguration) | ||||
|         val sessionParams = realm | ||||
|                 .where(SessionParamsEntity::class.java) | ||||
|                 .equalTo(SessionParamsEntityFields.USER_ID, userId) | ||||
|                 .findAll() | ||||
|                 .map { mapper.map(it) } | ||||
|                 .firstOrNull() | ||||
|         realm.close() | ||||
|         return sessionParams | ||||
|         return Realm.getInstance(realmConfiguration).use { realm -> | ||||
|             realm | ||||
|                     .where(SessionParamsEntity::class.java) | ||||
|                     .equalTo(SessionParamsEntityFields.USER_ID, userId) | ||||
|                     .findAll() | ||||
|                     .map { mapper.map(it) } | ||||
|                     .firstOrNull() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getAll(): List<SessionParams> { | ||||
|         val realm = Realm.getInstance(realmConfiguration) | ||||
|         val sessionParams = realm | ||||
|                 .where(SessionParamsEntity::class.java) | ||||
|                 .findAll() | ||||
|                 .mapNotNull { mapper.map(it) } | ||||
|         realm.close() | ||||
|         return sessionParams | ||||
|         return Realm.getInstance(realmConfiguration).use { realm -> | ||||
|             realm | ||||
|                     .where(SessionParamsEntity::class.java) | ||||
|                     .findAll() | ||||
|                     .mapNotNull { mapper.map(it) } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun save(sessionParams: SessionParams) { | ||||
|  | ||||
| @ -0,0 +1,130 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.login | ||||
| 
 | ||||
| import android.util.Patterns | ||||
| import dagger.Lazy | ||||
| import im.vector.matrix.android.api.MatrixCallback | ||||
| import im.vector.matrix.android.api.auth.data.Credentials | ||||
| import im.vector.matrix.android.api.auth.login.LoginWizard | ||||
| import im.vector.matrix.android.api.auth.registration.RegisterThreePid | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| import im.vector.matrix.android.api.util.Cancelable | ||||
| import im.vector.matrix.android.api.util.NoOpCancellable | ||||
| import im.vector.matrix.android.internal.auth.AuthAPI | ||||
| import im.vector.matrix.android.internal.auth.PendingSessionStore | ||||
| import im.vector.matrix.android.internal.auth.SessionCreator | ||||
| import im.vector.matrix.android.internal.auth.data.PasswordLoginParams | ||||
| import im.vector.matrix.android.internal.auth.data.ThreePidMedium | ||||
| import im.vector.matrix.android.internal.auth.db.PendingSessionData | ||||
| import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams | ||||
| import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse | ||||
| import im.vector.matrix.android.internal.auth.registration.RegisterAddThreePidTask | ||||
| import im.vector.matrix.android.internal.network.RetrofitFactory | ||||
| import im.vector.matrix.android.internal.network.executeRequest | ||||
| import im.vector.matrix.android.internal.task.launchToCallback | ||||
| import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.withContext | ||||
| import okhttp3.OkHttpClient | ||||
| 
 | ||||
| internal class DefaultLoginWizard( | ||||
|         okHttpClient: Lazy<OkHttpClient>, | ||||
|         retrofitFactory: RetrofitFactory, | ||||
|         private val coroutineDispatchers: MatrixCoroutineDispatchers, | ||||
|         private val sessionCreator: SessionCreator, | ||||
|         private val pendingSessionStore: PendingSessionStore | ||||
| ) : LoginWizard { | ||||
| 
 | ||||
|     private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") | ||||
| 
 | ||||
|     private val authAPI = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString()) | ||||
|             .create(AuthAPI::class.java) | ||||
| 
 | ||||
|     override fun login(login: String, | ||||
|                        password: String, | ||||
|                        deviceName: String, | ||||
|                        callback: MatrixCallback<Session>): Cancelable { | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             loginInternal(login, password, deviceName) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun loginInternal(login: String, | ||||
|                                       password: String, | ||||
|                                       deviceName: String) = withContext(coroutineDispatchers.computation) { | ||||
|         val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) { | ||||
|             PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName) | ||||
|         } else { | ||||
|             PasswordLoginParams.userIdentifier(login, password, deviceName) | ||||
|         } | ||||
|         val credentials = executeRequest<Credentials> { | ||||
|             apiCall = authAPI.login(loginParams) | ||||
|         } | ||||
| 
 | ||||
|         sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) | ||||
|     } | ||||
| 
 | ||||
|     override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable { | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             resetPasswordInternal(email, newPassword) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun resetPasswordInternal(email: String, newPassword: String) { | ||||
|         val param = RegisterAddThreePidTask.Params( | ||||
|                 RegisterThreePid.Email(email), | ||||
|                 pendingSessionData.clientSecret, | ||||
|                 pendingSessionData.sendAttempt | ||||
|         ) | ||||
| 
 | ||||
|         pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) | ||||
|                 .also { pendingSessionStore.savePendingSessionData(it) } | ||||
| 
 | ||||
|         val result = executeRequest<AddThreePidRegistrationResponse> { | ||||
|             apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param)) | ||||
|         } | ||||
| 
 | ||||
|         pendingSessionData = pendingSessionData.copy(resetPasswordData = ResetPasswordData(newPassword, result)) | ||||
|                 .also { pendingSessionStore.savePendingSessionData(it) } | ||||
|     } | ||||
| 
 | ||||
|     override fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable { | ||||
|         val safeResetPasswordData = pendingSessionData.resetPasswordData ?: run { | ||||
|             callback.onFailure(IllegalStateException("developer error, no reset password in progress")) | ||||
|             return NoOpCancellable | ||||
|         } | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             resetPasswordMailConfirmedInternal(safeResetPasswordData) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun resetPasswordMailConfirmedInternal(resetPasswordData: ResetPasswordData) { | ||||
|         val param = ResetPasswordMailConfirmed.create( | ||||
|                 pendingSessionData.clientSecret, | ||||
|                 resetPasswordData.addThreePidRegistrationResponse.sid, | ||||
|                 resetPasswordData.newPassword | ||||
|         ) | ||||
| 
 | ||||
|         executeRequest<Unit> { | ||||
|             apiCall = authAPI.resetPasswordMailConfirmed(param) | ||||
|         } | ||||
| 
 | ||||
|         // Set to null? | ||||
|         // resetPasswordData = null | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,29 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.login | ||||
| 
 | ||||
| import com.squareup.moshi.JsonClass | ||||
| import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse | ||||
| 
 | ||||
| /** | ||||
|  * Container to store the data when a reset password is in the email validation step | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| internal data class ResetPasswordData( | ||||
|         val newPassword: String, | ||||
|         val addThreePidRegistrationResponse: AddThreePidRegistrationResponse | ||||
| ) | ||||
| @ -0,0 +1,45 @@ | ||||
| /* | ||||
|  * Copyright 2014 OpenMarket Ltd | ||||
|  * Copyright 2017 Vector Creations Ltd | ||||
|  * Copyright 2018 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package im.vector.matrix.android.internal.auth.login | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| import im.vector.matrix.android.internal.auth.registration.AuthParams | ||||
| 
 | ||||
| /** | ||||
|  * Class to pass parameters to reset the password once a email has been validated. | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| internal data class ResetPasswordMailConfirmed( | ||||
|         // authentication parameters | ||||
|         @Json(name = "auth") | ||||
|         val auth: AuthParams? = null, | ||||
| 
 | ||||
|         // the new password | ||||
|         @Json(name = "new_password") | ||||
|         val newPassword: String? = null | ||||
| ) { | ||||
|     companion object { | ||||
|         fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed { | ||||
|             return ResetPasswordMailConfirmed( | ||||
|                     auth = AuthParams.createForResetPassword(clientSecret, sid), | ||||
|                     newPassword = newPassword | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,101 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| import im.vector.matrix.android.api.auth.registration.RegisterThreePid | ||||
| 
 | ||||
| /** | ||||
|  * Add a three Pid during authentication | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| internal data class AddThreePidRegistrationParams( | ||||
|         /** | ||||
|          * Required. A unique string generated by the client, and used to identify the validation attempt. | ||||
|          * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed 255 characters and it must not be empty. | ||||
|          */ | ||||
|         @Json(name = "client_secret") | ||||
|         val clientSecret: String, | ||||
| 
 | ||||
|         /** | ||||
|          * Required. The server will only send an email if the send_attempt is a number greater than the most recent one which it has seen, | ||||
|          * scoped to that email + client_secret pair. This is to avoid repeatedly sending the same email in the case of request retries between | ||||
|          * the POSTing user and the identity server. The client should increment this value if they desire a new email (e.g. a reminder) to be sent. | ||||
|          * If they do not, the server should respond with success but not resend the email. | ||||
|          */ | ||||
|         @Json(name = "send_attempt") | ||||
|         val sendAttempt: Int, | ||||
| 
 | ||||
|         /** | ||||
|          * Optional. When the validation is completed, the identity server will redirect the user to this URL. This option is ignored when | ||||
|          * submitting 3PID validation information through a POST request. | ||||
|          */ | ||||
|         @Json(name = "next_link") | ||||
|         val nextLink: String? = null, | ||||
| 
 | ||||
|         /** | ||||
|          * Required. The hostname of the identity server to communicate with. May optionally include a port. | ||||
|          * This parameter is ignored when the homeserver handles 3PID verification. | ||||
|          */ | ||||
|         @Json(name = "id_server") | ||||
|         val id_server: String? = null, | ||||
| 
 | ||||
|         /* ========================================================================================== | ||||
|          * For emails | ||||
|          * ========================================================================================== */ | ||||
| 
 | ||||
|         /** | ||||
|          * Required. The email address to validate. | ||||
|          */ | ||||
|         @Json(name = "email") | ||||
|         val email: String? = null, | ||||
| 
 | ||||
|         /* ========================================================================================== | ||||
|          * For Msisdn | ||||
|          * ========================================================================================== */ | ||||
| 
 | ||||
|         /** | ||||
|          * Required. The two-letter uppercase ISO country code that the number in phone_number should be parsed as if it were dialled from. | ||||
|          */ | ||||
|         @Json(name = "country") | ||||
|         val countryCode: String? = null, | ||||
| 
 | ||||
|         /** | ||||
|          * Required. The phone number to validate. | ||||
|          */ | ||||
|         @Json(name = "phone_number") | ||||
|         val msisdn: String? = null | ||||
| ) { | ||||
|     companion object { | ||||
|         fun from(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationParams { | ||||
|             return when (params.threePid) { | ||||
|                 is RegisterThreePid.Email  -> AddThreePidRegistrationParams( | ||||
|                         email = params.threePid.email, | ||||
|                         clientSecret = params.clientSecret, | ||||
|                         sendAttempt = params.sendAttempt | ||||
|                 ) | ||||
|                 is RegisterThreePid.Msisdn -> AddThreePidRegistrationParams( | ||||
|                         msisdn = params.threePid.msisdn, | ||||
|                         countryCode = params.threePid.countryCode, | ||||
|                         clientSecret = params.clientSecret, | ||||
|                         sendAttempt = params.sendAttempt | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,54 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| 
 | ||||
| @JsonClass(generateAdapter = true) | ||||
| internal data class AddThreePidRegistrationResponse( | ||||
|         /** | ||||
|          * Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-]. | ||||
|          * Their length must not exceed 255 characters and they must not be empty. | ||||
|          */ | ||||
|         @Json(name = "sid") | ||||
|         val sid: String, | ||||
| 
 | ||||
|         /** | ||||
|          * An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity | ||||
|          * Service API's POST /validate/email/submitToken endpoint. The homeserver must send this token to the user (if applicable), | ||||
|          * who should then be prompted to provide it to the client. | ||||
|          * | ||||
|          * If this field is not present, the client can assume that verification will happen without the client's involvement provided | ||||
|          * the homeserver advertises this specification version in the /versions response (ie: r0.5.0). | ||||
|          */ | ||||
|         @Json(name = "submit_url") | ||||
|         val submitUrl: String? = null, | ||||
| 
 | ||||
|         /* ========================================================================================== | ||||
|          * It seems that the homeserver is sending more data, we may need it | ||||
|          * ========================================================================================== */ | ||||
| 
 | ||||
|         @Json(name = "msisdn") | ||||
|         val msisdn: String? = null, | ||||
| 
 | ||||
|         @Json(name = "intl_fmt") | ||||
|         val formattedMsisdn: String? = null, | ||||
| 
 | ||||
|         @Json(name = "success") | ||||
|         val success: Boolean? = null | ||||
| ) | ||||
| @ -0,0 +1,102 @@ | ||||
| /* | ||||
|  * Copyright 2018 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| import im.vector.matrix.android.internal.auth.data.LoginFlowTypes | ||||
| 
 | ||||
| /** | ||||
|  * Open class, parent to all possible authentication parameters | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| internal data class AuthParams( | ||||
|         @Json(name = "type") | ||||
|         val type: String, | ||||
| 
 | ||||
|         /** | ||||
|          * Note: session can be null for reset password request | ||||
|          */ | ||||
|         @Json(name = "session") | ||||
|         val session: String?, | ||||
| 
 | ||||
|         /** | ||||
|          * parameter for "m.login.recaptcha" type | ||||
|          */ | ||||
|         @Json(name = "response") | ||||
|         val captchaResponse: String? = null, | ||||
| 
 | ||||
|         /** | ||||
|          * parameter for "m.login.email.identity" type | ||||
|          */ | ||||
|         @Json(name = "threepid_creds") | ||||
|         val threePidCredentials: ThreePidCredentials? = null | ||||
| ) { | ||||
| 
 | ||||
|     companion object { | ||||
|         fun createForCaptcha(session: String, captchaResponse: String): AuthParams { | ||||
|             return AuthParams( | ||||
|                     type = LoginFlowTypes.RECAPTCHA, | ||||
|                     session = session, | ||||
|                     captchaResponse = captchaResponse | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         fun createForEmailIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams { | ||||
|             return AuthParams( | ||||
|                     type = LoginFlowTypes.EMAIL_IDENTITY, | ||||
|                     session = session, | ||||
|                     threePidCredentials = threePidCredentials | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Note that there is a bug in Synapse (I have to investigate where), but if we pass LoginFlowTypes.MSISDN, | ||||
|          * the homeserver answer with the login flow with MatrixError fields and not with a simple MatrixError 401. | ||||
|          */ | ||||
|         fun createForMsisdnIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams { | ||||
|             return AuthParams( | ||||
|                     type = LoginFlowTypes.MSISDN, | ||||
|                     session = session, | ||||
|                     threePidCredentials = threePidCredentials | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         fun createForResetPassword(clientSecret: String, sid: String): AuthParams { | ||||
|             return AuthParams( | ||||
|                     type = LoginFlowTypes.EMAIL_IDENTITY, | ||||
|                     session = null, | ||||
|                     threePidCredentials = ThreePidCredentials( | ||||
|                             clientSecret = clientSecret, | ||||
|                             sid = sid | ||||
|                     ) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @JsonClass(generateAdapter = true) | ||||
| data class ThreePidCredentials( | ||||
|         @Json(name = "client_secret") | ||||
|         val clientSecret: String? = null, | ||||
| 
 | ||||
|         @Json(name = "id_server") | ||||
|         val idServer: String? = null, | ||||
| 
 | ||||
|         @Json(name = "sid") | ||||
|         val sid: String? = null | ||||
| ) | ||||
| @ -0,0 +1,246 @@ | ||||
| /* | ||||
|  * Copyright 2018 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import dagger.Lazy | ||||
| import im.vector.matrix.android.api.MatrixCallback | ||||
| import im.vector.matrix.android.api.auth.registration.RegisterThreePid | ||||
| import im.vector.matrix.android.api.auth.registration.RegistrationResult | ||||
| import im.vector.matrix.android.api.auth.registration.RegistrationWizard | ||||
| import im.vector.matrix.android.api.failure.Failure | ||||
| import im.vector.matrix.android.api.failure.Failure.RegistrationFlowError | ||||
| import im.vector.matrix.android.api.util.Cancelable | ||||
| import im.vector.matrix.android.api.util.NoOpCancellable | ||||
| import im.vector.matrix.android.internal.auth.AuthAPI | ||||
| import im.vector.matrix.android.internal.auth.PendingSessionStore | ||||
| import im.vector.matrix.android.internal.auth.SessionCreator | ||||
| import im.vector.matrix.android.internal.auth.data.LoginFlowTypes | ||||
| import im.vector.matrix.android.internal.auth.db.PendingSessionData | ||||
| import im.vector.matrix.android.internal.network.RetrofitFactory | ||||
| import im.vector.matrix.android.internal.task.launchToCallback | ||||
| import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.delay | ||||
| import okhttp3.OkHttpClient | ||||
| 
 | ||||
| /** | ||||
|  * This class execute the registration request and is responsible to keep the session of interactive authentication | ||||
|  */ | ||||
| internal class DefaultRegistrationWizard( | ||||
|         private val okHttpClient: Lazy<OkHttpClient>, | ||||
|         private val retrofitFactory: RetrofitFactory, | ||||
|         private val coroutineDispatchers: MatrixCoroutineDispatchers, | ||||
|         private val sessionCreator: SessionCreator, | ||||
|         private val pendingSessionStore: PendingSessionStore | ||||
| ) : RegistrationWizard { | ||||
| 
 | ||||
|     private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") | ||||
| 
 | ||||
|     private val authAPI = buildAuthAPI() | ||||
|     private val registerTask = DefaultRegisterTask(authAPI) | ||||
|     private val registerAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI) | ||||
|     private val validateCodeTask = DefaultValidateCodeTask(authAPI) | ||||
| 
 | ||||
|     override val currentThreePid: String? | ||||
|         get() { | ||||
|             return when (val threePid = pendingSessionData.currentThreePidData?.threePid) { | ||||
|                 is RegisterThreePid.Email  -> threePid.email | ||||
|                 is RegisterThreePid.Msisdn -> { | ||||
|                     // Take formatted msisdn if provided by the server | ||||
|                     pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn | ||||
|                 } | ||||
|                 null                       -> null | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     override val isRegistrationStarted: Boolean | ||||
|         get() = pendingSessionData.isRegistrationStarted | ||||
| 
 | ||||
|     override fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable { | ||||
|         val params = RegistrationParams() | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             performRegistrationRequest(params) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun createAccount(userName: String, | ||||
|                                password: String, | ||||
|                                initialDeviceDisplayName: String?, | ||||
|                                callback: MatrixCallback<RegistrationResult>): Cancelable { | ||||
|         val params = RegistrationParams( | ||||
|                 username = userName, | ||||
|                 password = password, | ||||
|                 initialDeviceDisplayName = initialDeviceDisplayName | ||||
|         ) | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             performRegistrationRequest(params) | ||||
|                     .also { | ||||
|                         pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) | ||||
|                                 .also { pendingSessionStore.savePendingSessionData(it) } | ||||
|                     } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun performReCaptcha(response: String, callback: MatrixCallback<RegistrationResult>): Cancelable { | ||||
|         val safeSession = pendingSessionData.currentSession ?: run { | ||||
|             callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) | ||||
|             return NoOpCancellable | ||||
|         } | ||||
|         val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             performRegistrationRequest(params) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable { | ||||
|         val safeSession = pendingSessionData.currentSession ?: run { | ||||
|             callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) | ||||
|             return NoOpCancellable | ||||
|         } | ||||
|         val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             performRegistrationRequest(params) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable { | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             pendingSessionData = pendingSessionData.copy(currentThreePidData = null) | ||||
|                     .also { pendingSessionStore.savePendingSessionData(it) } | ||||
| 
 | ||||
|             sendThreePid(threePid) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun sendAgainThreePid(callback: MatrixCallback<RegistrationResult>): Cancelable { | ||||
|         val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid ?: run { | ||||
|             callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) | ||||
|             return NoOpCancellable | ||||
|         } | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             sendThreePid(safeCurrentThreePid) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun sendThreePid(threePid: RegisterThreePid): RegistrationResult { | ||||
|         val safeSession = pendingSessionData.currentSession ?: throw IllegalStateException("developer error, call createAccount() method first") | ||||
|         val response = registerAddThreePidTask.execute( | ||||
|                 RegisterAddThreePidTask.Params( | ||||
|                         threePid, | ||||
|                         pendingSessionData.clientSecret, | ||||
|                         pendingSessionData.sendAttempt)) | ||||
| 
 | ||||
|         pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) | ||||
|                 .also { pendingSessionStore.savePendingSessionData(it) } | ||||
| 
 | ||||
|         val params = RegistrationParams( | ||||
|                 auth = if (threePid is RegisterThreePid.Email) { | ||||
|                     AuthParams.createForEmailIdentity(safeSession, | ||||
|                             ThreePidCredentials( | ||||
|                                     clientSecret = pendingSessionData.clientSecret, | ||||
|                                     sid = response.sid | ||||
|                             ) | ||||
|                     ) | ||||
|                 } else { | ||||
|                     AuthParams.createForMsisdnIdentity(safeSession, | ||||
|                             ThreePidCredentials( | ||||
|                                     clientSecret = pendingSessionData.clientSecret, | ||||
|                                     sid = response.sid | ||||
|                             ) | ||||
|                     ) | ||||
|                 } | ||||
|         ) | ||||
|         // Store data | ||||
|         pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params)) | ||||
|                 .also { pendingSessionStore.savePendingSessionData(it) } | ||||
| 
 | ||||
|         // and send the sid a first time | ||||
|         return performRegistrationRequest(params) | ||||
|     } | ||||
| 
 | ||||
|     override fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback<RegistrationResult>): Cancelable { | ||||
|         val safeParam = pendingSessionData.currentThreePidData?.registrationParams ?: run { | ||||
|             callback.onFailure(IllegalStateException("developer error, no pending three pid")) | ||||
|             return NoOpCancellable | ||||
|         } | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             performRegistrationRequest(safeParam, delayMillis) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable { | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             validateThreePid(code) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun validateThreePid(code: String): RegistrationResult { | ||||
|         val registrationParams = pendingSessionData.currentThreePidData?.registrationParams | ||||
|                 ?: throw IllegalStateException("developer error, no pending three pid") | ||||
|         val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first") | ||||
|         val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code") | ||||
|         val validationBody = ValidationCodeBody( | ||||
|                 clientSecret = pendingSessionData.clientSecret, | ||||
|                 sid = safeCurrentData.addThreePidRegistrationResponse.sid, | ||||
|                 code = code | ||||
|         ) | ||||
|         val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) | ||||
|         if (validationResponse.success == true) { | ||||
|             // The entered code is correct | ||||
|             // Same than validate email | ||||
|             return performRegistrationRequest(registrationParams, 3_000) | ||||
|         } else { | ||||
|             // The code is not correct | ||||
|             throw Failure.SuccessError | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun dummy(callback: MatrixCallback<RegistrationResult>): Cancelable { | ||||
|         val safeSession = pendingSessionData.currentSession ?: run { | ||||
|             callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) | ||||
|             return NoOpCancellable | ||||
|         } | ||||
|         return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { | ||||
|             val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) | ||||
|             performRegistrationRequest(params) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun performRegistrationRequest(registrationParams: RegistrationParams, | ||||
|                                                    delayMillis: Long = 0): RegistrationResult { | ||||
|         delay(delayMillis) | ||||
|         val credentials = try { | ||||
|             registerTask.execute(RegisterTask.Params(registrationParams)) | ||||
|         } catch (exception: Throwable) { | ||||
|             if (exception is RegistrationFlowError) { | ||||
|                 pendingSessionData = pendingSessionData.copy(currentSession = exception.registrationFlowResponse.session) | ||||
|                         .also { pendingSessionStore.savePendingSessionData(it) } | ||||
|                 return RegistrationResult.FlowResponse(exception.registrationFlowResponse.toFlowResult()) | ||||
|             } else { | ||||
|                 throw exception | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) | ||||
|         return RegistrationResult.Success(session) | ||||
|     } | ||||
| 
 | ||||
|     private fun buildAuthAPI(): AuthAPI { | ||||
|         val retrofit = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString()) | ||||
|         return retrofit.create(AuthAPI::class.java) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,31 @@ | ||||
| /* | ||||
|  * Copyright 2018 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package org.matrix.androidsdk.rest.model.login | ||||
| 
 | ||||
| import android.os.Parcelable | ||||
| import kotlinx.android.parcel.Parcelize | ||||
| 
 | ||||
| /** | ||||
|  * This class represent a localized privacy policy for registration Flow. | ||||
|  */ | ||||
| @Parcelize | ||||
| data class LocalizedFlowDataLoginTerms( | ||||
|         var policyName: String? = null, | ||||
|         var version: String? = null, | ||||
|         var localizedUrl: String? = null, | ||||
|         var localizedName: String? = null | ||||
| ) : Parcelable | ||||
| @ -0,0 +1,47 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import im.vector.matrix.android.api.auth.registration.RegisterThreePid | ||||
| import im.vector.matrix.android.internal.auth.AuthAPI | ||||
| import im.vector.matrix.android.internal.network.executeRequest | ||||
| import im.vector.matrix.android.internal.task.Task | ||||
| 
 | ||||
| internal interface RegisterAddThreePidTask : Task<RegisterAddThreePidTask.Params, AddThreePidRegistrationResponse> { | ||||
|     data class Params( | ||||
|             val threePid: RegisterThreePid, | ||||
|             val clientSecret: String, | ||||
|             val sendAttempt: Int | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| internal class DefaultRegisterAddThreePidTask(private val authAPI: AuthAPI) | ||||
|     : RegisterAddThreePidTask { | ||||
| 
 | ||||
|     override suspend fun execute(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationResponse { | ||||
|         return executeRequest { | ||||
|             apiCall = authAPI.add3Pid(params.threePid.toPath(), AddThreePidRegistrationParams.from(params)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun RegisterThreePid.toPath(): String { | ||||
|         return when (this) { | ||||
|             is RegisterThreePid.Email  -> "email" | ||||
|             is RegisterThreePid.Msisdn -> "msisdn" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,62 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import im.vector.matrix.android.api.auth.data.Credentials | ||||
| import im.vector.matrix.android.api.failure.Failure | ||||
| import im.vector.matrix.android.internal.auth.AuthAPI | ||||
| import im.vector.matrix.android.internal.di.MoshiProvider | ||||
| import im.vector.matrix.android.internal.network.executeRequest | ||||
| import im.vector.matrix.android.internal.task.Task | ||||
| 
 | ||||
| internal interface RegisterTask : Task<RegisterTask.Params, Credentials> { | ||||
|     data class Params( | ||||
|             val registrationParams: RegistrationParams | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| internal class DefaultRegisterTask(private val authAPI: AuthAPI) | ||||
|     : RegisterTask { | ||||
| 
 | ||||
|     override suspend fun execute(params: RegisterTask.Params): Credentials { | ||||
|         try { | ||||
|             return executeRequest { | ||||
|                 apiCall = authAPI.register(params.registrationParams) | ||||
|             } | ||||
|         } catch (throwable: Throwable) { | ||||
|             if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { | ||||
|                 // Parse to get a RegistrationFlowResponse | ||||
|                 val registrationFlowResponse = try { | ||||
|                     MoshiProvider.providesMoshi() | ||||
|                             .adapter(RegistrationFlowResponse::class.java) | ||||
|                             .fromJson(throwable.errorBody) | ||||
|                 } catch (e: Exception) { | ||||
|                     null | ||||
|                 } | ||||
|                 // check if the server response can be cast | ||||
|                 if (registrationFlowResponse != null) { | ||||
|                     throw Failure.RegistrationFlowError(registrationFlowResponse) | ||||
|                 } else { | ||||
|                     throw throwable | ||||
|                 } | ||||
|             } else { | ||||
|                 // Other error | ||||
|                 throw throwable | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -18,8 +18,12 @@ package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| import im.vector.matrix.android.api.auth.registration.FlowResult | ||||
| import im.vector.matrix.android.api.auth.registration.Stage | ||||
| import im.vector.matrix.android.api.auth.registration.TermPolicies | ||||
| import im.vector.matrix.android.api.util.JsonDict | ||||
| import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow | ||||
| import im.vector.matrix.android.internal.auth.data.LoginFlowTypes | ||||
| 
 | ||||
| @JsonClass(generateAdapter = true) | ||||
| data class RegistrationFlowResponse( | ||||
| @ -50,4 +54,46 @@ data class RegistrationFlowResponse( | ||||
|          */ | ||||
|         @Json(name = "params") | ||||
|         var params: JsonDict? = null | ||||
| 
 | ||||
|         /** | ||||
|          * WARNING, | ||||
|          * The two MatrixError fields "errcode" and "error" can also be present here in case of error when validating a stage, | ||||
|          * But in this case Moshi will be able to parse the result as a MatrixError, see [RetrofitExtensions.toFailure] | ||||
|          * Ex: when polling for "m.login.msisdn" validation | ||||
|          */ | ||||
| ) | ||||
| 
 | ||||
| /** | ||||
|  * Convert to something easier to handle on client side | ||||
|  */ | ||||
| fun RegistrationFlowResponse.toFlowResult(): FlowResult { | ||||
|     // Get all the returned stages | ||||
|     val allFlowTypes = mutableSetOf<String>() | ||||
| 
 | ||||
|     val missingStage = mutableListOf<Stage>() | ||||
|     val completedStage = mutableListOf<Stage>() | ||||
| 
 | ||||
|     this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } } | ||||
| 
 | ||||
|     allFlowTypes.forEach { type -> | ||||
|         val isMandatory = flows?.all { type in it.stages ?: emptyList() } == true | ||||
| 
 | ||||
|         val stage = when (type) { | ||||
|             LoginFlowTypes.RECAPTCHA      -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) | ||||
|                     ?: "") | ||||
|             LoginFlowTypes.DUMMY          -> Stage.Dummy(isMandatory) | ||||
|             LoginFlowTypes.TERMS          -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap<String, String>()) | ||||
|             LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory) | ||||
|             LoginFlowTypes.MSISDN         -> Stage.Msisdn(isMandatory) | ||||
|             else                          -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>)) | ||||
|         } | ||||
| 
 | ||||
|         if (type in completedStages ?: emptyList()) { | ||||
|             completedStage.add(stage) | ||||
|         } else { | ||||
|             missingStage.add(stage) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return FlowResult(missingStage, completedStage) | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,47 @@ | ||||
| /* | ||||
|  * Copyright 2014 OpenMarket Ltd | ||||
|  * Copyright 2017 Vector Creations Ltd | ||||
|  * Copyright 2018 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| 
 | ||||
| /** | ||||
|  * Class to pass parameters to the different registration types for /register. | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| internal data class RegistrationParams( | ||||
|         // authentication parameters | ||||
|         @Json(name = "auth") | ||||
|         val auth: AuthParams? = null, | ||||
| 
 | ||||
|         // the account username | ||||
|         @Json(name = "username") | ||||
|         val username: String? = null, | ||||
| 
 | ||||
|         // the account password | ||||
|         @Json(name = "password") | ||||
|         val password: String? = null, | ||||
| 
 | ||||
|         // device name | ||||
|         @Json(name = "initial_device_display_name") | ||||
|         val initialDeviceDisplayName: String? = null, | ||||
| 
 | ||||
|         // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app | ||||
|         // versions to end up in fallback because the HS returns the msisdn flow which they don't support | ||||
|         val x_show_msisdn: Boolean? = null | ||||
| ) | ||||
| @ -0,0 +1,26 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| 
 | ||||
| @JsonClass(generateAdapter = true) | ||||
| data class SuccessResult( | ||||
|         @Json(name = "success") | ||||
|         val success: Boolean? | ||||
| ) | ||||
| @ -0,0 +1,54 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import com.squareup.moshi.JsonClass | ||||
| import im.vector.matrix.android.api.auth.registration.RegisterThreePid | ||||
| 
 | ||||
| /** | ||||
|  * Container to store the data when a three pid is in validation step | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| internal data class ThreePidData( | ||||
|         val email: String, | ||||
|         val msisdn: String, | ||||
|         val country: String, | ||||
|         val addThreePidRegistrationResponse: AddThreePidRegistrationResponse, | ||||
|         val registrationParams: RegistrationParams | ||||
| ) { | ||||
|     val threePid: RegisterThreePid | ||||
|         get() { | ||||
|             return if (email.isNotBlank()) { | ||||
|                 RegisterThreePid.Email(email) | ||||
|             } else { | ||||
|                 RegisterThreePid.Msisdn(msisdn, country) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     companion object { | ||||
|         fun from(threePid: RegisterThreePid, | ||||
|                  addThreePidRegistrationResponse: AddThreePidRegistrationResponse, | ||||
|                  registrationParams: RegistrationParams): ThreePidData { | ||||
|             return when (threePid) { | ||||
|                 is RegisterThreePid.Email  -> | ||||
|                     ThreePidData(threePid.email, "", "", addThreePidRegistrationResponse, registrationParams) | ||||
|                 is RegisterThreePid.Msisdn -> | ||||
|                     ThreePidData("", threePid.msisdn, threePid.countryCode, addThreePidRegistrationResponse, registrationParams) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,38 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import im.vector.matrix.android.internal.auth.AuthAPI | ||||
| import im.vector.matrix.android.internal.network.executeRequest | ||||
| import im.vector.matrix.android.internal.task.Task | ||||
| 
 | ||||
| internal interface ValidateCodeTask : Task<ValidateCodeTask.Params, SuccessResult> { | ||||
|     data class Params( | ||||
|             val url: String, | ||||
|             val body: ValidationCodeBody | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| internal class DefaultValidateCodeTask(private val authAPI: AuthAPI) | ||||
|     : ValidateCodeTask { | ||||
| 
 | ||||
|     override suspend fun execute(params: ValidateCodeTask.Params): SuccessResult { | ||||
|         return executeRequest { | ||||
|             apiCall = authAPI.validate3Pid(params.url, params.body) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,35 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.auth.registration | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| 
 | ||||
| /** | ||||
|  * This object is used to send a code received by SMS to validate Msisdn ownership | ||||
|  */ | ||||
| @JsonClass(generateAdapter = true) | ||||
| data class ValidationCodeBody( | ||||
|         @Json(name = "client_secret") | ||||
|         val clientSecret: String, | ||||
| 
 | ||||
|         @Json(name = "sid") | ||||
|         val sid: String, | ||||
| 
 | ||||
|         @Json(name = "token") | ||||
|         val code: String | ||||
| ) | ||||
| @ -22,7 +22,7 @@ import com.squareup.moshi.Moshi | ||||
| import dagger.BindsInstance | ||||
| import dagger.Component | ||||
| import im.vector.matrix.android.api.Matrix | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.matrix.android.api.auth.AuthenticationService | ||||
| import im.vector.matrix.android.internal.SessionManager | ||||
| import im.vector.matrix.android.internal.auth.AuthModule | ||||
| import im.vector.matrix.android.internal.auth.SessionParamsStore | ||||
| @ -44,7 +44,7 @@ internal interface MatrixComponent { | ||||
|     @Unauthenticated | ||||
|     fun okHttpClient(): OkHttpClient | ||||
| 
 | ||||
|     fun authenticator(): Authenticator | ||||
|     fun authenticationService(): AuthenticationService | ||||
| 
 | ||||
|     fun context(): Context | ||||
| 
 | ||||
|  | ||||
| @ -19,6 +19,7 @@ package im.vector.matrix.android.internal.network | ||||
| internal object NetworkConstants { | ||||
| 
 | ||||
|     private const val URI_API_PREFIX_PATH = "_matrix/client" | ||||
|     const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/" | ||||
|     const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" | ||||
|     const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" | ||||
| 
 | ||||
|  | ||||
| @ -26,6 +26,7 @@ import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressServi | ||||
| import im.vector.matrix.android.internal.session.filter.FilterRepository | ||||
| import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabilitiesTask | ||||
| import im.vector.matrix.android.internal.session.sync.model.SyncResponse | ||||
| import im.vector.matrix.android.internal.session.user.UserStore | ||||
| import im.vector.matrix.android.internal.task.Task | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @ -41,7 +42,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, | ||||
|                                                    private val sessionParamsStore: SessionParamsStore, | ||||
|                                                    private val initialSyncProgressService: DefaultInitialSyncProgressService, | ||||
|                                                    private val syncTokenStore: SyncTokenStore, | ||||
|                                                    private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask | ||||
|                                                    private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, | ||||
|                                                    private val userStore: UserStore | ||||
| ) : SyncTask { | ||||
| 
 | ||||
|     override suspend fun execute(params: SyncTask.Params) { | ||||
| @ -60,6 +62,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, | ||||
| 
 | ||||
|         val isInitialSync = token == null | ||||
|         if (isInitialSync) { | ||||
|             // We might want to get the user information in parallel too | ||||
|             userStore.createOrUpdate(userId) | ||||
|             initialSyncProgressService.endAll() | ||||
|             initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) | ||||
|         } | ||||
|  | ||||
| @ -53,4 +53,7 @@ internal abstract class UserModule { | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun bindUserStore(userStore: RealmUserStore): UserStore | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,36 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.session.user | ||||
| 
 | ||||
| import com.zhuinden.monarchy.Monarchy | ||||
| import im.vector.matrix.android.internal.database.model.UserEntity | ||||
| import im.vector.matrix.android.internal.util.awaitTransaction | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| internal interface UserStore { | ||||
|     suspend fun createOrUpdate(userId: String, displayName: String? = null, avatarUrl: String? = null) | ||||
| } | ||||
| 
 | ||||
| internal class RealmUserStore @Inject constructor(private val monarchy: Monarchy) : UserStore { | ||||
| 
 | ||||
|     override suspend fun createOrUpdate(userId: String, displayName: String?, avatarUrl: String?) { | ||||
|         monarchy.awaitTransaction { | ||||
|             val userEntity = UserEntity(userId, displayName ?: "", avatarUrl ?: "") | ||||
|             it.insertOrUpdate(userEntity) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,38 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.matrix.android.internal.task | ||||
| 
 | ||||
| import im.vector.matrix.android.api.MatrixCallback | ||||
| import im.vector.matrix.android.api.util.Cancelable | ||||
| import im.vector.matrix.android.internal.extensions.foldToCallback | ||||
| import im.vector.matrix.android.internal.util.toCancelable | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.CoroutineStart | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlin.coroutines.CoroutineContext | ||||
| import kotlin.coroutines.EmptyCoroutineContext | ||||
| 
 | ||||
| internal fun <T> CoroutineScope.launchToCallback( | ||||
|         context: CoroutineContext = EmptyCoroutineContext, | ||||
|         callback: MatrixCallback<T>, | ||||
|         block: suspend () -> T | ||||
| ): Cancelable = launch(context, CoroutineStart.DEFAULT) { | ||||
|     val result = runCatching { | ||||
|         block() | ||||
|     } | ||||
|     result.foldToCallback(callback) | ||||
| }.toCancelable() | ||||
| @ -20,8 +20,8 @@ import im.vector.matrix.android.api.util.Cancelable | ||||
| import im.vector.matrix.android.internal.di.MatrixScope | ||||
| import im.vector.matrix.android.internal.extensions.foldToCallback | ||||
| import im.vector.matrix.android.internal.network.NetworkConnectivityChecker | ||||
| import im.vector.matrix.android.internal.util.CancelableCoroutine | ||||
| import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers | ||||
| import im.vector.matrix.android.internal.util.toCancelable | ||||
| import kotlinx.coroutines.* | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| @ -34,27 +34,28 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers | ||||
|     private val executorScope = CoroutineScope(SupervisorJob()) | ||||
| 
 | ||||
|     fun <PARAMS, RESULT> execute(task: ConfigurableTask<PARAMS, RESULT>): Cancelable { | ||||
|         val job = executorScope.launch(task.callbackThread.toDispatcher()) { | ||||
|             val resultOrFailure = runCatching { | ||||
|                 withContext(task.executionThread.toDispatcher()) { | ||||
|                     Timber.v("Enqueue task $task") | ||||
|                     retry(task.retryCount) { | ||||
|                         if (task.constraints.connectedToNetwork) { | ||||
|                             Timber.v("Waiting network for $task") | ||||
|                             networkConnectivityChecker.waitUntilConnected() | ||||
|         return executorScope | ||||
|                 .launch(task.callbackThread.toDispatcher()) { | ||||
|                     val resultOrFailure = runCatching { | ||||
|                         withContext(task.executionThread.toDispatcher()) { | ||||
|                             Timber.v("Enqueue task $task") | ||||
|                             retry(task.retryCount) { | ||||
|                                 if (task.constraints.connectedToNetwork) { | ||||
|                                     Timber.v("Waiting network for $task") | ||||
|                                     networkConnectivityChecker.waitUntilConnected() | ||||
|                                 } | ||||
|                                 Timber.v("Execute task $task on ${Thread.currentThread().name}") | ||||
|                                 task.execute(task.params) | ||||
|                             } | ||||
|                         } | ||||
|                         Timber.v("Execute task $task on ${Thread.currentThread().name}") | ||||
|                         task.execute(task.params) | ||||
|                     } | ||||
|                     resultOrFailure | ||||
|                             .onFailure { | ||||
|                                 Timber.d(it, "Task failed") | ||||
|                             } | ||||
|                             .foldToCallback(task.callback) | ||||
|                 } | ||||
|             } | ||||
|             resultOrFailure | ||||
|                     .onFailure { | ||||
|                         Timber.d(it, "Task failed") | ||||
|                     } | ||||
|                     .foldToCallback(task.callback) | ||||
|         } | ||||
|         return CancelableCoroutine(job) | ||||
|                 .toCancelable() | ||||
|     } | ||||
| 
 | ||||
|     fun cancelAll() = executorScope.coroutineContext.cancelChildren() | ||||
|  | ||||
| @ -19,7 +19,14 @@ package im.vector.matrix.android.internal.util | ||||
| import im.vector.matrix.android.api.util.Cancelable | ||||
| import kotlinx.coroutines.Job | ||||
| 
 | ||||
| internal class CancelableCoroutine(private val job: Job) : Cancelable { | ||||
| internal fun Job.toCancelable(): Cancelable { | ||||
|     return CancelableCoroutine(this) | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Private, use the extension above | ||||
|  */ | ||||
| private class CancelableCoroutine(private val job: Job) : Cancelable { | ||||
| 
 | ||||
|     override fun cancel() { | ||||
|         if (!job.isCancelled) { | ||||
|  | ||||
| @ -225,6 +225,7 @@ dependencies { | ||||
|     def glide_version = '4.10.0' | ||||
|     def moshi_version = '1.8.0' | ||||
|     def daggerVersion = '2.24' | ||||
|     def autofill_version = "1.0.0-rc01" | ||||
| 
 | ||||
|     implementation project(":matrix-sdk-android") | ||||
|     implementation project(":matrix-sdk-android-rx") | ||||
| @ -256,6 +257,9 @@ dependencies { | ||||
|     // Debug | ||||
|     implementation 'com.facebook.stetho:stetho:1.5.1' | ||||
| 
 | ||||
|     // Phone number https://github.com/google/libphonenumber | ||||
|     implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23' | ||||
| 
 | ||||
|     // rx | ||||
|     implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' | ||||
|     implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' | ||||
| @ -290,6 +294,7 @@ dependencies { | ||||
|     implementation "io.noties.markwon:html:$markwon_version" | ||||
|     implementation 'me.saket:better-link-movement-method:2.2.0' | ||||
|     implementation 'com.google.android:flexbox:1.1.1' | ||||
|     implementation "androidx.autofill:autofill:$autofill_version" | ||||
| 
 | ||||
|     // Passphrase strength helper | ||||
|     implementation 'com.nulab-inc:zxcvbn:1.2.7' | ||||
|  | ||||
| @ -33,7 +33,9 @@ | ||||
|         </activity-alias> | ||||
| 
 | ||||
|         <activity android:name=".features.home.HomeActivity" /> | ||||
|         <activity android:name=".features.login.LoginActivity" /> | ||||
|         <activity | ||||
|             android:name=".features.login.LoginActivity" | ||||
|             android:windowSoftInputMode="adjustResize" /> | ||||
|         <activity android:name=".features.media.ImageMediaViewerActivity" /> | ||||
|         <activity | ||||
|             android:name=".features.rageshake.BugReportActivity" | ||||
|  | ||||
							
								
								
									
										1
									
								
								vector/src/main/assets/onLogin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								vector/src/main/assets/onLogin.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| javascript:window.matrixLogin.onLogin = function(response) { sendObjectMessage({ 'action': 'onLogin', 'credentials': response }); }; | ||||
							
								
								
									
										1
									
								
								vector/src/main/assets/onRegistered.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								vector/src/main/assets/onRegistered.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| javascript:window.matrixRegistration.onRegistered = function(homeserverUrl, userId, accessToken) { sendObjectMessage({ 'action': 'onRegistered', 'homeServer': homeserverUrl, 'userId': userId, 'accessToken': accessToken }); } | ||||
							
								
								
									
										22
									
								
								vector/src/main/assets/reCaptchaPage.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								vector/src/main/assets/reCaptchaPage.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <script type="text/javascript"> | ||||
|       var verifyCallback = function(response) { | ||||
|         var iframe = document.createElement('iframe'); | ||||
|         iframe.setAttribute('src', 'js:' + JSON.stringify({'action': 'verifyCallback', 'response': response})); | ||||
|         document.documentElement.appendChild(iframe); | ||||
|         iframe.parentNode.removeChild(iframe); | ||||
|         iframe = null; | ||||
|       }; | ||||
| 
 | ||||
|       var onloadCallback = function() { | ||||
|         grecaptcha.render('recaptcha_widget', { 'sitekey' : '%s', 'callback': verifyCallback }); | ||||
|       }; | ||||
|     </script> | ||||
| </head> | ||||
| <body> | ||||
| <div id="recaptcha_widget"></div> | ||||
| <script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										1
									
								
								vector/src/main/assets/sendObject.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								vector/src/main/assets/sendObject.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| javascript:window.sendObjectMessage = function(parameters) { var iframe = document.createElement('iframe'); iframe.setAttribute('src', 'js:' + JSON.stringify(parameters)); document.documentElement.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null;}; | ||||
| @ -36,7 +36,7 @@ import com.github.piasy.biv.BigImageViewer | ||||
| import com.github.piasy.biv.loader.glide.GlideImageLoader | ||||
| import im.vector.matrix.android.api.Matrix | ||||
| import im.vector.matrix.android.api.MatrixConfiguration | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.matrix.android.api.auth.AuthenticationService | ||||
| import im.vector.riotx.core.di.ActiveSessionHolder | ||||
| import im.vector.riotx.core.di.DaggerVectorComponent | ||||
| import im.vector.riotx.core.di.HasVectorInjector | ||||
| @ -63,7 +63,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. | ||||
| 
 | ||||
|     lateinit var appContext: Context | ||||
|     // font thread handler | ||||
|     @Inject lateinit var authenticator: Authenticator | ||||
|     @Inject lateinit var authenticationService: AuthenticationService | ||||
|     @Inject lateinit var vectorConfiguration: VectorConfiguration | ||||
|     @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider | ||||
|     @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper | ||||
| @ -115,8 +115,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. | ||||
|         emojiCompatWrapper.init(fontRequest) | ||||
| 
 | ||||
|         notificationUtils.createNotificationChannels() | ||||
|         if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { | ||||
|             val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!! | ||||
|         if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { | ||||
|             val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! | ||||
|             activeSessionHolder.setActiveSession(lastAuthenticatedSession) | ||||
|             lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener, sessionListener) | ||||
|         } | ||||
|  | ||||
| @ -17,7 +17,7 @@ | ||||
| package im.vector.riotx.core.di | ||||
| 
 | ||||
| import arrow.core.Option | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.matrix.android.api.auth.AuthenticationService | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| import im.vector.riotx.ActiveSessionDataSource | ||||
| import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler | ||||
| @ -27,7 +27,7 @@ import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| @Singleton | ||||
| class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator, | ||||
| class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService, | ||||
|                                               private val sessionObservableStore: ActiveSessionDataSource, | ||||
|                                               private val keyRequestHandler: KeyRequestHandler, | ||||
|                                               private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler | ||||
| @ -64,7 +64,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent | ||||
| 
 | ||||
|     // TODO: Stop sync ? | ||||
| //    fun switchToSession(sessionParams: SessionParams) { | ||||
| //        val newActiveSession = authenticator.getSession(sessionParams) | ||||
| //        val newActiveSession = authenticationService.getSession(sessionParams) | ||||
| //        activeSession.set(newActiveSession) | ||||
| //    } | ||||
| } | ||||
|  | ||||
| @ -35,8 +35,8 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag | ||||
| import im.vector.riotx.features.home.group.GroupListFragment | ||||
| import im.vector.riotx.features.home.room.detail.RoomDetailFragment | ||||
| import im.vector.riotx.features.home.room.list.RoomListFragment | ||||
| import im.vector.riotx.features.login.LoginFragment | ||||
| import im.vector.riotx.features.login.LoginSsoFallbackFragment | ||||
| import im.vector.riotx.features.login.* | ||||
| import im.vector.riotx.features.login.terms.LoginTermsFragment | ||||
| import im.vector.riotx.features.reactions.EmojiSearchResultFragment | ||||
| import im.vector.riotx.features.roomdirectory.PublicRoomsFragment | ||||
| import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment | ||||
| @ -117,8 +117,63 @@ interface FragmentModule { | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginSsoFallbackFragment::class) | ||||
|     fun bindLoginSsoFallbackFragment(fragment: LoginSsoFallbackFragment): Fragment | ||||
|     @FragmentKey(LoginCaptchaFragment::class) | ||||
|     fun bindLoginCaptchaFragment(fragment: LoginCaptchaFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginTermsFragment::class) | ||||
|     fun bindLoginTermsFragment(fragment: LoginTermsFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginServerUrlFormFragment::class) | ||||
|     fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginResetPasswordMailConfirmationFragment::class) | ||||
|     fun bindLoginResetPasswordMailConfirmationFragment(fragment: LoginResetPasswordMailConfirmationFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginResetPasswordFragment::class) | ||||
|     fun bindLoginResetPasswordFragment(fragment: LoginResetPasswordFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginResetPasswordSuccessFragment::class) | ||||
|     fun bindLoginResetPasswordSuccessFragment(fragment: LoginResetPasswordSuccessFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginServerSelectionFragment::class) | ||||
|     fun bindLoginServerSelectionFragment(fragment: LoginServerSelectionFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginSignUpSignInSelectionFragment::class) | ||||
|     fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginSplashFragment::class) | ||||
|     fun bindLoginSplashFragment(fragment: LoginSplashFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginWebFragment::class) | ||||
|     fun bindLoginWebFragment(fragment: LoginWebFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginGenericTextInputFormFragment::class) | ||||
|     fun bindLoginGenericTextInputFormFragment(fragment: LoginGenericTextInputFormFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @FragmentKey(LoginWaitForEmailFragment::class) | ||||
|     fun bindLoginWaitForEmailFragment(fragment: LoginWaitForEmailFragment): Fragment | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|  | ||||
| @ -32,8 +32,8 @@ import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsB | ||||
| import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet | ||||
| import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet | ||||
| import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity | ||||
| import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet | ||||
| import im.vector.riotx.features.home.room.list.RoomListModule | ||||
| import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet | ||||
| import im.vector.riotx.features.invite.VectorInviteView | ||||
| import im.vector.riotx.features.link.LinkHandlerActivity | ||||
| import im.vector.riotx.features.login.LoginActivity | ||||
| @ -47,7 +47,7 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity | ||||
| import im.vector.riotx.features.reactions.widget.ReactionButton | ||||
| import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity | ||||
| import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity | ||||
| import im.vector.riotx.features.settings.* | ||||
| import im.vector.riotx.features.settings.VectorSettingsActivity | ||||
| import im.vector.riotx.features.share.IncomingShareActivity | ||||
| import im.vector.riotx.features.ui.UiStateRepository | ||||
| 
 | ||||
|  | ||||
| @ -21,13 +21,14 @@ import android.content.res.Resources | ||||
| import dagger.BindsInstance | ||||
| import dagger.Component | ||||
| import im.vector.matrix.android.api.Matrix | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.matrix.android.api.auth.AuthenticationService | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| import im.vector.riotx.ActiveSessionDataSource | ||||
| import im.vector.riotx.EmojiCompatFontProvider | ||||
| import im.vector.riotx.EmojiCompatWrapper | ||||
| import im.vector.riotx.VectorApplication | ||||
| import im.vector.riotx.core.pushers.PushersManager | ||||
| import im.vector.riotx.core.utils.AssetReader | ||||
| import im.vector.riotx.core.utils.DimensionConverter | ||||
| import im.vector.riotx.features.configuration.VectorConfiguration | ||||
| import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler | ||||
| @ -69,6 +70,8 @@ interface VectorComponent { | ||||
| 
 | ||||
|     fun resources(): Resources | ||||
| 
 | ||||
|     fun assetReader(): AssetReader | ||||
| 
 | ||||
|     fun dimensionConverter(): DimensionConverter | ||||
| 
 | ||||
|     fun vectorConfiguration(): VectorConfiguration | ||||
| @ -97,7 +100,7 @@ interface VectorComponent { | ||||
| 
 | ||||
|     fun incomingKeyRequestHandler(): KeyRequestHandler | ||||
| 
 | ||||
|     fun authenticator(): Authenticator | ||||
|     fun authenticationService(): AuthenticationService | ||||
| 
 | ||||
|     fun bugReporter(): BugReporter | ||||
| 
 | ||||
|  | ||||
| @ -24,7 +24,7 @@ import dagger.Binds | ||||
| import dagger.Module | ||||
| import dagger.Provides | ||||
| import im.vector.matrix.android.api.Matrix | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.matrix.android.api.auth.AuthenticationService | ||||
| import im.vector.matrix.android.api.session.Session | ||||
| import im.vector.riotx.features.navigation.DefaultNavigator | ||||
| import im.vector.riotx.features.navigation.Navigator | ||||
| @ -64,8 +64,8 @@ abstract class VectorModule { | ||||
| 
 | ||||
|         @Provides | ||||
|         @JvmStatic | ||||
|         fun providesAuthenticator(matrix: Matrix): Authenticator { | ||||
|             return matrix.authenticator() | ||||
|         fun providesAuthenticationService(matrix: Matrix): AuthenticationService { | ||||
|             return matrix.authenticationService() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -31,6 +31,7 @@ import im.vector.riotx.features.home.HomeSharedActionViewModel | ||||
| import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel | ||||
| import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel | ||||
| import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel | ||||
| import im.vector.riotx.features.login.LoginSharedActionViewModel | ||||
| import im.vector.riotx.features.reactions.EmojiChooserViewModel | ||||
| import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel | ||||
| import im.vector.riotx.features.workers.signout.SignOutViewModel | ||||
| @ -112,4 +113,9 @@ interface ViewModelModule { | ||||
|     @IntoMap | ||||
|     @ViewModelKey(RoomDirectorySharedActionViewModel::class) | ||||
|     fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel | ||||
| 
 | ||||
|     @Binds | ||||
|     @IntoMap | ||||
|     @ViewModelKey(LoginSharedActionViewModel::class) | ||||
|     fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel | ||||
| } | ||||
|  | ||||
| @ -21,6 +21,7 @@ import im.vector.matrix.android.api.failure.MatrixError | ||||
| import im.vector.riotx.R | ||||
| import im.vector.riotx.core.resources.StringProvider | ||||
| import java.net.SocketTimeoutException | ||||
| import java.net.UnknownHostException | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) { | ||||
| @ -34,23 +35,61 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi | ||||
|         return when (throwable) { | ||||
|             null                         -> null | ||||
|             is Failure.NetworkConnection -> { | ||||
|                 if (throwable.ioException is SocketTimeoutException) { | ||||
|                     stringProvider.getString(R.string.error_network_timeout) | ||||
|                 } else { | ||||
|                     stringProvider.getString(R.string.error_no_network) | ||||
|                 when { | ||||
|                     throwable.ioException is SocketTimeoutException -> | ||||
|                         stringProvider.getString(R.string.error_network_timeout) | ||||
|                     throwable.ioException is UnknownHostException   -> | ||||
|                         // Invalid homeserver? | ||||
|                         stringProvider.getString(R.string.login_error_unknown_host) | ||||
|                     else                                            -> | ||||
|                         stringProvider.getString(R.string.error_no_network) | ||||
|                 } | ||||
|             } | ||||
|             is Failure.ServerError       -> { | ||||
|                 if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) { | ||||
|                     // Special case for terms and conditions | ||||
|                     stringProvider.getString(R.string.error_terms_not_accepted) | ||||
|                 } else { | ||||
|                     throwable.error.message.takeIf { it.isNotEmpty() } | ||||
|                             ?: throwable.error.code.takeIf { it.isNotEmpty() } | ||||
|                 when { | ||||
|                     throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN  -> { | ||||
|                         // Special case for terms and conditions | ||||
|                         stringProvider.getString(R.string.error_terms_not_accepted) | ||||
|                     } | ||||
|                     throwable.error.code == MatrixError.FORBIDDEN | ||||
|                             && throwable.error.message == "Invalid password" -> { | ||||
|                         stringProvider.getString(R.string.auth_invalid_login_param) | ||||
|                     } | ||||
|                     throwable.error.code == MatrixError.USER_IN_USE          -> { | ||||
|                         stringProvider.getString(R.string.login_signup_error_user_in_use) | ||||
|                     } | ||||
|                     throwable.error.code == MatrixError.BAD_JSON             -> { | ||||
|                         stringProvider.getString(R.string.login_error_bad_json) | ||||
|                     } | ||||
|                     throwable.error.code == MatrixError.NOT_JSON             -> { | ||||
|                         stringProvider.getString(R.string.login_error_not_json) | ||||
|                     } | ||||
|                     throwable.error.code == MatrixError.LIMIT_EXCEEDED       -> { | ||||
|                         limitExceededError(throwable.error) | ||||
|                     } | ||||
|                     throwable.error.code == MatrixError.THREEPID_NOT_FOUND   -> { | ||||
|                         stringProvider.getString(R.string.login_reset_password_error_not_found) | ||||
|                     } | ||||
|                     else                                                     -> { | ||||
|                         throwable.error.message.takeIf { it.isNotEmpty() } | ||||
|                                 ?: throwable.error.code.takeIf { it.isNotEmpty() } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else                         -> throwable.localizedMessage | ||||
|         } | ||||
|                 ?: stringProvider.getString(R.string.unknown_error) | ||||
|     } | ||||
| 
 | ||||
|     private fun limitExceededError(error: MatrixError): String { | ||||
|         val delay = error.retryAfterMillis | ||||
| 
 | ||||
|         return if (delay == null) { | ||||
|             stringProvider.getString(R.string.login_error_limit_exceeded) | ||||
|         } else { | ||||
|             // Ensure at least 1 second | ||||
|             val delaySeconds = delay.toInt() / 1000 + 1 | ||||
|             stringProvider.getQuantityString(R.plurals.login_error_limit_exceeded_retry_after, delaySeconds, delaySeconds) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,26 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.core.error | ||||
| 
 | ||||
| import im.vector.matrix.android.api.failure.Failure | ||||
| import im.vector.matrix.android.api.failure.MatrixError | ||||
| import javax.net.ssl.HttpsURLConnection | ||||
| 
 | ||||
| fun Throwable.is401(): Boolean { | ||||
|     return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ | ||||
|             && this.error.code == MatrixError.UNAUTHORIZED) | ||||
| } | ||||
| @ -18,6 +18,7 @@ package im.vector.riotx.core.extensions | ||||
| 
 | ||||
| import android.os.Parcelable | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentTransaction | ||||
| import im.vector.riotx.core.platform.VectorBaseActivity | ||||
| 
 | ||||
| fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) { | ||||
| @ -44,8 +45,13 @@ fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragment: Fragment, | ||||
|     supportFragmentManager.commitTransaction { replace(frameId, fragment).addToBackStack(tag) } | ||||
| } | ||||
| 
 | ||||
| fun <T : Fragment> VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { | ||||
| fun <T : Fragment> VectorBaseActivity.addFragmentToBackstack(frameId: Int, | ||||
|                                                              fragmentClass: Class<T>, | ||||
|                                                              params: Parcelable? = null, | ||||
|                                                              tag: String? = null, | ||||
|                                                              option: ((FragmentTransaction) -> Unit)? = null) { | ||||
|     supportFragmentManager.commitTransaction { | ||||
|         option?.invoke(this) | ||||
|         replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -17,6 +17,7 @@ | ||||
| package im.vector.riotx.core.extensions | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import android.util.Patterns | ||||
| import androidx.fragment.app.Fragment | ||||
| 
 | ||||
| fun Boolean.toOnOff() = if (this) "ON" else "OFF" | ||||
| @ -27,3 +28,8 @@ inline fun <T> T.ooi(block: (T) -> Unit): T = also(block) | ||||
|  * Apply argument to a Fragment | ||||
|  */ | ||||
| fun <T : Fragment> T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bundle().apply(block) } | ||||
| 
 | ||||
| /** | ||||
|  * Check if a CharSequence is an email | ||||
|  */ | ||||
| fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches() | ||||
|  | ||||
| @ -79,3 +79,6 @@ fun <T : Fragment> VectorBaseFragment.addChildFragmentToBackstack(frameId: Int, | ||||
|         replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Define a missing constant | ||||
| const val POP_BACK_STACK_EXCLUSIVE = 0 | ||||
|  | ||||
| @ -21,6 +21,7 @@ interface OnBackPressed { | ||||
|     /** | ||||
|      * Returns true, if the on back pressed event has been handled by this Fragment. | ||||
|      * Otherwise return false | ||||
|      * @param toolbarButton true if this is the back button from the toolbar | ||||
|      */ | ||||
|     fun onBackPressed(): Boolean | ||||
|     fun onBackPressed(toolbarButton: Boolean): Boolean | ||||
| } | ||||
|  | ||||
| @ -278,7 +278,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { | ||||
| 
 | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         if (item.itemId == android.R.id.home) { | ||||
|             onBackPressed() | ||||
|             onBackPressed(true) | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
| @ -286,20 +286,24 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { | ||||
|     } | ||||
| 
 | ||||
|     override fun onBackPressed() { | ||||
|         val handled = recursivelyDispatchOnBackPressed(supportFragmentManager) | ||||
|         onBackPressed(false) | ||||
|     } | ||||
| 
 | ||||
|     private fun onBackPressed(fromToolbar: Boolean) { | ||||
|         val handled = recursivelyDispatchOnBackPressed(supportFragmentManager, fromToolbar) | ||||
|         if (!handled) { | ||||
|             super.onBackPressed() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { | ||||
|         val reverseOrder = fm.fragments.filter { it is VectorBaseFragment }.reversed() | ||||
|     private fun recursivelyDispatchOnBackPressed(fm: FragmentManager, fromToolbar: Boolean): Boolean { | ||||
|         val reverseOrder = fm.fragments.filterIsInstance<VectorBaseFragment>().reversed() | ||||
|         for (f in reverseOrder) { | ||||
|             val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) | ||||
|             val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager, fromToolbar) | ||||
|             if (handledByChildFragments) { | ||||
|                 return true | ||||
|             } | ||||
|             if (f is OnBackPressed && f.onBackPressed()) { | ||||
|             if (f is OnBackPressed && f.onBackPressed(fromToolbar)) { | ||||
|                 return true | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -0,0 +1,62 @@ | ||||
| /* | ||||
|  * Copyright 2018 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.core.utils | ||||
| 
 | ||||
| import android.content.Context | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * Read asset files | ||||
|  */ | ||||
| class AssetReader @Inject constructor(private val context: Context) { | ||||
| 
 | ||||
|     /* ========================================================================================== | ||||
|      * CACHE | ||||
|      * ========================================================================================== */ | ||||
|     private val cache = mutableMapOf<String, String?>() | ||||
| 
 | ||||
|     /** | ||||
|      * Read an asset from resource and return a String or null in case of error. | ||||
|      * | ||||
|      * @param assetFilename Asset filename | ||||
|      * @return the content of the asset file, or null in case of error | ||||
|      */ | ||||
|     fun readAssetFile(assetFilename: String): String? { | ||||
|         return cache.getOrPut(assetFilename, { | ||||
|             return try { | ||||
|                 context.assets.open(assetFilename) | ||||
|                         .use { asset -> | ||||
|                             buildString { | ||||
|                                 var ch = asset.read() | ||||
|                                 while (ch != -1) { | ||||
|                                     append(ch.toChar()) | ||||
|                                     ch = asset.read() | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|             } catch (e: Exception) { | ||||
|                 Timber.e(e, "## readAssetFile() failed") | ||||
|                 null | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fun clearCache() { | ||||
|         cache.clear() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										55
									
								
								vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.core.utils | ||||
| 
 | ||||
| import android.text.Editable | ||||
| import android.view.ViewGroup | ||||
| import androidx.core.view.children | ||||
| import com.google.android.material.textfield.TextInputLayout | ||||
| import im.vector.riotx.core.platform.SimpleTextWatcher | ||||
| 
 | ||||
| /** | ||||
|  * Find all TextInputLayout in a ViewGroup and in all its descendants | ||||
|  */ | ||||
| fun ViewGroup.findAllTextInputLayout(): List<TextInputLayout> { | ||||
|     val res = ArrayList<TextInputLayout>() | ||||
| 
 | ||||
|     children.forEach { | ||||
|         if (it is TextInputLayout) { | ||||
|             res.add(it) | ||||
|         } else if (it is ViewGroup) { | ||||
|             // Recursive call | ||||
|             res.addAll(it.findAllTextInputLayout()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return res | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Add a text change listener to all TextInputEditText to reset error on its TextInputLayout when the text is changed | ||||
|  */ | ||||
| fun autoResetTextInputLayoutErrors(textInputLayouts: List<TextInputLayout>) { | ||||
|     textInputLayouts.forEach { | ||||
|         it.editText?.addTextChangedListener(object : SimpleTextWatcher() { | ||||
|             override fun afterTextChanged(s: Editable) { | ||||
|                 // Reset the error | ||||
|                 it.error = null | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| @ -21,9 +21,7 @@ import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import com.bumptech.glide.Glide | ||||
| import im.vector.matrix.android.api.Matrix | ||||
| import im.vector.matrix.android.api.MatrixCallback | ||||
| import im.vector.matrix.android.api.auth.Authenticator | ||||
| import im.vector.riotx.R | ||||
| import im.vector.riotx.core.di.ActiveSessionHolder | ||||
| import im.vector.riotx.core.di.ScreenComponent | ||||
| @ -56,8 +54,6 @@ class MainActivity : VectorBaseActivity() { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Inject lateinit var matrix: Matrix | ||||
|     @Inject lateinit var authenticator: Authenticator | ||||
|     @Inject lateinit var sessionHolder: ActiveSessionHolder | ||||
|     @Inject lateinit var errorFormatter: ErrorFormatter | ||||
| 
 | ||||
|  | ||||
| @ -329,7 +329,7 @@ class RoomListFragment @Inject constructor( | ||||
|         stateView.state = StateView.State.Error(message) | ||||
|     } | ||||
| 
 | ||||
|     override fun onBackPressed(): Boolean { | ||||
|     override fun onBackPressed(toolbarButton: Boolean): Boolean { | ||||
|         if (createChatFabMenu.onBackPressed()) { | ||||
|             return true | ||||
|         } | ||||
|  | ||||
| @ -68,8 +68,8 @@ object ServerUrlsRepository { | ||||
|         val prefs = PreferenceManager.getDefaultSharedPreferences(context) | ||||
| 
 | ||||
|         return prefs.getString(HOME_SERVER_URL_PREF, | ||||
|                                prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, | ||||
|                                                getDefaultHomeServerUrl(context))!!)!! | ||||
|                 prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, | ||||
|                         getDefaultHomeServerUrl(context))!!)!! | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -80,5 +80,5 @@ object ServerUrlsRepository { | ||||
|     /** | ||||
|      * Return default home server url from resources | ||||
|      */ | ||||
|     fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.default_hs_server_url) | ||||
|     fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.matrix_org_server_url) | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,149 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import android.view.View | ||||
| import androidx.annotation.CallSuper | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.transition.TransitionInflater | ||||
| import com.airbnb.mvrx.activityViewModel | ||||
| import com.airbnb.mvrx.withState | ||||
| import im.vector.matrix.android.api.failure.Failure | ||||
| import im.vector.matrix.android.api.failure.MatrixError | ||||
| import im.vector.riotx.R | ||||
| import im.vector.riotx.core.platform.OnBackPressed | ||||
| import im.vector.riotx.core.platform.VectorBaseFragment | ||||
| import javax.net.ssl.HttpsURLConnection | ||||
| 
 | ||||
| /** | ||||
|  * Parent Fragment for all the login/registration screens | ||||
|  */ | ||||
| abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { | ||||
| 
 | ||||
|     protected val loginViewModel: LoginViewModel by activityViewModel() | ||||
|     protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel | ||||
| 
 | ||||
|     private var isResetPasswordStarted = false | ||||
| 
 | ||||
|     // Due to async, we keep a boolean to avoid displaying twice the cancellation dialog | ||||
|     private var displayCancelDialog = true | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
| 
 | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|             sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @CallSuper | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
| 
 | ||||
|         loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) | ||||
| 
 | ||||
|         loginViewModel.viewEvents | ||||
|                 .observe() | ||||
|                 .subscribe { | ||||
|                     handleLoginViewEvents(it) | ||||
|                 } | ||||
|                 .disposeOnDestroyView() | ||||
|     } | ||||
| 
 | ||||
|     private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { | ||||
|         when (loginViewEvents) { | ||||
|             is LoginViewEvents.Error -> showError(loginViewEvents.throwable) | ||||
|             else                     -> | ||||
|                 // This is handled by the Activity | ||||
|                 Unit | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun showError(throwable: Throwable) { | ||||
|         when (throwable) { | ||||
|             is Failure.ServerError -> { | ||||
|                 if (throwable.error.code == MatrixError.FORBIDDEN | ||||
|                         && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { | ||||
|                     AlertDialog.Builder(requireActivity()) | ||||
|                             .setTitle(R.string.dialog_title_error) | ||||
|                             .setMessage(getString(R.string.login_registration_disabled)) | ||||
|                             .setPositiveButton(R.string.ok, null) | ||||
|                             .show() | ||||
|                 } else { | ||||
|                     onError(throwable) | ||||
|                 } | ||||
|             } | ||||
|             else                   -> onError(throwable) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     abstract fun onError(throwable: Throwable) | ||||
| 
 | ||||
|     override fun onBackPressed(toolbarButton: Boolean): Boolean { | ||||
|         return when { | ||||
|             displayCancelDialog && loginViewModel.isRegistrationStarted -> { | ||||
|                 // Ask for confirmation before cancelling the registration | ||||
|                 AlertDialog.Builder(requireActivity()) | ||||
|                         .setTitle(R.string.login_signup_cancel_confirmation_title) | ||||
|                         .setMessage(R.string.login_signup_cancel_confirmation_content) | ||||
|                         .setPositiveButton(R.string.yes) { _, _ -> | ||||
|                             displayCancelDialog = false | ||||
|                             vectorBaseActivity.onBackPressed() | ||||
|                         } | ||||
|                         .setNegativeButton(R.string.no, null) | ||||
|                         .show() | ||||
| 
 | ||||
|                 true | ||||
|             } | ||||
|             displayCancelDialog && isResetPasswordStarted               -> { | ||||
|                 // Ask for confirmation before cancelling the reset password | ||||
|                 AlertDialog.Builder(requireActivity()) | ||||
|                         .setTitle(R.string.login_reset_password_cancel_confirmation_title) | ||||
|                         .setMessage(R.string.login_reset_password_cancel_confirmation_content) | ||||
|                         .setPositiveButton(R.string.yes) { _, _ -> | ||||
|                             displayCancelDialog = false | ||||
|                             vectorBaseActivity.onBackPressed() | ||||
|                         } | ||||
|                         .setNegativeButton(R.string.no, null) | ||||
|                         .show() | ||||
| 
 | ||||
|                 true | ||||
|             } | ||||
|             else                                                        -> { | ||||
|                 resetViewModel() | ||||
|                 // Do not consume the Back event | ||||
|                 false | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     final override fun invalidate() = withState(loginViewModel) { state -> | ||||
|         // True when email is sent with success to the homeserver | ||||
|         isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not() | ||||
| 
 | ||||
|         updateWithState(state) | ||||
|     } | ||||
| 
 | ||||
|     open fun updateWithState(state: LoginViewState) { | ||||
|         // No op by default | ||||
|     } | ||||
| 
 | ||||
|     // Reset any modification on the loginViewModel by the current fragment | ||||
|     abstract fun resetViewModel() | ||||
| } | ||||
| @ -0,0 +1,20 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| // TODO Check the link with Nad | ||||
| const val MODULAR_LINK = "https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication" | ||||
| @ -0,0 +1,39 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class HomeServerConnectionConfigFactory @Inject constructor() { | ||||
| 
 | ||||
|     fun create(url: String?): HomeServerConnectionConfig? { | ||||
|         if (url == null) { | ||||
|             return null | ||||
|         } | ||||
| 
 | ||||
|         return try { | ||||
|             HomeServerConnectionConfig.Builder() | ||||
|                     .withHomeServerUri(url) | ||||
|                     .build() | ||||
|         } catch (t: Throwable) { | ||||
|             Timber.e(t) | ||||
|             null | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,39 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import com.squareup.moshi.Json | ||||
| import com.squareup.moshi.JsonClass | ||||
| import im.vector.matrix.android.api.auth.data.Credentials | ||||
| 
 | ||||
| @JsonClass(generateAdapter = true) | ||||
| data class JavascriptResponse( | ||||
|         @Json(name = "action") | ||||
|         val action: String? = null, | ||||
| 
 | ||||
|         /** | ||||
|          * Use for captcha result | ||||
|          */ | ||||
|         @Json(name = "response") | ||||
|         val response: String? = null, | ||||
| 
 | ||||
|         /** | ||||
|          * Used for login/registration result | ||||
|          */ | ||||
|         @Json(name = "credentials") | ||||
|         val credentials: Credentials? = null | ||||
| ) | ||||
| @ -17,12 +17,42 @@ | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import im.vector.matrix.android.api.auth.data.Credentials | ||||
| import im.vector.matrix.android.api.auth.registration.RegisterThreePid | ||||
| import im.vector.riotx.core.platform.VectorViewModelAction | ||||
| 
 | ||||
| sealed class LoginAction : VectorViewModelAction { | ||||
|     data class UpdateServerType(val serverType: ServerType) : LoginAction() | ||||
|     data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() | ||||
|     data class Login(val login: String, val password: String) : LoginAction() | ||||
|     data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() | ||||
|     data class NavigateTo(val target: LoginActivity.Navigation) : LoginAction() | ||||
|     data class UpdateSignMode(val signMode: SignMode) : LoginAction() | ||||
|     data class WebLoginSuccess(val credentials: Credentials) : LoginAction() | ||||
|     data class InitWith(val loginConfig: LoginConfig) : LoginAction() | ||||
|     data class ResetPassword(val email: String, val newPassword: String) : LoginAction() | ||||
|     object ResetPasswordMailConfirmed : LoginAction() | ||||
| 
 | ||||
|     // Login or Register, depending on the signMode | ||||
|     data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : LoginAction() | ||||
| 
 | ||||
|     // Register actions | ||||
|     open class RegisterAction : LoginAction() | ||||
| 
 | ||||
|     data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction() | ||||
|     object SendAgainThreePid : RegisterAction() | ||||
|     // TODO Confirm Email (from link in the email, open in the phone, intercepted by RiotX) | ||||
|     data class ValidateThreePid(val code: String) : RegisterAction() | ||||
| 
 | ||||
|     data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction() | ||||
|     object StopEmailValidationCheck : RegisterAction() | ||||
| 
 | ||||
|     data class CaptchaDone(val captchaResponse: String) : RegisterAction() | ||||
|     object AcceptTerms : RegisterAction() | ||||
|     object RegisterDummy : RegisterAction() | ||||
| 
 | ||||
|     // Reset actions | ||||
|     open class ResetAction : LoginAction() | ||||
| 
 | ||||
|     object ResetHomeServerType : ResetAction() | ||||
|     object ResetHomeServerUrl : ResetAction() | ||||
|     object ResetSignMode : ResetAction() | ||||
|     object ResetLogin : ResetAction() | ||||
|     object ResetResetPassword : ResetAction() | ||||
| } | ||||
|  | ||||
| @ -18,28 +18,41 @@ package im.vector.riotx.features.login | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.appcompat.widget.Toolbar | ||||
| import androidx.core.view.ViewCompat | ||||
| import androidx.core.view.children | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentManager | ||||
| import com.airbnb.mvrx.Success | ||||
| import androidx.fragment.app.FragmentTransaction | ||||
| import com.airbnb.mvrx.viewModel | ||||
| import com.airbnb.mvrx.withState | ||||
| import im.vector.matrix.android.api.auth.registration.FlowResult | ||||
| import im.vector.matrix.android.api.auth.registration.Stage | ||||
| import im.vector.riotx.R | ||||
| import im.vector.riotx.core.di.ScreenComponent | ||||
| import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE | ||||
| import im.vector.riotx.core.extensions.addFragment | ||||
| import im.vector.riotx.core.extensions.addFragmentToBackstack | ||||
| import im.vector.riotx.core.extensions.observeEvent | ||||
| import im.vector.riotx.core.platform.ToolbarConfigurable | ||||
| import im.vector.riotx.core.platform.VectorBaseActivity | ||||
| import im.vector.riotx.features.disclaimer.showDisclaimerDialog | ||||
| import im.vector.riotx.features.home.HomeActivity | ||||
| import im.vector.riotx.features.login.terms.LoginTermsFragment | ||||
| import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument | ||||
| import im.vector.riotx.features.login.terms.toLocalizedLoginTerms | ||||
| import kotlinx.android.synthetic.main.activity_login.* | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class LoginActivity : VectorBaseActivity() { | ||||
| 
 | ||||
|     // Supported navigation actions for this Activity | ||||
|     sealed class Navigation { | ||||
|         object OpenSsoLoginFallback : Navigation() | ||||
|         object GoBack : Navigation() | ||||
|     } | ||||
| /** | ||||
|  * The LoginActivity manages the fragment navigation and also display the loading View | ||||
|  */ | ||||
| class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { | ||||
| 
 | ||||
|     private val loginViewModel: LoginViewModel by viewModel() | ||||
|     private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel | ||||
| 
 | ||||
|     @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory | ||||
| 
 | ||||
| @ -47,42 +60,290 @@ class LoginActivity : VectorBaseActivity() { | ||||
|         injector.inject(this) | ||||
|     } | ||||
| 
 | ||||
|     override fun getLayoutRes() = R.layout.activity_simple | ||||
|     private val enterAnim = R.anim.enter_fade_in | ||||
|     private val exitAnim = R.anim.exit_fade_out | ||||
| 
 | ||||
|     private val popEnterAnim = R.anim.no_anim | ||||
|     private val popExitAnim = R.anim.exit_fade_out | ||||
| 
 | ||||
|     private val topFragment: Fragment? | ||||
|         get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer) | ||||
| 
 | ||||
|     private val commonOption: (FragmentTransaction) -> Unit = { ft -> | ||||
|         // Find the loginLogo on the current Fragment, this should not return null | ||||
|         (topFragment?.view as? ViewGroup) | ||||
|                 // Find findViewById does not work, I do not know why | ||||
|                 // findViewById<View?>(R.id.loginLogo) | ||||
|                 ?.children | ||||
|                 ?.first { it.id == R.id.loginLogo } | ||||
|                 ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } | ||||
|         // TODO | ||||
|         ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) | ||||
|     } | ||||
| 
 | ||||
|     override fun getLayoutRes() = R.layout.activity_login | ||||
| 
 | ||||
|     override fun initUiAndData() { | ||||
|         if (isFirstCreation()) { | ||||
|             addFragment(R.id.simpleFragmentContainer, LoginFragment::class.java) | ||||
|             addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) | ||||
|         } | ||||
| 
 | ||||
|         // Get config extra | ||||
|         val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG) | ||||
|         if (loginConfig != null && isFirstCreation()) { | ||||
|             // TODO Check this | ||||
|             loginViewModel.handle(LoginAction.InitWith(loginConfig)) | ||||
|         } | ||||
| 
 | ||||
|         loginViewModel.navigationLiveData.observeEvent(this) { | ||||
|             when (it) { | ||||
|                 is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) | ||||
|                 is Navigation.GoBack               -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) | ||||
|         loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java) | ||||
|         loginSharedActionViewModel.observe() | ||||
|                 .subscribe { | ||||
|                     handleLoginNavigation(it) | ||||
|                 } | ||||
|                 .disposeOnDestroy() | ||||
| 
 | ||||
|         loginViewModel | ||||
|                 .subscribe(this) { | ||||
|                     updateWithState(it) | ||||
|                 } | ||||
|                 .disposeOnDestroy() | ||||
| 
 | ||||
|         loginViewModel.viewEvents | ||||
|                 .observe() | ||||
|                 .subscribe { | ||||
|                     handleLoginViewEvents(it) | ||||
|                 } | ||||
|                 .disposeOnDestroy() | ||||
|     } | ||||
| 
 | ||||
|     private fun handleLoginNavigation(loginNavigation: LoginNavigation) { | ||||
|         // Assigning to dummy make sure we do not forget a case | ||||
|         @Suppress("UNUSED_VARIABLE") | ||||
|         val dummy = when (loginNavigation) { | ||||
|             is LoginNavigation.OpenServerSelection                        -> | ||||
|                 addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                         LoginServerSelectionFragment::class.java, | ||||
|                         option = { ft -> | ||||
|                             findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } | ||||
|                             findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } | ||||
|                             findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } | ||||
|                             // TODO Disabled because it provokes a flickering | ||||
|                             // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) | ||||
|                         }) | ||||
|             is LoginNavigation.OnServerSelectionDone                      -> onServerSelectionDone() | ||||
|             is LoginNavigation.OnSignModeSelected                         -> onSignModeSelected() | ||||
|             is LoginNavigation.OnLoginFlowRetrieved                       -> | ||||
|                 addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                         LoginSignUpSignInSelectionFragment::class.java, | ||||
|                         option = commonOption) | ||||
|             is LoginNavigation.OnWebLoginError                            -> onWebLoginError(loginNavigation) | ||||
|             is LoginNavigation.OnForgetPasswordClicked                    -> | ||||
|                 addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                         LoginResetPasswordFragment::class.java, | ||||
|                         option = commonOption) | ||||
|             is LoginNavigation.OnResetPasswordSendThreePidDone            -> { | ||||
|                 supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) | ||||
|                 addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                         LoginResetPasswordMailConfirmationFragment::class.java, | ||||
|                         option = commonOption) | ||||
|             } | ||||
|             is LoginNavigation.OnResetPasswordMailConfirmationSuccess     -> { | ||||
|                 supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) | ||||
|                 addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                         LoginResetPasswordSuccessFragment::class.java, | ||||
|                         option = commonOption) | ||||
|             } | ||||
|             is LoginNavigation.OnResetPasswordMailConfirmationSuccessDone -> { | ||||
|                 // Go back to the login fragment | ||||
|                 supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) | ||||
|             } | ||||
|             is LoginNavigation.OnSendEmailSuccess                         -> | ||||
|                 addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                         LoginWaitForEmailFragment::class.java, | ||||
|                         LoginWaitForEmailFragmentArgument(loginNavigation.email), | ||||
|                         tag = FRAGMENT_REGISTRATION_STAGE_TAG, | ||||
|                         option = commonOption) | ||||
|             is LoginNavigation.OnSendMsisdnSuccess                        -> | ||||
|                 addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                         LoginGenericTextInputFormFragment::class.java, | ||||
|                         LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginNavigation.msisdn), | ||||
|                         tag = FRAGMENT_REGISTRATION_STAGE_TAG, | ||||
|                         option = commonOption) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { | ||||
|         when (loginViewEvents) { | ||||
|             is LoginViewEvents.RegistrationFlowResult -> { | ||||
|                 // Check that all flows are supported by the application | ||||
|                 if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { | ||||
|                     // Display a popup to propose use web fallback | ||||
|                     onRegistrationStageNotSupported() | ||||
|                 } else { | ||||
|                     if (loginViewEvents.isRegistrationStarted) { | ||||
|                         // Go on with registration flow | ||||
|                         handleRegistrationNavigation(loginViewEvents.flowResult) | ||||
|                     } else { | ||||
|                         // First ask for login and password | ||||
|                         // I add a tag to indicate that this fragment is a registration stage. | ||||
|                         // This way it will be automatically popped in when starting the next registration stage | ||||
|                         addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                                 LoginFragment::class.java, | ||||
|                                 tag = FRAGMENT_REGISTRATION_STAGE_TAG, | ||||
|                                 option = commonOption | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             is LoginViewEvents.OutdatedHomeserver     -> | ||||
|                 AlertDialog.Builder(this) | ||||
|                         .setTitle(R.string.login_error_outdated_homeserver_title) | ||||
|                         .setMessage(R.string.login_error_outdated_homeserver_content) | ||||
|                         .setPositiveButton(R.string.ok, null) | ||||
|                         .show() | ||||
|             is LoginViewEvents.Error                  -> | ||||
|                 // This is handled by the Fragments | ||||
|                 Unit | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun updateWithState(loginViewState: LoginViewState) { | ||||
|         if (loginViewState.isUserLogged()) { | ||||
|             val intent = HomeActivity.newIntent(this) | ||||
|             startActivity(intent) | ||||
|             finish() | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) { | ||||
|             if (it is Success) { | ||||
|                 val intent = HomeActivity.newIntent(this) | ||||
|                 startActivity(intent) | ||||
|                 finish() | ||||
|         // Loading | ||||
|         loginLoading.isVisible = loginViewState.isLoading() | ||||
|     } | ||||
| 
 | ||||
|     private fun onWebLoginError(onWebLoginError: LoginNavigation.OnWebLoginError) { | ||||
|         // Pop the backstack | ||||
|         supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) | ||||
| 
 | ||||
|         // And inform the user | ||||
|         AlertDialog.Builder(this) | ||||
|                 .setTitle(R.string.dialog_title_error) | ||||
|                 .setMessage(getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) | ||||
|                 .setPositiveButton(R.string.ok, null) | ||||
|                 .show() | ||||
|     } | ||||
| 
 | ||||
|     private fun onServerSelectionDone() = withState(loginViewModel) { state -> | ||||
|         when (state.serverType) { | ||||
|             ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow | ||||
|             ServerType.Modular, | ||||
|             ServerType.Other     -> addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                     LoginServerUrlFormFragment::class.java, | ||||
|                     option = commonOption) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun onSignModeSelected() = withState(loginViewModel) { state -> | ||||
|         when (state.signMode) { | ||||
|             SignMode.Unknown -> error("Sign mode has to be set before calling this method") | ||||
|             SignMode.SignUp  -> { | ||||
|                 // This is managed by the LoginViewEvents | ||||
|             } | ||||
|             SignMode.SignIn  -> { | ||||
|                 // It depends on the LoginMode | ||||
|                 when (state.loginMode) { | ||||
|                     LoginMode.Unknown     -> error("Developer error") | ||||
|                     LoginMode.Password    -> addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                             LoginFragment::class.java, | ||||
|                             tag = FRAGMENT_LOGIN_TAG, | ||||
|                             option = commonOption) | ||||
|                     LoginMode.Sso         -> addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                             LoginWebFragment::class.java, | ||||
|                             option = commonOption) | ||||
|                     LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onResume() { | ||||
|         super.onResume() | ||||
|     private fun onRegistrationStageNotSupported() { | ||||
|         AlertDialog.Builder(this) | ||||
|                 .setTitle(R.string.app_name) | ||||
|                 .setMessage(getString(R.string.login_registration_not_supported)) | ||||
|                 .setPositiveButton(R.string.yes) { _, _ -> | ||||
|                     addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                             LoginWebFragment::class.java, | ||||
|                             option = commonOption) | ||||
|                 } | ||||
|                 .setNegativeButton(R.string.no, null) | ||||
|                 .show() | ||||
|     } | ||||
| 
 | ||||
|         showDisclaimerDialog(this) | ||||
|     private fun onLoginModeNotSupported(supportedTypes: List<String>) { | ||||
|         AlertDialog.Builder(this) | ||||
|                 .setTitle(R.string.app_name) | ||||
|                 .setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) | ||||
|                 .setPositiveButton(R.string.yes) { _, _ -> | ||||
|                     addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                             LoginWebFragment::class.java, | ||||
|                             option = commonOption) | ||||
|                 } | ||||
|                 .setNegativeButton(R.string.no, null) | ||||
|                 .show() | ||||
|     } | ||||
| 
 | ||||
|     private fun handleRegistrationNavigation(flowResult: FlowResult) { | ||||
|         // Complete all mandatory stages first | ||||
|         val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } | ||||
| 
 | ||||
|         if (mandatoryStage != null) { | ||||
|             doStage(mandatoryStage) | ||||
|         } else { | ||||
|             // Consider optional stages | ||||
|             val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy } | ||||
|             if (optionalStage == null) { | ||||
|                 // Should not happen... | ||||
|             } else { | ||||
|                 doStage(optionalStage) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun doStage(stage: Stage) { | ||||
|         // Ensure there is no fragment for registration stage in the backstack | ||||
|         supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) | ||||
| 
 | ||||
|         when (stage) { | ||||
|             is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                     LoginCaptchaFragment::class.java, | ||||
|                     LoginCaptchaFragmentArgument(stage.publicKey), | ||||
|                     tag = FRAGMENT_REGISTRATION_STAGE_TAG, | ||||
|                     option = commonOption) | ||||
|             is Stage.Email     -> addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                     LoginGenericTextInputFormFragment::class.java, | ||||
|                     LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), | ||||
|                     tag = FRAGMENT_REGISTRATION_STAGE_TAG, | ||||
|                     option = commonOption) | ||||
|             is Stage.Msisdn    -> addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                     LoginGenericTextInputFormFragment::class.java, | ||||
|                     LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), | ||||
|                     tag = FRAGMENT_REGISTRATION_STAGE_TAG, | ||||
|                     option = commonOption) | ||||
|             is Stage.Terms     -> addFragmentToBackstack(R.id.loginFragmentContainer, | ||||
|                     LoginTermsFragment::class.java, | ||||
|                     LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))), | ||||
|                     tag = FRAGMENT_REGISTRATION_STAGE_TAG, | ||||
|                     option = commonOption) | ||||
|             else               -> Unit // Should not happen | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun configure(toolbar: Toolbar) { | ||||
|         configureToolbar(toolbar) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" | ||||
|         private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" | ||||
| 
 | ||||
|         private const val EXTRA_CONFIG = "EXTRA_CONFIG" | ||||
| 
 | ||||
|         fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { | ||||
|  | ||||
| @ -0,0 +1,193 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.DialogInterface | ||||
| import android.graphics.Bitmap | ||||
| import android.net.http.SslError | ||||
| import android.os.Build | ||||
| import android.os.Parcelable | ||||
| import android.view.KeyEvent | ||||
| import android.webkit.* | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.core.view.isVisible | ||||
| import com.airbnb.mvrx.args | ||||
| import im.vector.matrix.android.internal.di.MoshiProvider | ||||
| import im.vector.riotx.R | ||||
| import im.vector.riotx.core.error.ErrorFormatter | ||||
| import im.vector.riotx.core.utils.AssetReader | ||||
| import kotlinx.android.parcel.Parcelize | ||||
| import kotlinx.android.synthetic.main.fragment_login_captcha.* | ||||
| import timber.log.Timber | ||||
| import java.net.URLDecoder | ||||
| import java.util.* | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @Parcelize | ||||
| data class LoginCaptchaFragmentArgument( | ||||
|         val siteKey: String | ||||
| ) : Parcelable | ||||
| 
 | ||||
| /** | ||||
|  * In this screen, the user is asked to confirm he is not a robot | ||||
|  */ | ||||
| class LoginCaptchaFragment @Inject constructor( | ||||
|         private val assetReader: AssetReader, | ||||
|         private val errorFormatter: ErrorFormatter | ||||
| ) : AbstractLoginFragment() { | ||||
| 
 | ||||
|     override fun getLayoutResId() = R.layout.fragment_login_captcha | ||||
| 
 | ||||
|     private val params: LoginCaptchaFragmentArgument by args() | ||||
| 
 | ||||
|     private var isWebViewLoaded = false | ||||
| 
 | ||||
|     @SuppressLint("SetJavaScriptEnabled") | ||||
|     private fun setupWebView(state: LoginViewState) { | ||||
|         loginCaptchaWevView.settings.javaScriptEnabled = true | ||||
| 
 | ||||
|         val reCaptchaPage = assetReader.readAssetFile("reCaptchaPage.html") ?: error("missing asset reCaptchaPage.html") | ||||
| 
 | ||||
|         val html = Formatter().format(reCaptchaPage, params.siteKey).toString() | ||||
|         val mime = "text/html" | ||||
|         val encoding = "utf-8" | ||||
| 
 | ||||
|         val homeServerUrl = state.homeServerUrl ?: error("missing url of homeserver") | ||||
|         loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null) | ||||
|         loginCaptchaWevView.requestLayout() | ||||
| 
 | ||||
|         loginCaptchaWevView.webViewClient = object : WebViewClient() { | ||||
|             override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { | ||||
|                 super.onPageStarted(view, url, favicon) | ||||
| 
 | ||||
|                 // Show loader | ||||
|                 loginCaptchaProgress.isVisible = true | ||||
|             } | ||||
| 
 | ||||
|             override fun onPageFinished(view: WebView, url: String) { | ||||
|                 super.onPageFinished(view, url) | ||||
| 
 | ||||
|                 // Hide loader | ||||
|                 loginCaptchaProgress.isVisible = false | ||||
|             } | ||||
| 
 | ||||
|             override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { | ||||
|                 Timber.d("## onReceivedSslError() : " + error.certificate) | ||||
| 
 | ||||
|                 if (!isAdded) { | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|                 AlertDialog.Builder(requireActivity()) | ||||
|                         .setMessage(R.string.ssl_could_not_verify) | ||||
|                         .setPositiveButton(R.string.ssl_trust) { _, _ -> | ||||
|                             Timber.d("## onReceivedSslError() : the user trusted") | ||||
|                             handler.proceed() | ||||
|                         } | ||||
|                         .setNegativeButton(R.string.ssl_do_not_trust) { _, _ -> | ||||
|                             Timber.d("## onReceivedSslError() : the user did not trust") | ||||
|                             handler.cancel() | ||||
|                         } | ||||
|                         .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> | ||||
|                             if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { | ||||
|                                 handler.cancel() | ||||
|                                 Timber.d("## onReceivedSslError() : the user dismisses the trust dialog.") | ||||
|                                 dialog.dismiss() | ||||
|                                 return@OnKeyListener true | ||||
|                             } | ||||
|                             false | ||||
|                         }) | ||||
|                         .setCancelable(false) | ||||
|                         .show() | ||||
|             } | ||||
| 
 | ||||
|             // common error message | ||||
|             private fun onError(errorMessage: String) { | ||||
|                 Timber.e("## onError() : $errorMessage") | ||||
| 
 | ||||
|                 // TODO | ||||
|                 // Toast.makeText(this@AccountCreationCaptchaActivity, errorMessage, Toast.LENGTH_LONG).show() | ||||
| 
 | ||||
|                 // on error case, close this activity | ||||
|                 // runOnUiThread(Runnable { finish() }) | ||||
|             } | ||||
| 
 | ||||
|             @SuppressLint("NewApi") | ||||
|             override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { | ||||
|                 super.onReceivedHttpError(view, request, errorResponse) | ||||
| 
 | ||||
|                 if (request.url.toString().endsWith("favicon.ico")) { | ||||
|                     // Ignore this error | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||||
|                     onError(errorResponse.reasonPhrase) | ||||
|                 } else { | ||||
|                     onError(errorResponse.toString()) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { | ||||
|                 @Suppress("DEPRECATION") | ||||
|                 super.onReceivedError(view, errorCode, description, failingUrl) | ||||
|                 onError(description) | ||||
|             } | ||||
| 
 | ||||
|             override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { | ||||
|                 if (url?.startsWith("js:") == true) { | ||||
|                     var json = url.substring(3) | ||||
|                     var javascriptResponse: JavascriptResponse? = null | ||||
| 
 | ||||
|                     try { | ||||
|                         // URL decode | ||||
|                         json = URLDecoder.decode(json, "UTF-8") | ||||
|                         javascriptResponse = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java).fromJson(json) | ||||
|                     } catch (e: Exception) { | ||||
|                         Timber.e(e, "## shouldOverrideUrlLoading(): failed") | ||||
|                     } | ||||
| 
 | ||||
|                     val response = javascriptResponse?.response | ||||
|                     if (javascriptResponse?.action == "verifyCallback" && response != null) { | ||||
|                         loginViewModel.handle(LoginAction.CaptchaDone(response)) | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onError(throwable: Throwable) { | ||||
|         AlertDialog.Builder(requireActivity()) | ||||
|                 .setTitle(R.string.dialog_title_error) | ||||
|                 .setMessage(errorFormatter.toHumanReadable(throwable)) | ||||
|                 .setPositiveButton(R.string.ok, null) | ||||
|                 .show() | ||||
|     } | ||||
| 
 | ||||
|     override fun resetViewModel() { | ||||
|         loginViewModel.handle(LoginAction.ResetLogin) | ||||
|     } | ||||
| 
 | ||||
|     override fun updateWithState(state: LoginViewState) { | ||||
|         if (!isWebViewLoaded) { | ||||
|             setupWebView(state) | ||||
|             isWebViewLoaded = true | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -16,34 +16,38 @@ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import android.view.View | ||||
| import android.view.inputmethod.EditorInfo | ||||
| import android.widget.Toast | ||||
| import androidx.autofill.HintConstants | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.transition.TransitionManager | ||||
| import com.airbnb.mvrx.* | ||||
| import com.jakewharton.rxbinding3.view.focusChanges | ||||
| import butterknife.OnClick | ||||
| import com.airbnb.mvrx.Fail | ||||
| import com.airbnb.mvrx.Loading | ||||
| import com.airbnb.mvrx.Success | ||||
| import com.jakewharton.rxbinding3.widget.textChanges | ||||
| import im.vector.matrix.android.api.failure.Failure | ||||
| import im.vector.matrix.android.api.failure.MatrixError | ||||
| import im.vector.riotx.R | ||||
| import im.vector.riotx.core.extensions.setTextWithColoredPart | ||||
| import im.vector.riotx.core.error.ErrorFormatter | ||||
| import im.vector.riotx.core.extensions.hideKeyboard | ||||
| import im.vector.riotx.core.extensions.showPassword | ||||
| import im.vector.riotx.core.platform.VectorBaseFragment | ||||
| import im.vector.riotx.core.utils.openUrlInExternalBrowser | ||||
| import im.vector.riotx.features.homeserver.ServerUrlsRepository | ||||
| import io.reactivex.Observable | ||||
| import io.reactivex.functions.Function3 | ||||
| import io.reactivex.functions.BiFunction | ||||
| import io.reactivex.rxkotlin.subscribeBy | ||||
| import kotlinx.android.synthetic.main.fragment_login.* | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * What can be improved: | ||||
|  * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect | ||||
|  * In this screen, in signin mode: | ||||
|  * - the user is asked for login and password to sign in to a homeserver. | ||||
|  * - He also can reset his password | ||||
|  * In signup mode: | ||||
|  * - the user is asked for login and password | ||||
|  */ | ||||
| class LoginFragment @Inject constructor() : VectorBaseFragment() { | ||||
| 
 | ||||
|     private val viewModel: LoginViewModel by activityViewModel() | ||||
| class LoginFragment @Inject constructor( | ||||
|         private val errorFormatter: ErrorFormatter | ||||
| ) : AbstractLoginFragment() { | ||||
| 
 | ||||
|     private var passwordShown = false | ||||
| 
 | ||||
| @ -52,69 +56,101 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() { | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
| 
 | ||||
|         setupNotice() | ||||
|         setupAuthButton() | ||||
|         setupSubmitButton() | ||||
|         setupPasswordReveal() | ||||
|     } | ||||
| 
 | ||||
|         homeServerField.focusChanges() | ||||
|                 .subscribe { | ||||
|                     if (!it) { | ||||
|                         viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) | ||||
|                     } | ||||
|     private fun setupAutoFill(state: LoginViewState) { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             when (state.signMode) { | ||||
|                 SignMode.Unknown -> error("developer error") | ||||
|                 SignMode.SignUp  -> { | ||||
|                     loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) | ||||
|                     passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) | ||||
|                 } | ||||
|                 SignMode.SignIn  -> { | ||||
|                     loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) | ||||
|                     passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) | ||||
|                 } | ||||
|                 .disposeOnDestroyView() | ||||
| 
 | ||||
|         homeServerField.setOnEditorActionListener { _, actionId, _ -> | ||||
|             if (actionId == EditorInfo.IME_ACTION_DONE) { | ||||
|                 viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) | ||||
|                 return@setOnEditorActionListener true | ||||
|             } | ||||
|             return@setOnEditorActionListener false | ||||
|         } | ||||
| 
 | ||||
|         val initHsUrl = viewModel.getInitialHomeServerUrl() | ||||
|         if (initHsUrl != null) { | ||||
|             homeServerField.setText(initHsUrl) | ||||
|         } else { | ||||
|             homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext())) | ||||
|         } | ||||
|         viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) | ||||
|     } | ||||
| 
 | ||||
|     private fun setupNotice() { | ||||
|         riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part) | ||||
| 
 | ||||
|         riotx_no_registration_notice.setOnClickListener { | ||||
|             openUrlInExternalBrowser(requireActivity(), "https://about.riot.im/downloads") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun authenticate() { | ||||
|         val login = loginField.text?.trim().toString() | ||||
|         val password = passwordField.text?.trim().toString() | ||||
|     @OnClick(R.id.loginSubmit) | ||||
|     fun submit() { | ||||
|         cleanupUi() | ||||
| 
 | ||||
|         viewModel.handle(LoginAction.Login(login, password)) | ||||
|         val login = loginField.text.toString() | ||||
|         val password = passwordField.text.toString() | ||||
| 
 | ||||
|         loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device))) | ||||
|     } | ||||
| 
 | ||||
|     private fun setupAuthButton() { | ||||
|     private fun cleanupUi() { | ||||
|         loginSubmit.hideKeyboard() | ||||
|         loginFieldTil.error = null | ||||
|         passwordFieldTil.error = null | ||||
|     } | ||||
| 
 | ||||
|     private fun setupUi(state: LoginViewState) { | ||||
|         val resId = when (state.signMode) { | ||||
|             SignMode.Unknown -> error("developer error") | ||||
|             SignMode.SignUp  -> R.string.login_signup_to | ||||
|             SignMode.SignIn  -> R.string.login_connect_to | ||||
|         } | ||||
| 
 | ||||
|         when (state.serverType) { | ||||
|             ServerType.MatrixOrg -> { | ||||
|                 loginServerIcon.isVisible = true | ||||
|                 loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) | ||||
|                 loginTitle.text = getString(resId, state.homeServerUrlSimple) | ||||
|                 loginNotice.text = getString(R.string.login_server_matrix_org_text) | ||||
|             } | ||||
|             ServerType.Modular   -> { | ||||
|                 loginServerIcon.isVisible = true | ||||
|                 loginServerIcon.setImageResource(R.drawable.ic_logo_modular) | ||||
|                 // TODO | ||||
|                 loginTitle.text = getString(resId, "TODO") | ||||
|                 loginNotice.text = getString(R.string.login_server_modular_text) | ||||
|             } | ||||
|             ServerType.Other     -> { | ||||
|                 loginServerIcon.isVisible = false | ||||
|                 loginTitle.text = getString(resId, state.homeServerUrlSimple) | ||||
|                 loginNotice.text = getString(R.string.login_server_other_text) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun setupButtons(state: LoginViewState) { | ||||
|         forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn | ||||
| 
 | ||||
|         loginSubmit.text = getString(when (state.signMode) { | ||||
|             SignMode.Unknown -> error("developer error") | ||||
|             SignMode.SignUp  -> R.string.login_signup_submit | ||||
|             SignMode.SignIn  -> R.string.login_signin | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private fun setupSubmitButton() { | ||||
|         Observable | ||||
|                 .combineLatest( | ||||
|                         loginField.textChanges().map { it.trim().isNotEmpty() }, | ||||
|                         passwordField.textChanges().map { it.trim().isNotEmpty() }, | ||||
|                         homeServerField.textChanges().map { it.trim().isNotEmpty() }, | ||||
|                         Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty -> | ||||
|                             isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty | ||||
|                         BiFunction<Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty -> | ||||
|                             isLoginNotEmpty && isPasswordNotEmpty | ||||
|                         } | ||||
|                 ) | ||||
|                 .subscribeBy { authenticateButton.isEnabled = it } | ||||
|                 .subscribeBy { | ||||
|                     loginFieldTil.error = null | ||||
|                     passwordFieldTil.error = null | ||||
|                     loginSubmit.isEnabled = it | ||||
|                 } | ||||
|                 .disposeOnDestroyView() | ||||
|         authenticateButton.setOnClickListener { authenticate() } | ||||
| 
 | ||||
|         authenticateButtonSso.setOnClickListener { openSso() } | ||||
|     } | ||||
| 
 | ||||
|     private fun openSso() { | ||||
|         viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback)) | ||||
|     @OnClick(R.id.forgetPasswordButton) | ||||
|     fun forgetPasswordClicked() { | ||||
|         loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) | ||||
|     } | ||||
| 
 | ||||
|     private fun setupPasswordReveal() { | ||||
| @ -141,73 +177,47 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun invalidate() = withState(viewModel) { state -> | ||||
|         TransitionManager.beginDelayedTransition(login_fragment) | ||||
|     override fun resetViewModel() { | ||||
|         loginViewModel.handle(LoginAction.ResetLogin) | ||||
|     } | ||||
| 
 | ||||
|         when (state.asyncHomeServerLoginFlowRequest) { | ||||
|             is Incomplete -> { | ||||
|                 progressBar.isVisible = true | ||||
|                 touchArea.isVisible = true | ||||
|                 loginField.isVisible = false | ||||
|                 passwordContainer.isVisible = false | ||||
|                 authenticateButton.isVisible = false | ||||
|                 authenticateButtonSso.isVisible = false | ||||
|                 passwordShown = false | ||||
|                 renderPasswordField() | ||||
|             } | ||||
|             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 | ||||
|     override fun onError(throwable: Throwable) { | ||||
|         loginFieldTil.error = errorFormatter.toHumanReadable(throwable) | ||||
|     } | ||||
| 
 | ||||
|                 when (state.asyncHomeServerLoginFlowRequest()) { | ||||
|                     LoginMode.Password    -> { | ||||
|                         loginField.isVisible = true | ||||
|                         passwordContainer.isVisible = true | ||||
|                         authenticateButton.isVisible = true | ||||
|                         authenticateButtonSso.isVisible = false | ||||
|                         if (loginField.text.isNullOrBlank() && passwordField.text.isNullOrBlank()) { | ||||
|                             // Jump focus to login | ||||
|                             loginField.requestFocus() | ||||
|                         } | ||||
|                     } | ||||
|                     LoginMode.Sso         -> { | ||||
|                         loginField.isVisible = false | ||||
|                         passwordContainer.isVisible = false | ||||
|                         authenticateButton.isVisible = false | ||||
|                         authenticateButtonSso.isVisible = true | ||||
|                     } | ||||
|                     LoginMode.Unsupported -> { | ||||
|                         loginField.isVisible = false | ||||
|                         passwordContainer.isVisible = false | ||||
|                         authenticateButton.isVisible = false | ||||
|                         authenticateButtonSso.isVisible = false | ||||
|                         Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     override fun updateWithState(state: LoginViewState) { | ||||
|         setupUi(state) | ||||
|         setupAutoFill(state) | ||||
|         setupButtons(state) | ||||
| 
 | ||||
|         when (state.asyncLoginAction) { | ||||
|             is Loading -> { | ||||
|                 progressBar.isVisible = true | ||||
|                 touchArea.isVisible = true | ||||
| 
 | ||||
|                 // Ensure password is hidden | ||||
|                 passwordShown = false | ||||
|                 renderPasswordField() | ||||
|             } | ||||
|             is Fail    -> { | ||||
|                 progressBar.isVisible = false | ||||
|                 touchArea.isVisible = false | ||||
|                 Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show() | ||||
|                 val error = state.asyncLoginAction.error | ||||
|                 if (error is Failure.ServerError | ||||
|                         && error.error.code == MatrixError.FORBIDDEN | ||||
|                         && error.error.message.isEmpty()) { | ||||
|                     // Login with email, but email unknown | ||||
|                     loginFieldTil.error = getString(R.string.login_login_with_email_error) | ||||
|                 } else { | ||||
|                     // Trick to display the error without text. | ||||
|                     loginFieldTil.error = " " | ||||
|                     passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncLoginAction.error) | ||||
|                 } | ||||
|             } | ||||
|             // Success is handled by the LoginActivity | ||||
|             is Success -> Unit | ||||
|         } | ||||
| 
 | ||||
|         when (state.asyncRegistration) { | ||||
|             is Loading -> { | ||||
|                 // Ensure password is hidden | ||||
|                 passwordShown = false | ||||
|                 renderPasswordField() | ||||
|             } | ||||
|             // Success is handled by the LoginActivity | ||||
|             is Success -> Unit | ||||
|  | ||||
| @ -0,0 +1,252 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import android.os.Parcelable | ||||
| import android.text.InputType | ||||
| import android.view.View | ||||
| import androidx.autofill.HintConstants | ||||
| import androidx.core.view.isVisible | ||||
| import butterknife.OnClick | ||||
| import com.airbnb.mvrx.args | ||||
| import com.google.i18n.phonenumbers.NumberParseException | ||||
| import com.google.i18n.phonenumbers.PhoneNumberUtil | ||||
| import com.jakewharton.rxbinding3.widget.textChanges | ||||
| import im.vector.matrix.android.api.auth.registration.RegisterThreePid | ||||
| import im.vector.matrix.android.api.failure.Failure | ||||
| import im.vector.riotx.R | ||||
| import im.vector.riotx.core.error.ErrorFormatter | ||||
| import im.vector.riotx.core.error.is401 | ||||
| import im.vector.riotx.core.extensions.hideKeyboard | ||||
| import im.vector.riotx.core.extensions.isEmail | ||||
| import im.vector.riotx.core.extensions.setTextOrHide | ||||
| import kotlinx.android.parcel.Parcelize | ||||
| import kotlinx.android.synthetic.main.fragment_login_generic_text_input_form.* | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| enum class TextInputFormFragmentMode { | ||||
|     SetEmail, | ||||
|     SetMsisdn, | ||||
|     ConfirmMsisdn | ||||
| } | ||||
| 
 | ||||
| @Parcelize | ||||
| data class LoginGenericTextInputFormFragmentArgument( | ||||
|         val mode: TextInputFormFragmentMode, | ||||
|         val mandatory: Boolean, | ||||
|         val extra: String = "" | ||||
| ) : Parcelable | ||||
| 
 | ||||
| /** | ||||
|  * In this screen, the user is asked for a text input | ||||
|  */ | ||||
| class LoginGenericTextInputFormFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : AbstractLoginFragment() { | ||||
| 
 | ||||
|     private val params: LoginGenericTextInputFormFragmentArgument by args() | ||||
| 
 | ||||
|     override fun getLayoutResId() = R.layout.fragment_login_generic_text_input_form | ||||
| 
 | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
| 
 | ||||
|         setupUi() | ||||
|         setupSubmitButton() | ||||
|         setupTil() | ||||
|         setupAutoFill() | ||||
|     } | ||||
| 
 | ||||
|     private fun setupAutoFill() { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             loginGenericTextInputFormTextInput.setAutofillHints( | ||||
|                     when (params.mode) { | ||||
|                         TextInputFormFragmentMode.SetEmail      -> HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS | ||||
|                         TextInputFormFragmentMode.SetMsisdn     -> HintConstants.AUTOFILL_HINT_PHONE_NUMBER | ||||
|                         TextInputFormFragmentMode.ConfirmMsisdn -> HintConstants.AUTOFILL_HINT_SMS_OTP | ||||
|                     } | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun setupTil() { | ||||
|         loginGenericTextInputFormTextInput.textChanges() | ||||
|                 .subscribe { | ||||
|                     loginGenericTextInputFormTil.error = null | ||||
|                 } | ||||
|                 .disposeOnDestroyView() | ||||
|     } | ||||
| 
 | ||||
|     private fun setupUi() { | ||||
|         when (params.mode) { | ||||
|             TextInputFormFragmentMode.SetEmail      -> { | ||||
|                 loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title) | ||||
|                 loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice) | ||||
|                 loginGenericTextInputFormNotice2.setTextOrHide(null) | ||||
|                 loginGenericTextInputFormTil.hint = | ||||
|                         getString(if (params.mandatory) R.string.login_set_email_mandatory_hint else R.string.login_set_email_optional_hint) | ||||
|                 loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | ||||
|                 loginGenericTextInputFormOtherButton.isVisible = false | ||||
|                 loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit) | ||||
|             } | ||||
|             TextInputFormFragmentMode.SetMsisdn     -> { | ||||
|                 loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title) | ||||
|                 loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice) | ||||
|                 loginGenericTextInputFormNotice2.setTextOrHide(getString(R.string.login_set_msisdn_notice2)) | ||||
|                 loginGenericTextInputFormTil.hint = | ||||
|                         getString(if (params.mandatory) R.string.login_set_msisdn_mandatory_hint else R.string.login_set_msisdn_optional_hint) | ||||
|                 loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE | ||||
|                 loginGenericTextInputFormOtherButton.isVisible = false | ||||
|                 loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit) | ||||
|             } | ||||
|             TextInputFormFragmentMode.ConfirmMsisdn -> { | ||||
|                 loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title) | ||||
|                 loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice, params.extra) | ||||
|                 loginGenericTextInputFormNotice2.setTextOrHide(null) | ||||
|                 loginGenericTextInputFormTil.hint = | ||||
|                         getString(R.string.login_msisdn_confirm_hint) | ||||
|                 loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER | ||||
|                 loginGenericTextInputFormOtherButton.isVisible = true | ||||
|                 loginGenericTextInputFormOtherButton.text = getString(R.string.login_msisdn_confirm_send_again) | ||||
|                 loginGenericTextInputFormSubmit.text = getString(R.string.login_msisdn_confirm_submit) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.loginGenericTextInputFormOtherButton) | ||||
|     fun onOtherButtonClicked() { | ||||
|         when (params.mode) { | ||||
|             TextInputFormFragmentMode.ConfirmMsisdn -> { | ||||
|                 loginViewModel.handle(LoginAction.SendAgainThreePid) | ||||
|             } | ||||
|             else                                    -> { | ||||
|                 // Should not happen, button is not displayed | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.loginGenericTextInputFormSubmit) | ||||
|     fun submit() { | ||||
|         cleanupUi() | ||||
|         val text = loginGenericTextInputFormTextInput.text.toString() | ||||
| 
 | ||||
|         if (text.isEmpty()) { | ||||
|             // Perform dummy action | ||||
|             loginViewModel.handle(LoginAction.RegisterDummy) | ||||
|         } else { | ||||
|             when (params.mode) { | ||||
|                 TextInputFormFragmentMode.SetEmail      -> { | ||||
|                     loginViewModel.handle(LoginAction.AddThreePid(RegisterThreePid.Email(text))) | ||||
|                 } | ||||
|                 TextInputFormFragmentMode.SetMsisdn     -> { | ||||
|                     getCountryCodeOrShowError(text)?.let { countryCode -> | ||||
|                         loginViewModel.handle(LoginAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode))) | ||||
|                     } | ||||
|                 } | ||||
|                 TextInputFormFragmentMode.ConfirmMsisdn -> { | ||||
|                     loginViewModel.handle(LoginAction.ValidateThreePid(text)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun cleanupUi() { | ||||
|         loginGenericTextInputFormSubmit.hideKeyboard() | ||||
|         loginGenericTextInputFormSubmit.error = null | ||||
|     } | ||||
| 
 | ||||
|     private fun getCountryCodeOrShowError(text: String): String? { | ||||
|         // We expect an international format for the moment (see https://github.com/vector-im/riotX-android/issues/693) | ||||
|         if (text.startsWith("+")) { | ||||
|             try { | ||||
|                 val phoneNumber = PhoneNumberUtil.getInstance().parse(text, null) | ||||
|                 return PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode) | ||||
|             } catch (e: NumberParseException) { | ||||
|                 loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_other) | ||||
|             } | ||||
|         } else { | ||||
|             loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_not_international) | ||||
|         } | ||||
| 
 | ||||
|         // Error | ||||
|         return null | ||||
|     } | ||||
| 
 | ||||
|     private fun setupSubmitButton() { | ||||
|         loginGenericTextInputFormSubmit.isEnabled = false | ||||
|         loginGenericTextInputFormTextInput.textChanges() | ||||
|                 .subscribe { | ||||
|                     loginGenericTextInputFormSubmit.isEnabled = isInputValid(it) | ||||
|                 } | ||||
|                 .disposeOnDestroyView() | ||||
|     } | ||||
| 
 | ||||
|     private fun isInputValid(input: CharSequence): Boolean { | ||||
|         return if (input.isEmpty() && !params.mandatory) { | ||||
|             true | ||||
|         } else { | ||||
|             when (params.mode) { | ||||
|                 TextInputFormFragmentMode.SetEmail      -> { | ||||
|                     input.isEmail() | ||||
|                 } | ||||
|                 TextInputFormFragmentMode.SetMsisdn     -> { | ||||
|                     input.isNotBlank() | ||||
|                 } | ||||
|                 TextInputFormFragmentMode.ConfirmMsisdn -> { | ||||
|                     input.isNotBlank() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onError(throwable: Throwable) { | ||||
|         when (params.mode) { | ||||
|             TextInputFormFragmentMode.SetEmail      -> { | ||||
|                 if (throwable.is401()) { | ||||
|                     // This is normal use case, we go to the mail waiting screen | ||||
|                     loginSharedActionViewModel.post(LoginNavigation.OnSendEmailSuccess(loginViewModel.currentThreePid ?: "")) | ||||
|                 } else { | ||||
|                     loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) | ||||
|                 } | ||||
|             } | ||||
|             TextInputFormFragmentMode.SetMsisdn     -> { | ||||
|                 if (throwable.is401()) { | ||||
|                     // This is normal use case, we go to the enter code screen | ||||
|                     loginSharedActionViewModel.post(LoginNavigation.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: "")) | ||||
|                 } else { | ||||
|                     loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) | ||||
|                 } | ||||
|             } | ||||
|             TextInputFormFragmentMode.ConfirmMsisdn -> { | ||||
|                 when { | ||||
|                     throwable is Failure.SuccessError -> | ||||
|                         // The entered code is not correct | ||||
|                         loginGenericTextInputFormTil.error = getString(R.string.login_validation_code_is_not_correct) | ||||
|                     throwable.is401()                 -> | ||||
|                         // It can happen if user request again the 3pid | ||||
|                         Unit | ||||
|                     else                              -> | ||||
|                         loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun resetViewModel() { | ||||
|         loginViewModel.handle(LoginAction.ResetLogin) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,24 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| enum class LoginMode { | ||||
|     Unknown, | ||||
|     Password, | ||||
|     Sso, | ||||
|     Unsupported | ||||
| } | ||||
| @ -0,0 +1,36 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import im.vector.riotx.core.platform.VectorSharedAction | ||||
| 
 | ||||
| // Supported navigation actions for LoginActivity | ||||
| sealed class LoginNavigation : VectorSharedAction { | ||||
|     object OpenServerSelection : LoginNavigation() | ||||
|     object OnServerSelectionDone : LoginNavigation() | ||||
|     object OnLoginFlowRetrieved : LoginNavigation() | ||||
|     object OnSignModeSelected : LoginNavigation() | ||||
|     object OnForgetPasswordClicked : LoginNavigation() | ||||
|     object OnResetPasswordSendThreePidDone : LoginNavigation() | ||||
|     object OnResetPasswordMailConfirmationSuccess : LoginNavigation() | ||||
|     object OnResetPasswordMailConfirmationSuccessDone : LoginNavigation() | ||||
| 
 | ||||
|     data class OnSendEmailSuccess(val email: String) : LoginNavigation() | ||||
|     data class OnSendMsisdnSuccess(val msisdn: String) : LoginNavigation() | ||||
| 
 | ||||
|     data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() | ||||
| } | ||||
| @ -0,0 +1,166 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import android.view.View | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import butterknife.OnClick | ||||
| import com.airbnb.mvrx.Fail | ||||
| import com.airbnb.mvrx.Loading | ||||
| import com.airbnb.mvrx.Success | ||||
| import com.jakewharton.rxbinding3.widget.textChanges | ||||
| import im.vector.riotx.R | ||||
| import im.vector.riotx.core.error.ErrorFormatter | ||||
| import im.vector.riotx.core.extensions.hideKeyboard | ||||
| import im.vector.riotx.core.extensions.isEmail | ||||
| import im.vector.riotx.core.extensions.showPassword | ||||
| import io.reactivex.Observable | ||||
| import io.reactivex.functions.BiFunction | ||||
| import io.reactivex.rxkotlin.subscribeBy | ||||
| import kotlinx.android.synthetic.main.fragment_login_reset_password.* | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * In this screen, the user is asked for email and new password to reset his password | ||||
|  */ | ||||
| class LoginResetPasswordFragment @Inject constructor( | ||||
|         private val errorFormatter: ErrorFormatter | ||||
| ) : AbstractLoginFragment() { | ||||
| 
 | ||||
|     private var passwordShown = false | ||||
| 
 | ||||
|     // Show warning only once | ||||
|     private var showWarning = true | ||||
| 
 | ||||
|     override fun getLayoutResId() = R.layout.fragment_login_reset_password | ||||
| 
 | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
| 
 | ||||
|         setupSubmitButton() | ||||
|         setupPasswordReveal() | ||||
|     } | ||||
| 
 | ||||
|     private fun setupUi(state: LoginViewState) { | ||||
|         resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlSimple) | ||||
|     } | ||||
| 
 | ||||
|     private fun setupSubmitButton() { | ||||
|         Observable | ||||
|                 .combineLatest( | ||||
|                         resetPasswordEmail.textChanges().map { it.isEmail() }, | ||||
|                         passwordField.textChanges().map { it.isNotEmpty() }, | ||||
|                         BiFunction<Boolean, Boolean, Boolean> { isEmail, isPasswordNotEmpty -> | ||||
|                             isEmail && isPasswordNotEmpty | ||||
|                         } | ||||
|                 ) | ||||
|                 .subscribeBy { | ||||
|                     resetPasswordEmailTil.error = null | ||||
|                     passwordFieldTil.error = null | ||||
|                     resetPasswordSubmit.isEnabled = it | ||||
|                 } | ||||
|                 .disposeOnDestroyView() | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.resetPasswordSubmit) | ||||
|     fun submit() { | ||||
|         cleanupUi() | ||||
| 
 | ||||
|         if (showWarning) { | ||||
|             showWarning = false | ||||
|             // Display a warning as Riot-Web does first | ||||
|             AlertDialog.Builder(requireActivity()) | ||||
|                     .setTitle(R.string.login_reset_password_warning_title) | ||||
|                     .setMessage(R.string.login_reset_password_warning_content) | ||||
|                     .setPositiveButton(R.string.login_reset_password_warning_submit) { _, _ -> | ||||
|                         doSubmit() | ||||
|                     } | ||||
|                     .setNegativeButton(R.string.cancel, null) | ||||
|                     .show() | ||||
|         } else { | ||||
|             doSubmit() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun doSubmit() { | ||||
|         val email = resetPasswordEmail.text.toString() | ||||
|         val password = passwordField.text.toString() | ||||
| 
 | ||||
|         loginViewModel.handle(LoginAction.ResetPassword(email, password)) | ||||
|     } | ||||
| 
 | ||||
|     private fun cleanupUi() { | ||||
|         resetPasswordSubmit.hideKeyboard() | ||||
|         resetPasswordEmailTil.error = null | ||||
|         passwordFieldTil.error = null | ||||
|     } | ||||
| 
 | ||||
|     private fun setupPasswordReveal() { | ||||
|         passwordShown = false | ||||
| 
 | ||||
|         passwordReveal.setOnClickListener { | ||||
|             passwordShown = !passwordShown | ||||
| 
 | ||||
|             renderPasswordField() | ||||
|         } | ||||
| 
 | ||||
|         renderPasswordField() | ||||
|     } | ||||
| 
 | ||||
|     private fun renderPasswordField() { | ||||
|         passwordField.showPassword(passwordShown) | ||||
| 
 | ||||
|         if (passwordShown) { | ||||
|             passwordReveal.setImageResource(R.drawable.ic_eye_closed_black) | ||||
|             passwordReveal.contentDescription = getString(R.string.a11y_hide_password) | ||||
|         } else { | ||||
|             passwordReveal.setImageResource(R.drawable.ic_eye_black) | ||||
|             passwordReveal.contentDescription = getString(R.string.a11y_show_password) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun resetViewModel() { | ||||
|         loginViewModel.handle(LoginAction.ResetResetPassword) | ||||
|     } | ||||
| 
 | ||||
|     override fun onError(throwable: Throwable) { | ||||
|         AlertDialog.Builder(requireActivity()) | ||||
|                 .setTitle(R.string.dialog_title_error) | ||||
|                 .setMessage(errorFormatter.toHumanReadable(throwable)) | ||||
|                 .setPositiveButton(R.string.ok, null) | ||||
|                 .show() | ||||
|     } | ||||
| 
 | ||||
|     override fun updateWithState(state: LoginViewState) { | ||||
|         setupUi(state) | ||||
| 
 | ||||
|         when (state.asyncResetPassword) { | ||||
|             is Loading -> { | ||||
|                 // Ensure new password is hidden | ||||
|                 passwordShown = false | ||||
|                 renderPasswordField() | ||||
|             } | ||||
|             is Fail    -> { | ||||
|                 resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error) | ||||
|             } | ||||
|             is Success -> { | ||||
|                 loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSendThreePidDone) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,82 @@ | ||||
| /* | ||||
|  * Copyright 2019 New Vector Ltd | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.riotx.features.login | ||||
| 
 | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import butterknife.OnClick | ||||
| import com.airbnb.mvrx.Fail | ||||
| import com.airbnb.mvrx.Success | ||||
| import im.vector.riotx.R | ||||
| import im.vector.riotx.core.error.ErrorFormatter | ||||
| import im.vector.riotx.core.error.is401 | ||||
| import kotlinx.android.synthetic.main.fragment_login_reset_password_mail_confirmation.* | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * In this screen, the user is asked to check his email and to click on a button once it's done | ||||
|  */ | ||||
| class LoginResetPasswordMailConfirmationFragment @Inject constructor( | ||||
|         private val errorFormatter: ErrorFormatter | ||||
| ) : AbstractLoginFragment() { | ||||
| 
 | ||||
|     override fun getLayoutResId() = R.layout.fragment_login_reset_password_mail_confirmation | ||||
| 
 | ||||
|     private fun setupUi(state: LoginViewState) { | ||||
|         resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetPasswordEmail) | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.resetPasswordMailConfirmationSubmit) | ||||
|     fun submit() { | ||||
|         loginViewModel.handle(LoginAction.ResetPasswordMailConfirmed) | ||||
|     } | ||||
| 
 | ||||
|     override fun onError(throwable: Throwable) { | ||||
|         AlertDialog.Builder(requireActivity()) | ||||
|                 .setTitle(R.string.dialog_title_error) | ||||
|                 .setMessage(errorFormatter.toHumanReadable(throwable)) | ||||
|                 .setPositiveButton(R.string.ok, null) | ||||
|                 .show() | ||||
|     } | ||||
| 
 | ||||
|     override fun resetViewModel() { | ||||
|         loginViewModel.handle(LoginAction.ResetResetPassword) | ||||
|     } | ||||
| 
 | ||||
|     override fun updateWithState(state: LoginViewState) { | ||||
|         setupUi(state) | ||||
| 
 | ||||
|         when (state.asyncResetMailConfirmed) { | ||||
|             is Fail    -> { | ||||
|                 // Link in email not yet clicked ? | ||||
|                 val message = if (state.asyncResetMailConfirmed.error.is401()) { | ||||
|                     getString(R.string.auth_reset_password_error_unauthorized) | ||||
|                 } else { | ||||
|                     errorFormatter.toHumanReadable(state.asyncResetMailConfirmed.error) | ||||
|                 } | ||||
| 
 | ||||
|                 AlertDialog.Builder(requireActivity()) | ||||
|                         .setTitle(R.string.dialog_title_error) | ||||
|                         .setMessage(message) | ||||
|                         .setPositiveButton(R.string.ok, null) | ||||
|                         .show() | ||||
|             } | ||||
|             is Success -> { | ||||
|                 loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccess) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user