Login screens: registration fallback

This commit is contained in:
Benoit Marty 2019-11-19 16:04:57 +01:00
parent 1c03163a33
commit 1dc7dfc896
10 changed files with 105 additions and 77 deletions

View file

@ -31,4 +31,5 @@ data class Credentials(
@Json(name = "access_token") val accessToken: String,
@Json(name = "refresh_token") val refreshToken: String?,
@Json(name = "device_id") val deviceId: String?
// TODO Add Wellknown
)

View file

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

View file

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

View file

@ -0,0 +1 @@
javascript:window.sendObjectMessage = function(parameters) { var iframe = document.createElement('iframe'); iframe.setAttribute('src', 'js:' + JSON.stringify(parameters)); document.documentElement.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null;};

View file

@ -0,0 +1,39 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.login
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.Credentials
@JsonClass(generateAdapter = true)
data class JavascriptResponse(
@Json(name = "action")
val action: String? = null,
/**
* Use for captcha result
*/
@Json(name = "response")
val response: String? = null,
/**
* Used for login/registration result
*/
@Json(name = "credentials")
val credentials: Credentials? = null
)

View file

@ -103,9 +103,9 @@ class LoginActivity : VectorBaseActivity() {
when (loginViewEvents) {
is LoginViewEvents.RegistrationFlowResult -> {
// Check that all flows are supported by the application
if (loginViewEvents.flowResult.missingStages.any { it is Stage.Other }) {
if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) {
// Display a popup to propose use web fallback
// TODO
onRegistrationStageNotSupported()
} else {
// Go on with registration flow
// loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
@ -159,7 +159,9 @@ class LoginActivity : VectorBaseActivity() {
private fun onSignModeSelected() {
when (loginViewModel.signMode) {
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
SignMode.SignUp -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java)
SignMode.SignUp -> {
// This is managed by the LoginViewEvents
}
SignMode.SignIn -> {
// It depends on the LoginMode
withState(loginViewModel) {
@ -174,6 +176,17 @@ class LoginActivity : VectorBaseActivity() {
}
}
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)
}
.setNegativeButton(R.string.no, null)
.show()
}
private fun onLoginModeNotSupported(unsupportedLoginMode: LoginMode.Unsupported) {
AlertDialog.Builder(this)
.setTitle(R.string.app_name)

View file

@ -27,8 +27,6 @@ import android.view.View
import android.webkit.*
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.args
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
import im.vector.riotx.core.utils.AssetReader
@ -44,14 +42,6 @@ data class LoginCaptchaFragmentArgument(
val siteKey: String
) : Parcelable
@JsonClass(generateAdapter = true)
data class JavascriptResponse(
@Json(name = "action")
val action: String? = null,
@Json(name = "response")
val response: String? = null
)
/**
* In this screen, the user is asked to confirm he is not a robot
*/

View file

@ -30,10 +30,9 @@ import android.webkit.SslErrorHandler
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
import im.vector.riotx.core.utils.AssetReader
import kotlinx.android.synthetic.main.fragment_login_web.*
import timber.log.Timber
import java.net.URLDecoder
@ -43,7 +42,7 @@ import javax.inject.Inject
* This screen is displayed for SSO login and also when the application does not support login flow or registration flow
* of the homeserfver, as a fallback to login or to create an account
*/
class LoginWebFragment @Inject constructor() : AbstractLoginFragment() {
class LoginWebFragment @Inject constructor(private val assetReader: AssetReader) : AbstractLoginFragment() {
private lateinit var homeServerUrl: String
private lateinit var signMode: SignMode
@ -151,33 +150,17 @@ class LoginWebFragment @Inject constructor() : AbstractLoginFragment() {
// avoid infinite onPageFinished call
if (url.startsWith("http")) {
// Generic method to make a bridge between JS and the UIWebView
val mxcJavascriptSendObjectMessage = "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;" +
" };"
val mxcJavascriptSendObjectMessage = assetReader.readAssetFile("sendObject.js")
view.loadUrl(mxcJavascriptSendObjectMessage)
if (signMode == SignMode.SignIn) {
// The function the fallback page calls when the login is complete
val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" +
" sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" +
" };"
view.loadUrl(mxcJavascriptOnRegistered)
val mxcJavascriptOnLogin = assetReader.readAssetFile("onLogin.js")
view.loadUrl(mxcJavascriptOnLogin)
} else {
// MODE_REGISTER
// The function the fallback page calls when the registration is complete
val mxcJavascriptOnRegistered = "javascript:window.matrixRegistration.onRegistered" +
" = function(homeserverUrl, userId, accessToken) {" +
" sendObjectMessage({ 'action': 'onRegistered'," +
" 'homeServer': homeserverUrl," +
" 'userId': userId," +
" 'accessToken': accessToken });" +
" };"
val mxcJavascriptOnRegistered = assetReader.readAssetFile("onRegistered.js")
view.loadUrl(mxcJavascriptOnRegistered)
}
}
@ -209,46 +192,27 @@ class LoginWebFragment @Inject constructor() : AbstractLoginFragment() {
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
if (null != url && url.startsWith("js:")) {
var json = url.substring(3)
var parameters: Map<String, Any>? = null
var parameters: JavascriptResponse? = null
try {
// URL decode
json = URLDecoder.decode(json, "UTF-8")
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
@Suppress("UNCHECKED_CAST")
parameters = adapter.fromJson(json) as JsonDict?
val adapter = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java)
parameters = adapter.fromJson(json)
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed")
}
// succeeds to parse parameters
if (parameters != null) {
val action = parameters["action"] as String
val action = parameters.action
if (signMode == SignMode.SignIn) {
try {
if (action == "onLogin") {
@Suppress("UNCHECKED_CAST")
val credentials = parameters["credentials"] as Map<String, String>
val userId = credentials["user_id"]
val accessToken = credentials["access_token"]
val homeServer = credentials["home_server"]
val deviceId = credentials["device_id"]
// check if the parameters are defined
if (null != homeServer && null != userId && null != accessToken) {
val safeCredentials = Credentials(
userId = userId,
accessToken = accessToken,
homeServer = homeServer,
deviceId = deviceId,
refreshToken = null
)
loginViewModel.handle(LoginAction.WebLoginSuccess(safeCredentials))
val credentials = parameters.credentials
if (credentials != null) {
loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
}
} catch (e: Exception) {
@ -258,21 +222,8 @@ class LoginWebFragment @Inject constructor() : AbstractLoginFragment() {
// MODE_REGISTER
// check the required parameters
if (action == "onRegistered") {
// TODO The keys are very strange, this code comes from Riot-Android...
if (parameters.containsKey("homeServer")
&& parameters.containsKey("userId")
&& parameters.containsKey("accessToken")) {
// We cannot parse Credentials here because of https://github.com/matrix-org/synapse/issues/4756
// Build on object manually
val credentials = Credentials(
userId = parameters["userId"] as String,
accessToken = parameters["accessToken"] as String,
homeServer = parameters["homeServer"] as String,
// TODO We need deviceId on RiotX...
deviceId = "TODO",
refreshToken = null
)
val credentials = parameters.credentials
if (credentials != null) {
loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.login
import im.vector.matrix.android.api.auth.registration.Stage
/**
* Stage.Other is not supported, as well as any other new stage added to the SDK before it is added to the list below
*/
fun Stage.isSupported(): Boolean {
return this is Stage.ReCaptcha
|| this is Stage.Dummy
|| this is Stage.Msisdn
|| this is Stage.Terms
|| this is Stage.Email
}

View file

@ -56,6 +56,7 @@
<string name="login_sso_error_message">An error occurred when loading the page: %1$s (%2$d)</string>
<string name="login_mode_not_supported">The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client?</string>
<string name="login_registration_not_supported">The application is not able to create an account on this homeserver.\n\nDo you want to signup using a web client?</string>
<!-- Replaced string is the homeserver url -->
<string name="login_reset_password_on">Reset password on %1$s</string>