Merge pull request #5737 from vector-im/feature/adm/ftue-captcha

FTUE - Registration Captcha and T&Cs screens
This commit is contained in:
Adam Brown 2022-04-14 16:42:25 +01:00 committed by GitHub
commit dc5902e8f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 717 additions and 223 deletions

1
changelog.d/5279.wip Normal file
View File

@ -0,0 +1 @@
Updates the Captcha and T&Cs registration screens in the FTUE flow to match the updated UI style

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?vctr_content_tertiary" android:state_checked="false" />
<item android:color="?colorPrimary" android:state_checked="true" />
</selector>

View File

@ -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
@ -114,6 +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.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
@ -407,6 +409,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)
@ -472,6 +479,11 @@ interface FragmentModule {
@FragmentKey(FtueAuthWebFragment::class)
fun bindFtueAuthWebFragment(fragment: FtueAuthWebFragment): Fragment
@Binds
@IntoMap
@FragmentKey(FtueAuthLegacyStyleTermsFragment::class)
fun bindFtueAuthLegacyStyleTermsFragment(fragment: FtueAuthLegacyStyleTermsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(FtueAuthTermsFragment::class)

View File

@ -32,3 +32,12 @@ fun View.showKeyboard(andRequestFocus: Boolean = false) {
val imm = context?.getSystemService<InputMethodManager>()
imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
fun View.setHorizontalPadding(padding: Int) {
setPadding(
padding,
paddingTop,
padding,
paddingBottom
)
}

View File

@ -24,6 +24,7 @@ class PolicyController @Inject constructor() : TypedEpoxyController<List<Localiz
var listener: PolicyControllerListener? = null
var horizontalPadding: Int? = null
var homeServer: String? = null
override fun buildModels(data: List<LocalizedFlowDataLoginTermsChecked>) {
@ -32,6 +33,7 @@ class PolicyController @Inject constructor() : TypedEpoxyController<List<Localiz
policyItem {
id(entry.localizedFlowDataLoginTerms.policyName)
checked(entry.checked)
horizontalPadding(host.horizontalPadding)
title(entry.localizedFlowDataLoginTerms.localizedName)
subtitle(host.homeServer)
clickListener { host.listener?.openPolicy(entry.localizedFlowDataLoginTerms) }

View File

@ -26,6 +26,7 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setHorizontalPadding
@EpoxyModelClass(layout = R.layout.item_policy)
abstract class PolicyItem : EpoxyModelWithHolder<PolicyItem.Holder>() {
@ -38,6 +39,9 @@ abstract class PolicyItem : EpoxyModelWithHolder<PolicyItem.Holder>() {
@EpoxyAttribute
var subtitle: String? = null
@EpoxyAttribute
var horizontalPadding: Int? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null
@ -46,13 +50,12 @@ abstract class PolicyItem : EpoxyModelWithHolder<PolicyItem.Holder>() {
override fun bind(holder: Holder) {
super.bind(holder)
holder.let {
it.checkbox.isChecked = checked
it.checkbox.setOnCheckedChangeListener(checkChangeListener)
it.title.text = title
it.subtitle.text = subtitle
it.view.onClick(clickListener)
}
horizontalPadding?.let { holder.view.setHorizontalPadding(it) }
holder.checkbox.isChecked = checked
holder.checkbox.setOnCheckedChangeListener(checkChangeListener)
holder.title.text = title
holder.subtitle.text = subtitle
holder.view.onClick(clickListener)
}
// Ensure checkbox behaves as expected (remove the listener)

View File

@ -0,0 +1,160 @@
/*
* 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) {
progressView.isVisible = true
}
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
if (container.isAdded) {
progressView.isVisible = false
}
}
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
Timber.d("## onReceivedSslError() : ${error.certificate}")
if (container.isAdded) {
showSslErrorDialog(container, handler)
}
}
private fun onError(errorMessage: String) {
Timber.e("## onError() : $errorMessage")
}
@SuppressLint("NewApi")
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
super.onReceivedHttpError(view, request, errorResponse)
when {
request.url.toString().endsWith("favicon.ico") -> {
// ignore favicon errors
}
else -> onError(errorResponse.toText())
}
}
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) {
val javascriptResponse = parseJsonFromUrl(url)
val response = javascriptResponse?.response
if (javascriptResponse?.action == "verifyCallback" && response != null) {
onSuccess(response)
}
}
return true
}
private fun parseJsonFromUrl(url: String): JavascriptResponse? {
return try {
val json = URLDecoder.decode(url.substringAfter("js:"), "UTF-8")
MatrixJsonParser.getMoshi().adapter(JavascriptResponse::class.java).fromJson(json)
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading(): failed")
null
}
}
}
}
private fun showSslErrorDialog(container: Fragment, handler: SslErrorHandler) {
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()
}
}
private fun WebResourceResponse.toText() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) reasonPhrase else toString()

View File

@ -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,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.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 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,141 +33,17 @@ 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
) : AbstractFtueAuthFragment<FragmentLoginCaptchaBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginCaptchaBinding {
return FragmentLoginCaptchaBinding.inflate(inflater, container, false)
}
private val captchaWebview: CaptchaWebview
) : AbstractFtueAuthFragment<FragmentFtueLoginCaptchaBinding>() {
private val params: FtueAuthCaptchaFragmentArgument by args()
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 getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueLoginCaptchaBinding {
return FragmentFtueLoginCaptchaBinding.inflate(inflater, container, false)
}
override fun resetViewModel() {
@ -196,7 +52,9 @@ class FtueAuthCaptchaFragment @Inject constructor(
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
}
}

View File

@ -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<FragmentFtueSignUpCombinedBinding>() {
class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAuthFragment<FragmentFtueCombinedRegisterBinding>() {
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?) {

View File

@ -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 FtueAuthLegacyStyleCaptchaFragmentArgument(
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<FragmentLoginCaptchaBinding>() {
private val params: FtueAuthLegacyStyleCaptchaFragmentArgument 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
}
}
}

View File

@ -17,6 +17,7 @@
package im.vector.app.features.onboarding.ftueauth
import android.content.Intent
import android.os.Parcelable
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
@ -51,8 +52,9 @@ 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.FtueAuthLegacyStyleTermsFragment
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.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
@ -201,20 +203,18 @@ class FtueAuthVariant(
is OnboardingViewEvents.OnSendEmailSuccess -> {
// Pop the enter email Fragment
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
activity.addFragmentToBackstack(views.loginFragmentContainer,
addRegistrationStageFragmentToBackstack(
FtueAuthWaitForEmailFragment::class.java,
FtueAuthWaitForEmailFragmentArgument(viewEvents.email),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
)
}
is OnboardingViewEvents.OnSendMsisdnSuccess -> {
// Pop the enter Msisdn Fragment
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
activity.addFragmentToBackstack(views.loginFragmentContainer,
addRegistrationStageFragmentToBackstack(
FtueAuthGenericTextInputFormFragment::class.java,
FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, viewEvents.msisdn),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
)
}
is OnboardingViewEvents.Failure,
is OnboardingViewEvents.Loading ->
@ -245,12 +245,7 @@ class FtueAuthVariant(
}
private fun openCombinedRegister() {
activity.addFragmentToBackstack(
views.loginFragmentContainer,
FtueAuthCombinedRegisterFragment::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption
)
addRegistrationStageFragmentToBackstack(FtueAuthCombinedRegisterFragment::class.java)
}
private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) =
@ -382,30 +377,46 @@ class FtueAuthVariant(
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
when (stage) {
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,
is Stage.ReCaptcha -> onCaptcha(stage)
is Stage.Email -> addRegistrationStageFragmentToBackstack(
FtueAuthGenericTextInputFormFragment::class.java,
FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer,
)
is Stage.Msisdn -> addRegistrationStageFragmentToBackstack(
FtueAuthGenericTextInputFormFragment::class.java,
FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
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)
)
is Stage.Terms -> onTerms(stage)
else -> Unit // Should not happen
}
}
private fun onTerms(stage: Stage.Terms) {
when {
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack(
FtueAuthTermsFragment::class.java,
FtueAuthTermsLegacyStyleFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))),
)
else -> addRegistrationStageFragmentToBackstack(
FtueAuthLegacyStyleTermsFragment::class.java,
FtueAuthTermsLegacyStyleFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))),
)
}
}
private fun onCaptcha(stage: Stage.ReCaptcha) {
when {
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack(
FtueAuthCaptchaFragment::class.java,
FtueAuthCaptchaFragmentArgument(stage.publicKey),
)
else -> addRegistrationStageFragmentToBackstack(
FtueAuthLegacyStyleCaptchaFragment::class.java,
FtueAuthLegacyStyleCaptchaFragmentArgument(stage.publicKey),
)
}
}
private fun onAccountSignedIn() {
navigateToHome(createdAccount = false)
}
@ -447,4 +458,14 @@ class FtueAuthVariant(
useCustomAnimation = true
)
}
private fun addRegistrationStageFragmentToBackstack(fragmentClass: Class<out Fragment>, params: Parcelable? = null) {
activity.addFragmentToBackstack(
views.loginFragmentContainer,
fragmentClass,
params,
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption
)
}
}

View File

@ -0,0 +1,124 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.onboarding.ftueauth.terms
import android.os.Bundle
import android.os.Parcelable
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.FragmentLoginTermsBinding
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 kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.data.LocalizedFlowDataLoginTerms
import javax.inject.Inject
@Parcelize
data class FtueAuthTermsLegacyStyleFragmentArgument(
val localizedFlowDataLoginTerms: List<LocalizedFlowDataLoginTerms>
) : Parcelable
/**
* LoginTermsFragment displays the list of policies the user has to accept
*/
class FtueAuthLegacyStyleTermsFragment @Inject constructor(
private val policyController: PolicyController
) : AbstractFtueAuthFragment<FragmentLoginTermsBinding>(),
PolicyController.PolicyControllerListener {
private val params: FtueAuthTermsLegacyStyleFragmentArgument by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginTermsBinding {
return FragmentLoginTermsBinding.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)
policyController.listener = this
val list = ArrayList<LocalizedFlowDataLoginTermsChecked>()
params.localizedFlowDataLoginTerms
.forEach {
list.add(LocalizedFlowDataLoginTermsChecked(it))
}
loginTermsViewState = LoginTermsViewState(list)
}
private fun setupViews() {
views.loginTermsSubmit.setOnClickListener { submit() }
}
override fun onDestroyView() {
views.loginTermsPolicyList.cleanup()
policyController.listener = null
super.onDestroyView()
}
private fun renderState() {
policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked)
views.loginTermsSubmit.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)
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018 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.
@ -17,16 +17,18 @@
package im.vector.app.features.onboarding.ftueauth.terms
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.doOnLayout
import com.airbnb.mvrx.args
import im.vector.app.R
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.FragmentLoginTermsBinding
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
@ -34,50 +36,46 @@ 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 kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.data.LocalizedFlowDataLoginTerms
import javax.inject.Inject
@Parcelize
data class FtueAuthTermsFragmentArgument(
val localizedFlowDataLoginTerms: List<LocalizedFlowDataLoginTerms>
) : Parcelable
import kotlin.math.roundToInt
/**
* LoginTermsFragment displays the list of policies the user has to accept
*/
class FtueAuthTermsFragment @Inject constructor(
private val policyController: PolicyController
) : AbstractFtueAuthFragment<FragmentLoginTermsBinding>(),
) : AbstractFtueAuthFragment<FragmentFtueLoginTermsBinding>(),
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)
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)
policyController.listener = this
val list = ArrayList<LocalizedFlowDataLoginTermsChecked>()
params.localizedFlowDataLoginTerms
.forEach {
list.add(LocalizedFlowDataLoginTermsChecked(it))
}
loginTermsViewState = LoginTermsViewState(list)
}
private fun setupViews() {
views.loginTermsSubmit.setOnClickListener { submit() }
views.termsSubmit.setOnClickListener { submit() }
views.loginTermsPolicyList.setHasFixedSize(false)
views.loginTermsPolicyList.configureWith(policyController, hasFixedSize = false, dividerDrawable = R.drawable.divider_horizontal)
views.termsGutterStart.doOnLayout {
val gutterSize = views.contentRoot.width * (views.termsGutterStart.layoutParams as ConstraintLayout.LayoutParams).guidePercent
policyController.horizontalPadding = gutterSize.roundToInt()
}
policyController.listener = this
}
override fun onDestroyView() {
@ -90,7 +88,7 @@ class FtueAuthTermsFragment @Inject constructor(
policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked)
// Button is enabled only if all checkboxes are checked
views.loginTermsSubmit.isEnabled = loginTermsViewState.allChecked()
views.termsSubmit.isEnabled = loginTermsViewState.allChecked()
}
override fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) {

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="71dp"
android:height="70dp"
android:viewportWidth="71"
android:viewportHeight="70">
<path
android:pathData="M19.778,21.722C19.778,18.501 22.389,15.889 25.611,15.889H45.056C48.277,15.889 50.889,18.501 50.889,21.722V48.944C50.889,52.166 48.277,54.778 45.056,54.778H25.611C22.389,54.778 19.778,52.166 19.778,48.944V21.722ZM25.611,39.708C25.611,38.903 26.264,38.25 27.069,38.25H43.597C44.403,38.25 45.056,38.903 45.056,39.708C45.056,40.514 44.403,41.167 43.597,41.167H27.069C26.264,41.167 25.611,40.514 25.611,39.708ZM27.069,45.056C26.264,45.056 25.611,45.708 25.611,46.514C25.611,47.319 26.264,47.972 27.069,47.972H35.819C36.625,47.972 37.278,47.319 37.278,46.514C37.278,45.708 36.625,45.056 35.819,45.056H27.069Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/captchaRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/captchaGutterStart"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/captchaGutterEnd"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
<Space
android:id="@+id/headerSpacing"
android:layout_width="match_parent"
android:layout_height="52dp"
app:layout_constraintBottom_toTopOf="@id/captchaHeaderIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed" />
<ImageView
android:id="@+id/captchaHeaderIcon"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:backgroundTint="?colorSecondary"
android:contentDescription="@null"
android:src="@drawable/ic_user_fg"
app:layout_constraintBottom_toTopOf="@id/captchaHeaderTitle"
app:layout_constraintEnd_toEndOf="@id/captchaGutterEnd"
app:layout_constraintHeight_percent="0.10"
app:layout_constraintStart_toStartOf="@id/captchaGutterStart"
app:layout_constraintTop_toBottomOf="@id/headerSpacing"
app:tint="@color/palette_white" />
<TextView
android:id="@+id/captchaHeaderTitle"
style="@style/Widget.Vector.TextView.Title.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/ftue_auth_create_account_title"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/captchaHeaderSubtitle"
app:layout_constraintEnd_toEndOf="@id/captchaGutterEnd"
app:layout_constraintStart_toStartOf="@id/captchaGutterStart"
app:layout_constraintTop_toBottomOf="@id/captchaHeaderIcon" />
<TextView
android:id="@+id/captchaHeaderSubtitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/auth_recaptcha_message"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
app:layout_constraintEnd_toEndOf="@id/captchaGutterEnd"
app:layout_constraintStart_toStartOf="@id/captchaGutterStart"
app:layout_constraintTop_toBottomOf="@id/captchaHeaderTitle" />
<Space
android:id="@+id/titleContentSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/loginCaptchaWevView"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/captchaHeaderSubtitle" />
<WebView
android:id="@+id/loginCaptchaWevView"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/login_a11y_captcha_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/captchaGutterEnd"
app:layout_constraintStart_toStartOf="@id/captchaGutterStart"
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing" />
<ProgressBar
android:id="@+id/loginCaptchaProgress"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/captchaGutterEnd"
app:layout_constraintStart_toStartOf="@id/captchaGutterStart"
app:layout_constraintTop_toBottomOf="@id/headerSpacing"
tools:ignore="UnknownId,NotSibling" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/LoginFormScrollView"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:fillViewport="true"
android:paddingTop="0dp"
android:paddingBottom="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/contentRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/termsGutterStart"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/termsGutterEnd"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
<Space
android:id="@+id/headerSpacing"
android:layout_width="match_parent"
android:layout_height="52dp"
app:layout_constraintBottom_toTopOf="@id/termsHeaderIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed" />
<ImageView
android:id="@+id/termsHeaderIcon"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:backgroundTint="?colorSecondary"
android:contentDescription="@null"
android:src="@drawable/ic_privacy_policy"
app:layout_constraintBottom_toTopOf="@id/termsHeaderTitle"
app:layout_constraintEnd_toEndOf="@id/termsGutterEnd"
app:layout_constraintHeight_percent="0.12"
app:layout_constraintStart_toStartOf="@id/termsGutterStart"
app:layout_constraintTop_toBottomOf="@id/headerSpacing"
app:tint="@color/palette_white" />
<TextView
android:id="@+id/termsHeaderTitle"
style="@style/Widget.Vector.TextView.Title.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/ftue_auth_terms_title"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/termsHeaderSubtitle"
app:layout_constraintEnd_toEndOf="@id/termsGutterEnd"
app:layout_constraintStart_toStartOf="@id/termsGutterStart"
app:layout_constraintTop_toBottomOf="@id/termsHeaderIcon" />
<TextView
android:id="@+id/termsHeaderSubtitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/ftue_auth_terms_subtitle"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
app:layout_constraintEnd_toEndOf="@id/termsGutterEnd"
app:layout_constraintStart_toStartOf="@id/termsGutterStart"
app:layout_constraintTop_toBottomOf="@id/termsHeaderTitle" />
<Space
android:id="@+id/titleContentSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/loginTermsPolicyList"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/termsHeaderSubtitle" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/loginTermsPolicyList"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/entrySpacing"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing" />
<Space
android:id="@+id/entrySpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/termsSubmit"
app:layout_constraintHeight_percent="0.05"
app:layout_constraintTop_toBottomOf="@id/loginTermsPolicyList" />
<Button
android:id="@+id/termsSubmit"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_signup_submit"
android:textAllCaps="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/termsGutterEnd"
app:layout_constraintStart_toStartOf="@id/termsGutterStart"
app:layout_constraintTop_toBottomOf="@id/entrySpacing" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -29,7 +29,6 @@
android:text="@string/auth_recaptcha_message"
android:textColor="?vctr_content_secondary" />
<!-- contentDescription does not work on WebView? -->
<WebView
android:id="@+id/loginCaptchaWevView"
android:layout_width="match_parent"

View File

@ -48,6 +48,8 @@
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintBottom_toTopOf="@id/loginTermsSubmit"
app:layout_constraintTop_toBottomOf="@id/loginTermsNotice"
tools:listitem="@layout/item_policy" />

View File

@ -52,6 +52,8 @@
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintBottom_toTopOf="@id/loginTermsSubmit"
app:layout_constraintTop_toBottomOf="@id/loginTermsNotice"
tools:listitem="@layout/item_policy" />

View File

@ -6,24 +6,22 @@
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:minHeight="72dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
tools:viewBindingIgnore="true">
<CheckBox
android:id="@+id/adapter_item_policy_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:buttonTint="@color/checkbox_tint_selector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/adapter_item_policy_title"
style="@style/Widget.Vector.TextView.Body"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawablePadding="8dp"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/adapter_item_policy_subtitle"
app:layout_constraintEnd_toStartOf="@id/adapter_item_policy_arrow"
@ -50,16 +48,15 @@
<ImageView
android:id="@+id/adapter_item_policy_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:paddingStart="8dp"
android:paddingEnd="0dp"
android:rotationY="@integer/rtl_mirror_flip"
android:src="@drawable/ic_arrow_right"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?vctr_content_secondary"
app:tint="?vctr_content_tertiary"
tools:ignore="MissingPrefix" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -28,4 +28,7 @@
<string name="ftue_auth_choose_server_ems_subtitle">Element Matrix Services (EMS) is a robust and reliable hosting service for fast, secure and real time communication. Find out how on <a href="${ftue_ems_url}">element.io/ems</a></string>
<string name="ftue_auth_choose_server_ems_cta">Get in touch</string>
<string name="ftue_auth_terms_title">Privacy policy</string>
<string name="ftue_auth_terms_subtitle">Please read through T&amp;C. You must accept in order to continue.</string>
</resources>