From c98fe59965ba54d335c2e78d80c7481403351267 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 5 Apr 2022 16:34:32 +0100 Subject: [PATCH 01/17] formatting --- .../onboarding/ftueauth/FtueAuthVariant.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index ea479b1cdc..a35014ca64 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -382,26 +382,34 @@ class FtueAuthVariant( supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) when (stage) { - is Stage.ReCaptcha -> activity.addFragmentToBackstack(views.loginFragmentContainer, + is Stage.ReCaptcha -> activity.addFragmentToBackstack( + views.loginFragmentContainer, FtueAuthCaptchaFragment::class.java, FtueAuthCaptchaFragmentArgument(stage.publicKey), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Email -> activity.addFragmentToBackstack(views.loginFragmentContainer, + option = commonOption + ) + is Stage.Email -> activity.addFragmentToBackstack( + views.loginFragmentContainer, FtueAuthGenericTextInputFormFragment::class.java, FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer, + option = commonOption + ) + is Stage.Msisdn -> activity.addFragmentToBackstack( + views.loginFragmentContainer, FtueAuthGenericTextInputFormFragment::class.java, FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Terms -> activity.addFragmentToBackstack(views.loginFragmentContainer, + option = commonOption + ) + is Stage.Terms -> activity.addFragmentToBackstack( + views.loginFragmentContainer, FtueAuthTermsFragment::class.java, FtueAuthTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) + option = commonOption + ) else -> Unit // Should not happen } } From 81a325b769f57f11c1d4678cbc115e9920fb0786 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 6 Apr 2022 09:49:01 +0100 Subject: [PATCH 02/17] extracting the captcha webview logic to a reusable class --- .../onboarding/ftueauth/CaptchaWebview.kt | 177 ++++++++++++++++++ .../ftueauth/FtueAuthCaptchaFragment.kt | 151 +-------------- 2 files changed, 182 insertions(+), 146 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt new file mode 100644 index 0000000000..52cec44a68 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2022 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.app.features.onboarding.ftueauth + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.graphics.Bitmap +import android.net.http.SslError +import android.os.Build +import android.view.KeyEvent +import android.view.View +import android.webkit.SslErrorHandler +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import im.vector.app.core.utils.AssetReader +import im.vector.app.features.login.JavascriptResponse +import im.vector.app.features.onboarding.OnboardingViewState +import org.matrix.android.sdk.api.util.MatrixJsonParser +import timber.log.Timber +import java.net.URLDecoder +import java.util.Formatter +import javax.inject.Inject + +class CaptchaWebview @Inject constructor( + private val assetReader: AssetReader +) { + + @SuppressLint("SetJavaScriptEnabled") + fun setupWebView( + container: Fragment, + webView: WebView, + progressView: View, + siteKey: String, + state: OnboardingViewState, + onSuccess: (String) -> Unit + ) { + webView.settings.javaScriptEnabled = true + + val reCaptchaPage = assetReader.readAssetFile("reCaptchaPage.html") ?: error("missing asset reCaptchaPage.html") + + val html = Formatter().format(reCaptchaPage, siteKey).toString() + val mime = "text/html" + val encoding = "utf-8" + + val homeServerUrl = state.selectedHomeserver.upstreamUrl ?: error("missing url of homeserver") + webView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null) + webView.requestLayout() + + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + if (!container.isAdded) { + return + } + + // Show loader + progressView.isVisible = true + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + + if (!container.isAdded) { + return + } + + // Hide loader + progressView.isVisible = false + } + + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { + Timber.d("## onReceivedSslError() : ${error.certificate}") + + if (!container.isAdded) { + return + } + + MaterialAlertDialogBuilder(container.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 = MatrixJsonParser.getMoshi().adapter(JavascriptResponse::class.java).fromJson(json) + } catch (e: Exception) { + Timber.e(e, "## shouldOverrideUrlLoading(): failed") + } + + val response = javascriptResponse?.response + if (javascriptResponse?.action == "verifyCallback" && response != null) { + onSuccess(response) + } + } + return true + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt index c8bec3776a..4c8dd8e932 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt @@ -16,35 +16,15 @@ package im.vector.app.features.onboarding.ftueauth -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.view.LayoutInflater import android.view.ViewGroup -import android.webkit.SslErrorHandler -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.core.view.isVisible import com.airbnb.mvrx.args -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import im.vector.app.R -import im.vector.app.core.utils.AssetReader import im.vector.app.databinding.FragmentLoginCaptchaBinding -import im.vector.app.features.login.JavascriptResponse import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.RegisterAction import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.util.MatrixJsonParser -import timber.log.Timber -import java.net.URLDecoder -import java.util.Formatter import javax.inject.Inject @Parcelize @@ -53,10 +33,10 @@ data class FtueAuthCaptchaFragmentArgument( ) : Parcelable /** - * In this screen, the user is asked to confirm he is not a robot + * In this screen, the user is asked to confirm they are not a robot */ class FtueAuthCaptchaFragment @Inject constructor( - private val assetReader: AssetReader + private val captchaWebview: CaptchaWebview ) : AbstractFtueAuthFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginCaptchaBinding { @@ -67,136 +47,15 @@ class FtueAuthCaptchaFragment @Inject constructor( private var isWebViewLoaded = false - @SuppressLint("SetJavaScriptEnabled") - private fun setupWebView(state: OnboardingViewState) { - views.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.selectedHomeserver.upstreamUrl ?: error("missing url of homeserver") - views.loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null) - views.loginCaptchaWevView.requestLayout() - - views.loginCaptchaWevView.webViewClient = object : WebViewClient() { - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - - if (!isAdded) { - return - } - - // Show loader - views.loginCaptchaProgress.isVisible = true - } - - override fun onPageFinished(view: WebView, url: String) { - super.onPageFinished(view, url) - - if (!isAdded) { - return - } - - // Hide loader - views.loginCaptchaProgress.isVisible = false - } - - override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { - Timber.d("## onReceivedSslError() : ${error.certificate}") - - if (!isAdded) { - return - } - - MaterialAlertDialogBuilder(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 = MatrixJsonParser.getMoshi().adapter(JavascriptResponse::class.java).fromJson(json) - } catch (e: Exception) { - Timber.e(e, "## shouldOverrideUrlLoading(): failed") - } - - val response = javascriptResponse?.response - if (javascriptResponse?.action == "verifyCallback" && response != null) { - viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CaptchaDone(response))) - } - } - return true - } - } - } - override fun resetViewModel() { viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) } override fun updateWithState(state: OnboardingViewState) { if (!isWebViewLoaded) { - setupWebView(state) + captchaWebview.setupWebView(this, views.loginCaptchaWevView, views.loginCaptchaProgress, params.siteKey, state) { + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CaptchaDone(it))) + } isWebViewLoaded = true } } From 863b4b810f83da099cb450ab37e6b3c6ca4e199b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 6 Apr 2022 09:59:25 +0100 Subject: [PATCH 03/17] duplicating the captcha fragment to style separately for the combined register flow --- .../im/vector/app/core/di/FragmentModule.kt | 6 ++ .../ftueauth/FtueAuthCaptchaFragment.kt | 22 +++---- .../FtueAuthCombinedRegisterFragment.kt | 8 +-- .../FtueAuthLegacyStyleCaptchaFragment.kt | 61 +++++++++++++++++++ .../onboarding/ftueauth/FtueAuthVariant.kt | 2 +- ...ml => fragment_ftue_combined_register.xml} | 0 .../layout/fragment_ftue_login_captcha.xml | 50 +++++++++++++++ 7 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyStyleCaptchaFragment.kt rename vector/src/main/res/layout/{fragment_ftue_sign_up_combined.xml => fragment_ftue_combined_register.xml} (100%) create mode 100644 vector/src/main/res/layout/fragment_ftue_login_captcha.xml diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 4dcfbe16f8..7a04a4c8b8 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -102,6 +102,7 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment +import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthPersonalizationCompleteFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment @@ -407,6 +408,11 @@ interface FragmentModule { @FragmentKey(LoginWaitForEmailFragment2::class) fun bindLoginWaitForEmailFragment2(fragment: LoginWaitForEmailFragment2): Fragment + @Binds + @IntoMap + @FragmentKey(FtueAuthLegacyStyleCaptchaFragment::class) + fun bindFtueAuthLegacyStyleCaptchaFragment(fragment: FtueAuthLegacyStyleCaptchaFragment): Fragment + @Binds @IntoMap @FragmentKey(FtueAuthCaptchaFragment::class) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt index 4c8dd8e932..e658c4948e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 New Vector Ltd + * Copyright (c) 2022 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. @@ -16,37 +16,29 @@ package im.vector.app.features.onboarding.ftueauth -import android.os.Parcelable import android.view.LayoutInflater import android.view.ViewGroup import com.airbnb.mvrx.args -import im.vector.app.databinding.FragmentLoginCaptchaBinding +import im.vector.app.databinding.FragmentFtueLoginCaptchaBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.RegisterAction -import kotlinx.parcelize.Parcelize import javax.inject.Inject -@Parcelize -data class FtueAuthCaptchaFragmentArgument( - val siteKey: String -) : Parcelable - /** * In this screen, the user is asked to confirm they are not a robot */ class FtueAuthCaptchaFragment @Inject constructor( private val captchaWebview: CaptchaWebview -) : AbstractFtueAuthFragment() { - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginCaptchaBinding { - return FragmentLoginCaptchaBinding.inflate(inflater, container, false) - } +) : AbstractFtueAuthFragment() { private val params: FtueAuthCaptchaFragmentArgument by args() - private var isWebViewLoaded = false + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueLoginCaptchaBinding { + return FragmentFtueLoginCaptchaBinding.inflate(inflater, container, false) + } + override fun resetViewModel() { viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt index 441fd64b0b..0755f18c8c 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt @@ -37,7 +37,7 @@ import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.realignPercentagesToParent import im.vector.app.core.extensions.toReducedUrl -import im.vector.app.databinding.FragmentFtueSignUpCombinedBinding +import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView @@ -56,10 +56,10 @@ import org.matrix.android.sdk.api.failure.isUsernameInUse import org.matrix.android.sdk.api.failure.isWeakPassword import javax.inject.Inject -class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAuthFragment() { +class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAuthFragment() { - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueSignUpCombinedBinding { - return FragmentFtueSignUpCombinedBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedRegisterBinding { + return FragmentFtueCombinedRegisterBinding.inflate(inflater, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyStyleCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyStyleCaptchaFragment.kt new file mode 100644 index 0000000000..dc17ab1cb8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyStyleCaptchaFragment.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2022 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.app.features.onboarding.ftueauth + +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.ViewGroup +import com.airbnb.mvrx.args +import im.vector.app.databinding.FragmentLoginCaptchaBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewState +import im.vector.app.features.onboarding.RegisterAction +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +@Parcelize +data class FtueAuthCaptchaFragmentArgument( + val siteKey: String +) : Parcelable + +/** + * In this screen, the user is asked to confirm they are not a robot + */ +class FtueAuthLegacyStyleCaptchaFragment @Inject constructor( + private val captchaWebview: CaptchaWebview +) : AbstractFtueAuthFragment() { + + private val params: FtueAuthCaptchaFragmentArgument by args() + private var isWebViewLoaded = false + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginCaptchaBinding { + return FragmentLoginCaptchaBinding.inflate(inflater, container, false) + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + } + + override fun updateWithState(state: OnboardingViewState) { + if (!isWebViewLoaded) { + captchaWebview.setupWebView(this, views.loginCaptchaWevView, views.loginCaptchaProgress, params.siteKey, state) { + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CaptchaDone(it))) + } + isWebViewLoaded = true + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index a35014ca64..461975ed07 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -384,7 +384,7 @@ class FtueAuthVariant( when (stage) { is Stage.ReCaptcha -> activity.addFragmentToBackstack( views.loginFragmentContainer, - FtueAuthCaptchaFragment::class.java, + FtueAuthLegacyStyleCaptchaFragment::class.java, FtueAuthCaptchaFragmentArgument(stage.publicKey), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption diff --git a/vector/src/main/res/layout/fragment_ftue_sign_up_combined.xml b/vector/src/main/res/layout/fragment_ftue_combined_register.xml similarity index 100% rename from vector/src/main/res/layout/fragment_ftue_sign_up_combined.xml rename to vector/src/main/res/layout/fragment_ftue_combined_register.xml diff --git a/vector/src/main/res/layout/fragment_ftue_login_captcha.xml b/vector/src/main/res/layout/fragment_ftue_login_captcha.xml new file mode 100644 index 0000000000..655bd98097 --- /dev/null +++ b/vector/src/main/res/layout/fragment_ftue_login_captcha.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + From f4747aa06911518779dd5b39934f3d7a37e6b5bd Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 6 Apr 2022 11:44:50 +0100 Subject: [PATCH 04/17] styling the captcha wth the update designs --- .../ftueauth/FtueAuthCaptchaFragment.kt | 7 ++ .../FtueAuthLegacyStyleCaptchaFragment.kt | 4 +- .../onboarding/ftueauth/FtueAuthVariant.kt | 27 ++-- .../layout/fragment_ftue_login_captcha.xml | 117 +++++++++++++----- 4 files changed, 117 insertions(+), 38 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt index e658c4948e..a3665a8f40 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.onboarding.ftueauth +import android.os.Parcelable import android.view.LayoutInflater import android.view.ViewGroup import com.airbnb.mvrx.args @@ -23,8 +24,14 @@ import im.vector.app.databinding.FragmentFtueLoginCaptchaBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.RegisterAction +import kotlinx.parcelize.Parcelize import javax.inject.Inject +@Parcelize +data class FtueAuthCaptchaFragmentArgument( + val siteKey: String +) : Parcelable + /** * In this screen, the user is asked to confirm they are not a robot */ diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyStyleCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyStyleCaptchaFragment.kt index dc17ab1cb8..2accab00e0 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyStyleCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyStyleCaptchaFragment.kt @@ -28,7 +28,7 @@ import kotlinx.parcelize.Parcelize import javax.inject.Inject @Parcelize -data class FtueAuthCaptchaFragmentArgument( +data class FtueAuthLegacyStyleCaptchaFragmentArgument( val siteKey: String ) : Parcelable @@ -39,7 +39,7 @@ class FtueAuthLegacyStyleCaptchaFragment @Inject constructor( private val captchaWebview: CaptchaWebview ) : AbstractFtueAuthFragment() { - private val params: FtueAuthCaptchaFragmentArgument by args() + private val params: FtueAuthLegacyStyleCaptchaFragmentArgument by args() private var isWebViewLoaded = false override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginCaptchaBinding { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 461975ed07..80343afa97 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -382,13 +382,7 @@ class FtueAuthVariant( supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) when (stage) { - is Stage.ReCaptcha -> activity.addFragmentToBackstack( - views.loginFragmentContainer, - FtueAuthLegacyStyleCaptchaFragment::class.java, - FtueAuthCaptchaFragmentArgument(stage.publicKey), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption - ) + is Stage.ReCaptcha -> onCaptcha(stage) is Stage.Email -> activity.addFragmentToBackstack( views.loginFragmentContainer, FtueAuthGenericTextInputFormFragment::class.java, @@ -414,6 +408,25 @@ class FtueAuthVariant( } } + private fun onCaptcha(stage: Stage.ReCaptcha) { + when { + vectorFeatures.isOnboardingCombinedRegisterEnabled() -> activity.addFragmentToBackstack( + views.loginFragmentContainer, + FtueAuthCaptchaFragment::class.java, + FtueAuthCaptchaFragmentArgument(stage.publicKey), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption + ) + else -> activity.addFragmentToBackstack( + views.loginFragmentContainer, + FtueAuthLegacyStyleCaptchaFragment::class.java, + FtueAuthLegacyStyleCaptchaFragmentArgument(stage.publicKey), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption + ) + } + } + private fun onAccountSignedIn() { navigateToHome(createdAccount = false) } diff --git a/vector/src/main/res/layout/fragment_ftue_login_captcha.xml b/vector/src/main/res/layout/fragment_ftue_login_captcha.xml index 655bd98097..c07d03a64c 100644 --- a/vector/src/main/res/layout/fragment_ftue_login_captcha.xml +++ b/vector/src/main/res/layout/fragment_ftue_login_captcha.xml @@ -1,50 +1,109 @@ - - - + app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" /> - + - + - - + - + + + + + + + + - + + From 11dbd0e80cfbda80d118c4eb79ad3586befa1a10 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 6 Apr 2022 14:14:29 +0100 Subject: [PATCH 05/17] renaming the terms fragment in prep to duplicate with the new style --- .../src/main/java/im/vector/app/core/di/FragmentModule.kt | 6 +++--- .../app/features/onboarding/ftueauth/FtueAuthVariant.kt | 8 ++++---- ...rmsFragment.kt => FtueAuthLegacyStyleTermsFragment.kt} | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) rename vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/{FtueAuthTermsFragment.kt => FtueAuthLegacyStyleTermsFragment.kt} (95%) diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 7a04a4c8b8..91831fc56b 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -115,7 +115,7 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthSplashFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthUseCaseFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthWaitForEmailFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthWebFragment -import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment +import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthLegacyStyleTermsFragment import im.vector.app.features.pin.PinFragment import im.vector.app.features.poll.create.CreatePollFragment import im.vector.app.features.qrcode.QrCodeScannerFragment @@ -480,8 +480,8 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(FtueAuthTermsFragment::class) - fun bindFtueAuthTermsFragment(fragment: FtueAuthTermsFragment): Fragment + @FragmentKey(FtueAuthLegacyStyleTermsFragment::class) + fun bindFtueAuthTermsFragment(fragment: FtueAuthLegacyStyleTermsFragment): Fragment @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 80343afa97..be6d30e5ce 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -51,8 +51,8 @@ import im.vector.app.features.onboarding.OnboardingVariant import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewModel import im.vector.app.features.onboarding.OnboardingViewState -import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment -import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragmentArgument +import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthLegacyStyleTermsFragment +import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsLegacyStyleFragmentArgument import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.extensions.tryOrNull @@ -399,8 +399,8 @@ class FtueAuthVariant( ) is Stage.Terms -> activity.addFragmentToBackstack( views.loginFragmentContainer, - FtueAuthTermsFragment::class.java, - FtueAuthTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), + FtueAuthLegacyStyleTermsFragment::class.java, + FtueAuthTermsLegacyStyleFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption ) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthLegacyStyleTermsFragment.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt rename to vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthLegacyStyleTermsFragment.kt index e5f0b0f167..f306289a5a 100755 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthLegacyStyleTermsFragment.kt @@ -39,19 +39,19 @@ import org.matrix.android.sdk.api.auth.data.LocalizedFlowDataLoginTerms import javax.inject.Inject @Parcelize -data class FtueAuthTermsFragmentArgument( +data class FtueAuthTermsLegacyStyleFragmentArgument( val localizedFlowDataLoginTerms: List ) : Parcelable /** * LoginTermsFragment displays the list of policies the user has to accept */ -class FtueAuthTermsFragment @Inject constructor( +class FtueAuthLegacyStyleTermsFragment @Inject constructor( private val policyController: PolicyController ) : AbstractFtueAuthFragment(), PolicyController.PolicyControllerListener { - private val params: FtueAuthTermsFragmentArgument by args() + private val params: FtueAuthTermsLegacyStyleFragmentArgument by args() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginTermsBinding { return FragmentLoginTermsBinding.inflate(inflater, container, false) From 06147967a4257d8d3b7bc4facbd66a71e8148673 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 6 Apr 2022 16:37:48 +0100 Subject: [PATCH 06/17] creating a ftue version of the policy fragment --- .../im/vector/app/core/di/FragmentModule.kt | 8 +- .../onboarding/ftueauth/FtueAuthVariant.kt | 18 ++- .../ftueauth/terms/FtueAuthTermsFragment.kt | 120 +++++++++++++++++ .../res/layout/fragment_ftue_login_terms.xml | 121 ++++++++++++++++++ 4 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt create mode 100644 vector/src/main/res/layout/fragment_ftue_login_terms.xml diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 91831fc56b..e3e64063f3 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -116,6 +116,7 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthUseCaseFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthWaitForEmailFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthWebFragment import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthLegacyStyleTermsFragment +import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment import im.vector.app.features.pin.PinFragment import im.vector.app.features.poll.create.CreatePollFragment import im.vector.app.features.qrcode.QrCodeScannerFragment @@ -481,7 +482,12 @@ interface FragmentModule { @Binds @IntoMap @FragmentKey(FtueAuthLegacyStyleTermsFragment::class) - fun bindFtueAuthTermsFragment(fragment: FtueAuthLegacyStyleTermsFragment): Fragment + fun bindFtueAuthLegacyStyleTermsFragment(fragment: FtueAuthLegacyStyleTermsFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(FtueAuthTermsFragment::class) + fun bindFtueAuthTermsFragment(fragment: FtueAuthTermsFragment): Fragment @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index be6d30e5ce..c8f88280d6 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -52,6 +52,7 @@ import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewModel import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthLegacyStyleTermsFragment +import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsLegacyStyleFragmentArgument import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage @@ -397,14 +398,27 @@ class FtueAuthVariant( tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption ) - is Stage.Terms -> activity.addFragmentToBackstack( + is Stage.Terms -> onTerms(stage) + else -> Unit // Should not happen + } + } + + private fun onTerms(stage: Stage.Terms) { + when { + vectorFeatures.isOnboardingCombinedRegisterEnabled() -> activity.addFragmentToBackstack( + views.loginFragmentContainer, + FtueAuthTermsFragment::class.java, + FtueAuthTermsLegacyStyleFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption + ) + else -> activity.addFragmentToBackstack( views.loginFragmentContainer, FtueAuthLegacyStyleTermsFragment::class.java, FtueAuthTermsLegacyStyleFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption ) - else -> Unit // Should not happen } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt new file mode 100644 index 0000000000..0ba84e32c8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2022 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.app.features.onboarding.ftueauth.terms + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.core.utils.openUrlInChromeCustomTab +import im.vector.app.databinding.FragmentFtueLoginTermsBinding +import im.vector.app.features.login.terms.LocalizedFlowDataLoginTermsChecked +import im.vector.app.features.login.terms.LoginTermsViewState +import im.vector.app.features.login.terms.PolicyController +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewState +import im.vector.app.features.onboarding.RegisterAction +import im.vector.app.features.onboarding.ftueauth.AbstractFtueAuthFragment +import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms +import javax.inject.Inject + +/** + * LoginTermsFragment displays the list of policies the user has to accept + */ +class FtueAuthTermsFragment @Inject constructor( + private val policyController: PolicyController +) : AbstractFtueAuthFragment(), + PolicyController.PolicyControllerListener { + + private val params: FtueAuthTermsLegacyStyleFragmentArgument by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueLoginTermsBinding { + return FragmentFtueLoginTermsBinding.inflate(inflater, container, false) + } + + private var loginTermsViewState: LoginTermsViewState = LoginTermsViewState(emptyList()) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupViews() + views.loginTermsPolicyList.configureWith(policyController, hasFixedSize = false) + policyController.listener = this + + val list = ArrayList() + + params.localizedFlowDataLoginTerms + .forEach { + list.add(LocalizedFlowDataLoginTermsChecked(it)) + } + + loginTermsViewState = LoginTermsViewState(list) + } + + private fun setupViews() { + views.displayNameSubmit.setOnClickListener { submit() } + views.loginTermsPolicyList.setHasFixedSize(false) + } + + override fun onDestroyView() { + views.loginTermsPolicyList.cleanup() + policyController.listener = null + super.onDestroyView() + } + + private fun renderState() { + policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked) + + // Button is enabled only if all checkboxes are checked + views.displayNameSubmit.isEnabled = loginTermsViewState.allChecked() + } + + override fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) { + if (isChecked) { + loginTermsViewState.check(localizedFlowDataLoginTerms) + } else { + loginTermsViewState.uncheck(localizedFlowDataLoginTerms) + } + + renderState() + } + + override fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) { + localizedFlowDataLoginTerms.localizedUrl + ?.takeIf { it.isNotBlank() } + ?.let { + openUrlInChromeCustomTab(requireContext(), null, it) + } + } + + private fun submit() { + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AcceptTerms)) + } + + override fun updateWithState(state: OnboardingViewState) { + policyController.homeServer = state.selectedHomeserver.userFacingUrl.toReducedUrl() + renderState() + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + } +} diff --git a/vector/src/main/res/layout/fragment_ftue_login_terms.xml b/vector/src/main/res/layout/fragment_ftue_login_terms.xml new file mode 100644 index 0000000000..1a6de5e1ec --- /dev/null +++ b/vector/src/main/res/layout/fragment_ftue_login_terms.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + +