mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 21:48:50 +03:00
Merge pull request #1451 from vector-im/feature/sso_redirect
Correctly handle SSO login redirection
This commit is contained in:
commit
dffe096c59
37 changed files with 418 additions and 79 deletions
|
@ -9,6 +9,8 @@ Improvements 🙌:
|
||||||
- New wording for notice when current user is the sender
|
- New wording for notice when current user is the sender
|
||||||
- Hide "X made no changes" event by default in timeline (#1430)
|
- Hide "X made no changes" event by default in timeline (#1430)
|
||||||
- Hide left rooms in breadcrumbs (#766)
|
- Hide left rooms in breadcrumbs (#766)
|
||||||
|
- Correctly handle SSO login redirection
|
||||||
|
- SSO login is now performed in the default browser, or in Chrome Custom tab if available (#1400)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Switch theme is not fully taken into account without restarting the app
|
- Switch theme is not fully taken into account without restarting the app
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
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.
|
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
|
## Sign in flows
|
||||||
|
|
||||||
### Get the flow
|
### Get the flow
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ We get credential (200)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"user_id": "@alice:matrix.org",
|
"user_id": "@alice:matrix.org",
|
||||||
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
|
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lr",
|
||||||
"home_server": "matrix.org",
|
"home_server": "matrix.org",
|
||||||
"device_id": "GTVREDALBF",
|
"device_id": "GTVREDALBF",
|
||||||
"well_known": {
|
"well_known": {
|
||||||
|
@ -117,7 +117,7 @@ We get the credentials (200)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"user_id": "@alice:matrix.org",
|
"user_id": "@alice:matrix.org",
|
||||||
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg",
|
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3Jnfrfdegfszsefddvf",
|
||||||
"home_server": "matrix.org",
|
"home_server": "matrix.org",
|
||||||
"device_id": "WBSREDASND",
|
"device_id": "WBSREDASND",
|
||||||
"well_known": {
|
"well_known": {
|
||||||
|
@ -145,12 +145,59 @@ Not supported yet in RiotX
|
||||||
"flows": [
|
"flows": [
|
||||||
{
|
{
|
||||||
"type": "m.login.sso"
|
"type": "m.login.sso"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "m.login.token"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page
|
||||||
|
|
||||||
|
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=riotx%3A%2F%2Friotx
|
||||||
|
|
||||||
|
The parameter `redirectUrl` is set to `riotx://riotx`.
|
||||||
|
|
||||||
|
ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs)
|
||||||
|
|
||||||
|
The browser will then take care of the SSO login, which may include creating a third party account, entering an email, settings a display name, or any other possibilities.
|
||||||
|
|
||||||
|
During the process, user may be asked to validate an email by clicking on a link it contains. The link has to be opened in the browser which initiates the authentication. This is why we cannot use WebView anymore.
|
||||||
|
|
||||||
|
Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken`
|
||||||
|
|
||||||
|
> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
|
||||||
|
|
||||||
|
This navigation is intercepted by RiotX by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
|
||||||
|
|
||||||
|
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "m.login.token",
|
||||||
|
"token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We get the credentials (200)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "@alice:homeserver.with.sso",
|
||||||
|
"access_token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVyIGtleQowMDEwY2lkIGdlbiA9IDEKMDAyY2NpZCB1c2",
|
||||||
|
"home_server": "homeserver.with.sso",
|
||||||
|
"device_id": "DETBTVAHCH",
|
||||||
|
"well_known": {
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https:\/\/homeserver.with.sso\/"
|
||||||
|
},
|
||||||
|
"m.identity_server": {
|
||||||
|
"base_url": "https:\/\/vector.im"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Reset password
|
## Reset password
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,6 @@ const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/"
|
||||||
* Path to use when the client want to connect using SSO
|
* Path to use when the client want to connect using SSO
|
||||||
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
|
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
|
||||||
*/
|
*/
|
||||||
const val SSO_FALLBACK_PATH = "/_matrix/client/r0/login/sso/redirect"
|
const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
|
||||||
|
|
||||||
const val SSO_REDIRECT_URL_PARAM = "redirectUrl"
|
const val SSO_REDIRECT_URL_PARAM = "redirectUrl"
|
||||||
|
|
|
@ -18,10 +18,10 @@ package im.vector.matrix.android.api.auth.data
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||||
|
|
||||||
// Either a LoginFlowResponse, or an error if the homeserver is outdated
|
// Either a list of supported login types, or an error if the homeserver is outdated
|
||||||
sealed class LoginFlowResult {
|
sealed class LoginFlowResult {
|
||||||
data class Success(
|
data class Success(
|
||||||
val loginFlowResponse: LoginFlowResponse,
|
val supportedLoginTypes: List<String>,
|
||||||
val isLoginAndRegistrationSupported: Boolean,
|
val isLoginAndRegistrationSupported: Boolean,
|
||||||
val homeServerUrl: String
|
val homeServerUrl: String
|
||||||
) : LoginFlowResult()
|
) : LoginFlowResult()
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.auth.data
|
package im.vector.matrix.android.api.auth.data
|
||||||
|
|
||||||
object LoginFlowTypes {
|
object LoginFlowTypes {
|
||||||
const val PASSWORD = "m.login.password"
|
const val PASSWORD = "m.login.password"
|
|
@ -34,6 +34,12 @@ interface LoginWizard {
|
||||||
deviceName: String,
|
deviceName: String,
|
||||||
callback: MatrixCallback<Session>): Cancelable
|
callback: MatrixCallback<Session>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange a login token to an access token
|
||||||
|
*/
|
||||||
|
fun loginWithToken(loginToken: String,
|
||||||
|
callback: MatrixCallback<Session>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset user password
|
* Reset user password
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -21,15 +21,12 @@ sealed class Stage(open val mandatory: Boolean) {
|
||||||
// m.login.recaptcha
|
// m.login.recaptcha
|
||||||
data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
|
data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
|
||||||
|
|
||||||
// m.login.oauth2
|
|
||||||
// m.login.email.identity
|
// m.login.email.identity
|
||||||
data class Email(override val mandatory: Boolean) : Stage(mandatory)
|
data class Email(override val mandatory: Boolean) : Stage(mandatory)
|
||||||
|
|
||||||
// m.login.msisdn
|
// m.login.msisdn
|
||||||
data class Msisdn(override val mandatory: Boolean) : Stage(mandatory)
|
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
|
// 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
|
// and a password, the dummy stage has to be done
|
||||||
data class Dummy(override val mandatory: Boolean) : Stage(mandatory)
|
data class Dummy(override val mandatory: Boolean) : Stage(mandatory)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.matrix.android.api.auth.data.Versions
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
||||||
import im.vector.matrix.android.internal.auth.data.RiotConfig
|
import im.vector.matrix.android.internal.auth.data.RiotConfig
|
||||||
|
import im.vector.matrix.android.internal.auth.data.TokenLoginParams
|
||||||
import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
|
import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
|
||||||
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
|
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.AddThreePidRegistrationResponse
|
||||||
|
@ -54,7 +55,7 @@ internal interface AuthAPI {
|
||||||
fun versions(): Call<Versions>
|
fun versions(): Call<Versions>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register to the homeserver
|
* Register to the homeserver, or get error 401 with a RegistrationFlowResponse object if registration is incomplete
|
||||||
* Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
|
* Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
|
||||||
*/
|
*/
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
|
||||||
|
@ -91,6 +92,11 @@ internal interface AuthAPI {
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||||
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>
|
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>
|
||||||
|
|
||||||
|
// Unfortunately we cannot use interface for @Body parameter, so I duplicate the method for the type TokenLoginParams
|
||||||
|
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||||
|
fun login(@Body loginParams: TokenLoginParams): Call<Credentials>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask the homeserver to reset the password associated with the provided email.
|
* Ask the homeserver to reset the password associated with the provided email.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -236,7 +236,7 @@ internal class DefaultAuthenticationService @Inject constructor(
|
||||||
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
|
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
|
||||||
apiCall = authAPI.getLoginFlows()
|
apiCall = authAPI.getLoginFlows()
|
||||||
}
|
}
|
||||||
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
|
LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
|
||||||
} else {
|
} else {
|
||||||
// Not supported
|
// Not supported
|
||||||
LoginFlowResult.OutdatedHomeserver
|
LoginFlowResult.OutdatedHomeserver
|
||||||
|
|
|
@ -20,7 +20,19 @@ import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LoginFlowResponse(
|
internal data class LoginFlowResponse(
|
||||||
|
/**
|
||||||
|
* The homeserver's supported login types
|
||||||
|
*/
|
||||||
@Json(name = "flows")
|
@Json(name = "flows")
|
||||||
val flows: List<InteractiveAuthenticationFlow>
|
val flows: List<LoginFlow>?
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class LoginFlow(
|
||||||
|
/**
|
||||||
|
* The login type. This is supplied as the type when logging in.
|
||||||
|
*/
|
||||||
|
@Json(name = "type")
|
||||||
|
val type: String?
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.data
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ref:
|
* Ref:
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.data
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class TokenLoginParams(
|
||||||
|
@Json(name = "type") override val type: String = LoginFlowTypes.TOKEN,
|
||||||
|
@Json(name = "token") val token: String
|
||||||
|
) : LoginParams
|
|
@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.auth.PendingSessionStore
|
||||||
import im.vector.matrix.android.internal.auth.SessionCreator
|
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.PasswordLoginParams
|
||||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||||
|
import im.vector.matrix.android.internal.auth.data.TokenLoginParams
|
||||||
import im.vector.matrix.android.internal.auth.db.PendingSessionData
|
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.AddThreePidRegistrationParams
|
||||||
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
|
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
|
||||||
|
@ -65,6 +66,22 @@ internal class DefaultLoginWizard(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/latest#handling-the-authentication-endpoint
|
||||||
|
*/
|
||||||
|
override fun loginWithToken(loginToken: String, callback: MatrixCallback<Session>): Cancelable {
|
||||||
|
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||||
|
val loginParams = TokenLoginParams(
|
||||||
|
token = loginToken
|
||||||
|
)
|
||||||
|
val credentials = executeRequest<Credentials>(null) {
|
||||||
|
apiCall = authAPI.login(loginParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun loginInternal(login: String,
|
private suspend fun loginInternal(login: String,
|
||||||
password: String,
|
password: String,
|
||||||
deviceName: String) = withContext(coroutineDispatchers.computation) {
|
deviceName: String) = withContext(coroutineDispatchers.computation) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.auth.registration
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open class, parent to all possible authentication parameters
|
* Open class, parent to all possible authentication parameters
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.registration
|
||||||
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
|
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.RegistrationResult
|
||||||
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
|
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
|
||||||
|
@ -28,7 +29,6 @@ import im.vector.matrix.android.api.util.NoOpCancellable
|
||||||
import im.vector.matrix.android.internal.auth.AuthAPI
|
import im.vector.matrix.android.internal.auth.AuthAPI
|
||||||
import im.vector.matrix.android.internal.auth.PendingSessionStore
|
import im.vector.matrix.android.internal.auth.PendingSessionStore
|
||||||
import im.vector.matrix.android.internal.auth.SessionCreator
|
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.auth.db.PendingSessionData
|
||||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.task.launchToCallback
|
import im.vector.matrix.android.internal.task.launchToCallback
|
||||||
|
|
|
@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.auth.registration
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.api.auth.registration.FlowResult
|
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.Stage
|
||||||
import im.vector.matrix.android.api.auth.registration.TermPolicies
|
import im.vector.matrix.android.api.auth.registration.TermPolicies
|
||||||
import im.vector.matrix.android.api.util.JsonDict
|
import im.vector.matrix.android.api.util.JsonDict
|
||||||
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
|
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class RegistrationFlowResponse(
|
data class RegistrationFlowResponse(
|
||||||
|
|
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides the authentication data by using user and password
|
* This class provides the authentication data by using user and password
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.tasks
|
package im.vector.matrix.android.internal.crypto.tasks
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
|
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
|
|
@ -332,6 +332,9 @@ dependencies {
|
||||||
implementation 'com.google.android:flexbox:1.1.1'
|
implementation 'com.google.android:flexbox:1.1.1'
|
||||||
implementation "androidx.autofill:autofill:$autofill_version"
|
implementation "androidx.autofill:autofill:$autofill_version"
|
||||||
|
|
||||||
|
// Custom Tab
|
||||||
|
implementation 'androidx.browser:browser:1.2.0'
|
||||||
|
|
||||||
// Passphrase strength helper
|
// Passphrase strength helper
|
||||||
implementation 'com.nulab-inc:zxcvbn:1.2.7'
|
implementation 'com.nulab-inc:zxcvbn:1.2.7'
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,19 @@
|
||||||
<activity android:name=".features.home.HomeActivity" />
|
<activity android:name=".features.home.HomeActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.login.LoginActivity"
|
android:name=".features.login.LoginActivity"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:launchMode="singleTask"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="riotx" />
|
||||||
|
<data android:host="riotx" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity android:name=".features.media.ImageMediaViewerActivity" />
|
<activity android:name=".features.media.ImageMediaViewerActivity" />
|
||||||
<activity android:name=".features.media.BigImageViewerActivity" />
|
<activity android:name=".features.media.BigImageViewerActivity" />
|
||||||
<activity
|
<activity
|
||||||
|
|
|
@ -59,6 +59,7 @@ import im.vector.riotx.features.login.LoginResetPasswordSuccessFragment
|
||||||
import im.vector.riotx.features.login.LoginServerSelectionFragment
|
import im.vector.riotx.features.login.LoginServerSelectionFragment
|
||||||
import im.vector.riotx.features.login.LoginServerUrlFormFragment
|
import im.vector.riotx.features.login.LoginServerUrlFormFragment
|
||||||
import im.vector.riotx.features.login.LoginSignUpSignInSelectionFragment
|
import im.vector.riotx.features.login.LoginSignUpSignInSelectionFragment
|
||||||
|
import im.vector.riotx.features.login.LoginSignUpSignInSsoFragment
|
||||||
import im.vector.riotx.features.login.LoginSplashFragment
|
import im.vector.riotx.features.login.LoginSplashFragment
|
||||||
import im.vector.riotx.features.login.LoginWaitForEmailFragment
|
import im.vector.riotx.features.login.LoginWaitForEmailFragment
|
||||||
import im.vector.riotx.features.login.LoginWebFragment
|
import im.vector.riotx.features.login.LoginWebFragment
|
||||||
|
@ -217,6 +218,11 @@ interface FragmentModule {
|
||||||
@FragmentKey(LoginSignUpSignInSelectionFragment::class)
|
@FragmentKey(LoginSignUpSignInSelectionFragment::class)
|
||||||
fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment
|
fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(LoginSignUpSignInSsoFragment::class)
|
||||||
|
fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(LoginSplashFragment::class)
|
@FragmentKey(LoginSplashFragment::class)
|
||||||
|
|
|
@ -21,10 +21,14 @@ import android.content.ActivityNotFoundException
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Browser
|
import android.provider.Browser
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import androidx.browser.customtabs.CustomTabsSession
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.vector.riotx.BuildConfig
|
import im.vector.riotx.BuildConfig
|
||||||
|
@ -64,6 +68,34 @@ fun openUrlInExternalBrowser(context: Context, uri: Uri?) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open url in custom tab or, if not available, in the default browser
|
||||||
|
* If several compatible browsers are installed, the user will be proposed to choose one.
|
||||||
|
* Ref: https://developer.chrome.com/multidevice/android/customtabs
|
||||||
|
*/
|
||||||
|
fun openUrlInChromeCustomTab(context: Context, session: CustomTabsSession?, url: String) {
|
||||||
|
try {
|
||||||
|
CustomTabsIntent.Builder()
|
||||||
|
.setToolbarColor(ContextCompat.getColor(context, R.color.riotx_background_light))
|
||||||
|
.apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_header_panel_background_light))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_background_light))
|
||||||
|
.setColorScheme(CustomTabsIntent.COLOR_SCHEME_LIGHT)
|
||||||
|
// Note: setting close button icon does not work
|
||||||
|
.setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp))
|
||||||
|
.setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
|
||||||
|
.setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
|
||||||
|
.apply { session?.let { setSession(it) } }
|
||||||
|
.build()
|
||||||
|
.launchUrl(context, Uri.parse(url))
|
||||||
|
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||||
|
context.toast(R.string.error_no_external_application_found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open sound recorder external application
|
* Open sound recorder external application
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.crypto.recover
|
package im.vector.riotx.features.crypto.recover
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.failure.MatrixError
|
import im.vector.matrix.android.api.failure.MatrixError
|
||||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||||
|
@ -28,7 +29,6 @@ import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner
|
||||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||||
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||||
|
|
|
@ -24,6 +24,7 @@ sealed class LoginAction : VectorViewModelAction {
|
||||||
data class UpdateServerType(val serverType: ServerType) : LoginAction()
|
data class UpdateServerType(val serverType: ServerType) : LoginAction()
|
||||||
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
|
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
|
||||||
data class UpdateSignMode(val signMode: SignMode) : LoginAction()
|
data class UpdateSignMode(val signMode: SignMode) : LoginAction()
|
||||||
|
data class LoginWithToken(val loginToken: String) : LoginAction()
|
||||||
data class WebLoginSuccess(val credentials: Credentials) : LoginAction()
|
data class WebLoginSuccess(val credentials: Credentials) : LoginAction()
|
||||||
data class InitWith(val loginConfig: LoginConfig) : LoginAction()
|
data class InitWith(val loginConfig: LoginConfig) : LoginAction()
|
||||||
data class ResetPassword(val email: String, val newPassword: String) : LoginAction()
|
data class ResetPassword(val email: String, val newPassword: String) : LoginAction()
|
||||||
|
|
|
@ -33,6 +33,7 @@ import com.airbnb.mvrx.viewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.matrix.android.api.auth.registration.FlowResult
|
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.Stage
|
||||||
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE
|
import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE
|
||||||
|
@ -155,7 +156,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected()
|
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected()
|
||||||
is LoginViewEvents.OnLoginFlowRetrieved ->
|
is LoginViewEvents.OnLoginFlowRetrieved ->
|
||||||
addFragmentToBackstack(R.id.loginFragmentContainer,
|
addFragmentToBackstack(R.id.loginFragmentContainer,
|
||||||
LoginSignUpSignInSelectionFragment::class.java,
|
if (loginViewEvents.isSso) {
|
||||||
|
LoginSignUpSignInSsoFragment::class.java
|
||||||
|
} else {
|
||||||
|
LoginSignUpSignInSelectionFragment::class.java
|
||||||
|
},
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents)
|
is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents)
|
||||||
is LoginViewEvents.OnForgetPasswordClicked ->
|
is LoginViewEvents.OnForgetPasswordClicked ->
|
||||||
|
@ -239,16 +244,14 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
SignMode.SignIn -> {
|
SignMode.SignIn -> {
|
||||||
// It depends on the LoginMode
|
// It depends on the LoginMode
|
||||||
when (state.loginMode) {
|
when (state.loginMode) {
|
||||||
LoginMode.Unknown -> error("Developer error")
|
LoginMode.Unknown,
|
||||||
|
LoginMode.Sso -> error("Developer error")
|
||||||
LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer,
|
LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer,
|
||||||
LoginFragment::class.java,
|
LoginFragment::class.java,
|
||||||
tag = FRAGMENT_LOGIN_TAG,
|
tag = FRAGMENT_LOGIN_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer,
|
|
||||||
LoginWebFragment::class.java,
|
|
||||||
option = commonOption)
|
|
||||||
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
|
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
|
||||||
}
|
}.exhaustive
|
||||||
}
|
}
|
||||||
SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer,
|
SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer,
|
||||||
LoginFragment::class.java,
|
LoginFragment::class.java,
|
||||||
|
@ -257,6 +260,17 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the SSO redirection here
|
||||||
|
*/
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
|
intent?.data
|
||||||
|
?.let { tryThis { it.getQueryParameter("loginToken") } }
|
||||||
|
?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun onRegistrationStageNotSupported() {
|
private fun onRegistrationStageNotSupported() {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.app_name)
|
.setTitle(R.string.app_name)
|
||||||
|
|
|
@ -55,7 +55,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.loginServerChoiceModularLearnMore)
|
@OnClick(R.id.loginServerChoiceModularLearnMore)
|
||||||
fun learMore() {
|
fun learnMore() {
|
||||||
openUrlInExternalBrowser(requireActivity(), MODULAR_LINK)
|
openUrlInExternalBrowser(requireActivity(), MODULAR_LINK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
|
||||||
|
|
||||||
if (state.loginMode != LoginMode.Unknown) {
|
if (state.loginMode != LoginMode.Unknown) {
|
||||||
// LoginFlow for matrix.org has been retrieved
|
// LoginFlow for matrix.org has been retrieved
|
||||||
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
|
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
|
||||||
|
|
||||||
if (state.loginMode != LoginMode.Unknown) {
|
if (state.loginMode != LoginMode.Unknown) {
|
||||||
// The home server url is valid
|
// The home server url is valid
|
||||||
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
|
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,13 +26,11 @@ import javax.inject.Inject
|
||||||
/**
|
/**
|
||||||
* In this screen, the user is asked to sign up or to sign in to the homeserver
|
* In this screen, the user is asked to sign up or to sign in to the homeserver
|
||||||
*/
|
*/
|
||||||
class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
|
open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection
|
override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection
|
||||||
|
|
||||||
private var isSsoSignIn: Boolean = false
|
protected fun setupUi(state: LoginViewState) {
|
||||||
|
|
||||||
private fun setupUi(state: LoginViewState) {
|
|
||||||
when (state.serverType) {
|
when (state.serverType) {
|
||||||
ServerType.MatrixOrg -> {
|
ServerType.MatrixOrg -> {
|
||||||
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
|
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
|
||||||
|
@ -54,25 +52,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupButtons(state: LoginViewState) {
|
private fun setupButtons() {
|
||||||
isSsoSignIn = state.loginMode == LoginMode.Sso
|
loginSignupSigninSubmit.text = getString(R.string.login_signup)
|
||||||
|
loginSignupSigninSignIn.isVisible = true
|
||||||
if (isSsoSignIn) {
|
|
||||||
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
|
|
||||||
loginSignupSigninSignIn.isVisible = false
|
|
||||||
} else {
|
|
||||||
loginSignupSigninSubmit.text = getString(R.string.login_signup)
|
|
||||||
loginSignupSigninSignIn.isVisible = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.loginSignupSigninSubmit)
|
@OnClick(R.id.loginSignupSigninSubmit)
|
||||||
fun signUp() {
|
open fun submit() {
|
||||||
if (isSsoSignIn) {
|
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
|
||||||
signIn()
|
|
||||||
} else {
|
|
||||||
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.loginSignupSigninSignIn)
|
@OnClick(R.id.loginSignupSigninSignIn)
|
||||||
|
@ -86,6 +73,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
|
||||||
|
|
||||||
override fun updateWithState(state: LoginViewState) {
|
override fun updateWithState(state: LoginViewState) {
|
||||||
setupUi(state)
|
setupUi(state)
|
||||||
setupButtons(state)
|
setupButtons()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.content.ComponentName
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.browser.customtabs.CustomTabsClient
|
||||||
|
import androidx.browser.customtabs.CustomTabsServiceConnection
|
||||||
|
import androidx.browser.customtabs.CustomTabsSession
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.utils.openUrlInChromeCustomTab
|
||||||
|
import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this screen, the user is asked to sign up or to sign in using SSO
|
||||||
|
* This Fragment binds a CustomTabsServiceConnection if available, then prefetch the SSO url, as it will be likely to be opened.
|
||||||
|
*/
|
||||||
|
open class LoginSignUpSignInSsoFragment @Inject constructor() : LoginSignUpSignInSelectionFragment() {
|
||||||
|
|
||||||
|
private var ssoUrl: String? = null
|
||||||
|
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
|
||||||
|
private var customTabsClient: CustomTabsClient? = null
|
||||||
|
private var customTabsSession: CustomTabsSession? = null
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
|
||||||
|
val packageName = CustomTabsClient.getPackageName(requireContext(), null)
|
||||||
|
|
||||||
|
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
|
||||||
|
if (packageName != null) {
|
||||||
|
customTabsServiceConnection = object : CustomTabsServiceConnection() {
|
||||||
|
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
|
||||||
|
customTabsClient = client
|
||||||
|
.also { it.warmup(0L) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.also {
|
||||||
|
CustomTabsClient.bindCustomTabsService(
|
||||||
|
requireContext(),
|
||||||
|
// Despite the API, packageName cannot be null
|
||||||
|
packageName,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prefetchUrl(url: String) {
|
||||||
|
if (ssoUrl != null) return
|
||||||
|
|
||||||
|
ssoUrl = url
|
||||||
|
if (customTabsSession == null) {
|
||||||
|
customTabsSession = customTabsClient?.newSession(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
customTabsServiceConnection?.let { requireContext().unbindService(it) }
|
||||||
|
customTabsServiceConnection = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupButtons() {
|
||||||
|
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
|
||||||
|
loginSignupSigninSignIn.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun submit() {
|
||||||
|
ssoUrl?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateWithState(state: LoginViewState) {
|
||||||
|
setupUi(state)
|
||||||
|
setupButtons()
|
||||||
|
prefetchUrl(state.getSsoUrl())
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ sealed class LoginViewEvents : VectorViewEvents {
|
||||||
|
|
||||||
object OpenServerSelection : LoginViewEvents()
|
object OpenServerSelection : LoginViewEvents()
|
||||||
object OnServerSelectionDone : LoginViewEvents()
|
object OnServerSelectionDone : LoginViewEvents()
|
||||||
object OnLoginFlowRetrieved : LoginViewEvents()
|
data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents()
|
||||||
object OnSignModeSelected : LoginViewEvents()
|
object OnSignModeSelected : LoginViewEvents()
|
||||||
object OnForgetPasswordClicked : LoginViewEvents()
|
object OnForgetPasswordClicked : LoginViewEvents()
|
||||||
object OnResetPasswordSendThreePidDone : LoginViewEvents()
|
object OnResetPasswordSendThreePidDone : LoginViewEvents()
|
||||||
|
|
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.AuthenticationService
|
import im.vector.matrix.android.api.auth.AuthenticationService
|
||||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||||
import im.vector.matrix.android.api.auth.data.LoginFlowResult
|
import im.vector.matrix.android.api.auth.data.LoginFlowResult
|
||||||
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.api.auth.login.LoginWizard
|
import im.vector.matrix.android.api.auth.login.LoginWizard
|
||||||
import im.vector.matrix.android.api.auth.registration.FlowResult
|
import im.vector.matrix.android.api.auth.registration.FlowResult
|
||||||
import im.vector.matrix.android.api.auth.registration.RegistrationResult
|
import im.vector.matrix.android.api.auth.registration.RegistrationResult
|
||||||
|
@ -40,7 +41,6 @@ import im.vector.matrix.android.api.auth.registration.Stage
|
||||||
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
|
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
|
@ -110,6 +110,7 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
is LoginAction.InitWith -> handleInitWith(action)
|
is LoginAction.InitWith -> handleInitWith(action)
|
||||||
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action)
|
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action)
|
||||||
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action)
|
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action)
|
||||||
|
is LoginAction.LoginWithToken -> handleLoginWithToken(action)
|
||||||
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||||
is LoginAction.ResetPassword -> handleResetPassword(action)
|
is LoginAction.ResetPassword -> handleResetPassword(action)
|
||||||
is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
|
is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
|
||||||
|
@ -120,6 +121,41 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleLoginWithToken(action: LoginAction.LoginWithToken) {
|
||||||
|
val safeLoginWizard = loginWizard
|
||||||
|
|
||||||
|
if (safeLoginWizard == null) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
asyncLoginAction = Fail(Throwable("Bad configuration"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
asyncLoginAction = Loading()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTask = safeLoginWizard.loginWithToken(
|
||||||
|
action.loginToken,
|
||||||
|
object : MatrixCallback<Session> {
|
||||||
|
override fun onSuccess(data: Session) {
|
||||||
|
onSessionCreated(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
_viewEvents.post(LoginViewEvents.Failure(failure))
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
asyncLoginAction = Fail(failure)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) {
|
private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) {
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
|
@ -635,9 +671,9 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
is LoginFlowResult.Success -> {
|
is LoginFlowResult.Success -> {
|
||||||
val loginMode = when {
|
val loginMode = when {
|
||||||
// SSO login is taken first
|
// SSO login is taken first
|
||||||
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso
|
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
|
||||||
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
|
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
|
||||||
else -> LoginMode.Unsupported
|
else -> LoginMode.Unsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
|
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
|
||||||
|
@ -648,7 +684,7 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
asyncHomeServerLoginFlowRequest = Uninitialized,
|
asyncHomeServerLoginFlowRequest = Uninitialized,
|
||||||
homeServerUrl = data.homeServerUrl,
|
homeServerUrl = data.homeServerUrl,
|
||||||
loginMode = loginMode,
|
loginMode = loginMode,
|
||||||
loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList()
|
loginModeSupportedTypes = data.supportedLoginTypes.toList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@ import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.PersistState
|
import com.airbnb.mvrx.PersistState
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import im.vector.matrix.android.api.auth.SSO_REDIRECT_PATH
|
||||||
|
import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM
|
||||||
|
import im.vector.riotx.core.extensions.appendParamToUrl
|
||||||
|
|
||||||
data class LoginViewState(
|
data class LoginViewState(
|
||||||
val asyncLoginAction: Async<Unit> = Uninitialized,
|
val asyncLoginAction: Async<Unit> = Uninitialized,
|
||||||
|
@ -64,4 +67,22 @@ data class LoginViewState(
|
||||||
fun isUserLogged(): Boolean {
|
fun isUserLogged(): Boolean {
|
||||||
return asyncLoginAction is Success
|
return asyncLoginAction is Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSsoUrl(): String {
|
||||||
|
return buildString {
|
||||||
|
append(homeServerUrl?.trim { it == '/' })
|
||||||
|
append(SSO_REDIRECT_PATH)
|
||||||
|
// Set a redirect url we will intercept later
|
||||||
|
appendParamToUrl(SSO_REDIRECT_URL_PARAM, RIOTX_REDIRECT_URL)
|
||||||
|
deviceId?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
// But https://github.com/matrix-org/synapse/issues/5755
|
||||||
|
appendParamToUrl("device_id", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
|
||||||
|
private const val RIOTX_REDIRECT_URL = "riotx://riotx"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,6 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import com.airbnb.mvrx.activityViewModel
|
import com.airbnb.mvrx.activityViewModel
|
||||||
import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH
|
import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH
|
||||||
import im.vector.matrix.android.api.auth.REGISTER_FALLBACK_PATH
|
import im.vector.matrix.android.api.auth.REGISTER_FALLBACK_PATH
|
||||||
import im.vector.matrix.android.api.auth.SSO_FALLBACK_PATH
|
|
||||||
import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM
|
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
@ -48,7 +46,7 @@ import java.net.URLDecoder
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This screen is displayed for SSO login and also when the application does not support login flow or registration flow
|
* This screen is displayed when the application does not support login flow or registration flow
|
||||||
* of the homeserver, as a fallback to login or to create an account
|
* of the homeserver, as a fallback to login or to create an account
|
||||||
*/
|
*/
|
||||||
class LoginWebFragment @Inject constructor(
|
class LoginWebFragment @Inject constructor(
|
||||||
|
@ -128,17 +126,7 @@ class LoginWebFragment @Inject constructor(
|
||||||
val url = buildString {
|
val url = buildString {
|
||||||
append(state.homeServerUrl?.trim { it == '/' })
|
append(state.homeServerUrl?.trim { it == '/' })
|
||||||
if (state.signMode == SignMode.SignIn) {
|
if (state.signMode == SignMode.SignIn) {
|
||||||
if (state.loginMode == LoginMode.Sso) {
|
append(LOGIN_FALLBACK_PATH)
|
||||||
append(SSO_FALLBACK_PATH)
|
|
||||||
// We do not want to deal with the result, so let the fallback login page to handle it for us
|
|
||||||
appendParamToUrl(SSO_REDIRECT_URL_PARAM,
|
|
||||||
buildString {
|
|
||||||
append(state.homeServerUrl?.trim { it == '/' })
|
|
||||||
append(LOGIN_FALLBACK_PATH)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
append(LOGIN_FALLBACK_PATH)
|
|
||||||
}
|
|
||||||
state.deviceId?.takeIf { it.isNotBlank() }?.let {
|
state.deviceId?.takeIf { it.isNotBlank() }?.let {
|
||||||
// But https://github.com/matrix-org/synapse/issues/5755
|
// But https://github.com/matrix-org/synapse/issues/5755
|
||||||
appendParamToUrl("device_id", it)
|
appendParamToUrl("device_id", it)
|
||||||
|
@ -226,7 +214,9 @@ class LoginWebFragment @Inject constructor(
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
|
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
|
||||||
if (null != url && url.startsWith("js:")) {
|
if (url == null) return super.shouldOverrideUrlLoading(view, url as String?)
|
||||||
|
|
||||||
|
if (url.startsWith("js:")) {
|
||||||
var json = url.substring(3)
|
var json = url.substring(3)
|
||||||
var javascriptResponse: JavascriptResponse? = null
|
var javascriptResponse: JavascriptResponse? = null
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,10 @@ import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
|
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
|
|
|
@ -30,13 +30,13 @@ import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||||
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
|
|
|
@ -28,9 +28,9 @@ import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.AuthenticationService
|
import im.vector.matrix.android.api.auth.AuthenticationService
|
||||||
import im.vector.matrix.android.api.auth.data.LoginFlowResult
|
import im.vector.matrix.android.api.auth.data.LoginFlowResult
|
||||||
|
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.extensions.hasUnsavedKeys
|
import im.vector.riotx.core.extensions.hasUnsavedKeys
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
@ -105,9 +105,9 @@ class SoftLogoutViewModel @AssistedInject constructor(
|
||||||
is LoginFlowResult.Success -> {
|
is LoginFlowResult.Success -> {
|
||||||
val loginMode = when {
|
val loginMode = when {
|
||||||
// SSO login is taken first
|
// SSO login is taken first
|
||||||
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso
|
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
|
||||||
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
|
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
|
||||||
else -> LoginMode.Unsupported
|
else -> LoginMode.Unsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
|
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
|
||||||
|
|
23
vector/src/main/res/drawable/ic_back_24dp.xml
Normal file
23
vector/src/main/res/drawable/ic_back_24dp.xml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M20,12H4"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeColor="#2E2F32"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
tools:strokeColor="#00F000" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M10,18L4,12L10,6"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeColor="#2E2F32"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
tools:strokeColor="#00F000" />
|
||||||
|
</vector>
|
Loading…
Reference in a new issue