mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-24 02:15:46 +03:00
Merge pull request #4738 from vector-im/feature/adm/cloning-login-fragments-to-ftue
Cloning the `Login` fragments to `FtueAuth`
This commit is contained in:
commit
b40324a8ba
18 changed files with 2408 additions and 49 deletions
|
@ -94,6 +94,18 @@ import im.vector.app.features.login2.created.AccountCreatedFragment
|
|||
import im.vector.app.features.login2.terms.LoginTermsFragment2
|
||||
import im.vector.app.features.matrixto.MatrixToRoomSpaceFragment
|
||||
import im.vector.app.features.matrixto.MatrixToUserFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordMailConfirmationFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthServerSelectionFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthSignUpSignInSelectionFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthSplashFragment
|
||||
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.pin.PinFragment
|
||||
import im.vector.app.features.poll.create.CreatePollFragment
|
||||
import im.vector.app.features.qrcode.QrCodeScannerFragment
|
||||
|
@ -386,6 +398,66 @@ interface FragmentModule {
|
|||
@FragmentKey(LoginWaitForEmailFragment2::class)
|
||||
fun bindLoginWaitForEmailFragment2(fragment: LoginWaitForEmailFragment2): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthCaptchaFragment::class)
|
||||
fun bindFtueAuthCaptchaFragment(fragment: FtueAuthCaptchaFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthGenericTextInputFormFragment::class)
|
||||
fun bindFtueAuthGenericTextInputFormFragment(fragment: FtueAuthGenericTextInputFormFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthLoginFragment::class)
|
||||
fun bindFtueAuthLoginFragment(fragment: FtueAuthLoginFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthResetPasswordFragment::class)
|
||||
fun bindFtueAuthResetPasswordFragment(fragment: FtueAuthResetPasswordFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthResetPasswordMailConfirmationFragment::class)
|
||||
fun bindFtueAuthResetPasswordMailConfirmationFragment(fragment: FtueAuthResetPasswordMailConfirmationFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthResetPasswordSuccessFragment::class)
|
||||
fun bindFtueAuthResetPasswordSuccessFragment(fragment: FtueAuthResetPasswordSuccessFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthServerSelectionFragment::class)
|
||||
fun bindFtueAuthServerSelectionFragment(fragment: FtueAuthServerSelectionFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthSignUpSignInSelectionFragment::class)
|
||||
fun bindFtueAuthSignUpSignInSelectionFragment(fragment: FtueAuthSignUpSignInSelectionFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthSplashFragment::class)
|
||||
fun bindFtueAuthSplashFragment(fragment: FtueAuthSplashFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthWaitForEmailFragment::class)
|
||||
fun bindFtueAuthWaitForEmailFragment(fragment: FtueAuthWaitForEmailFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthWebFragment::class)
|
||||
fun bindFtueAuthWebFragment(fragment: FtueAuthWebFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthTermsFragment::class)
|
||||
fun bindFtueAuthTermsFragment(fragment: FtueAuthTermsFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(UserListFragment::class)
|
||||
|
|
|
@ -457,7 +457,7 @@ interface MavericksViewModelModule {
|
|||
@Binds
|
||||
@IntoMap
|
||||
@MavericksViewModelKey(OnboardingViewModel::class)
|
||||
fun ftueViewModelFactory(factory: OnboardingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
fun onboardingViewModelFactory(factory: OnboardingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
|
|
|
@ -35,30 +35,30 @@ import im.vector.app.core.extensions.exhaustive
|
|||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivityLoginBinding
|
||||
import im.vector.app.features.home.HomeActivity
|
||||
import im.vector.app.features.login.LoginCaptchaFragment
|
||||
import im.vector.app.features.login.LoginCaptchaFragmentArgument
|
||||
import im.vector.app.features.login.LoginConfig
|
||||
import im.vector.app.features.login.LoginFragment
|
||||
import im.vector.app.features.login.LoginGenericTextInputFormFragment
|
||||
import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.LoginResetPasswordFragment
|
||||
import im.vector.app.features.login.LoginResetPasswordMailConfirmationFragment
|
||||
import im.vector.app.features.login.LoginResetPasswordSuccessFragment
|
||||
import im.vector.app.features.login.LoginServerSelectionFragment
|
||||
import im.vector.app.features.login.LoginServerUrlFormFragment
|
||||
import im.vector.app.features.login.LoginSignUpSignInSelectionFragment
|
||||
import im.vector.app.features.login.LoginSplashFragment
|
||||
import im.vector.app.features.login.LoginWaitForEmailFragment
|
||||
import im.vector.app.features.login.LoginWaitForEmailFragmentArgument
|
||||
import im.vector.app.features.login.LoginWebFragment
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.login.TextInputFormFragmentMode
|
||||
import im.vector.app.features.login.isSupported
|
||||
import im.vector.app.features.login.terms.LoginTermsFragment
|
||||
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
|
||||
import im.vector.app.features.login.terms.toLocalizedLoginTerms
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragmentArgument
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragmentArgument
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordMailConfirmationFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthServerSelectionFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthServerUrlFormFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthSignUpSignInSelectionFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthSplashFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthWaitForEmailFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthWaitForEmailFragmentArgument
|
||||
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.FtueAuthTermsFragmentArgument
|
||||
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
|
||||
|
@ -102,7 +102,7 @@ class OnboardingAuthVariant(
|
|||
onboardingViewModel.onEach {
|
||||
updateWithState(it)
|
||||
}
|
||||
onboardingViewModel.observeViewEvents { handleLoginViewEvents(it) }
|
||||
onboardingViewModel.observeViewEvents { handleOnboardingViewEvents(it) }
|
||||
}
|
||||
|
||||
// Get config extra
|
||||
|
@ -117,10 +117,10 @@ class OnboardingAuthVariant(
|
|||
}
|
||||
|
||||
private fun addFirstFragment() {
|
||||
activity.addFragment(views.loginFragmentContainer, LoginSplashFragment::class.java)
|
||||
activity.addFragment(views.loginFragmentContainer, FtueAuthSplashFragment::class.java)
|
||||
}
|
||||
|
||||
private fun handleLoginViewEvents(onboardingViewEvents: OnboardingViewEvents) {
|
||||
private fun handleOnboardingViewEvents(onboardingViewEvents: OnboardingViewEvents) {
|
||||
when (onboardingViewEvents) {
|
||||
is OnboardingViewEvents.RegistrationFlowResult -> {
|
||||
// Check that all flows are supported by the application
|
||||
|
@ -136,7 +136,7 @@ class OnboardingAuthVariant(
|
|||
// I add a tag to indicate that this fragment is a registration stage.
|
||||
// This way it will be automatically popped in when starting the next registration stage
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginFragment::class.java,
|
||||
FtueAuthLoginFragment::class.java,
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption
|
||||
)
|
||||
|
@ -153,7 +153,7 @@ class OnboardingAuthVariant(
|
|||
}
|
||||
is OnboardingViewEvents.OpenServerSelection ->
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginServerSelectionFragment::class.java,
|
||||
FtueAuthServerSelectionFragment::class.java,
|
||||
option = { ft ->
|
||||
activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// Disable transition of text
|
||||
|
@ -167,23 +167,23 @@ class OnboardingAuthVariant(
|
|||
is OnboardingViewEvents.OnSignModeSelected -> onSignModeSelected(onboardingViewEvents)
|
||||
is OnboardingViewEvents.OnLoginFlowRetrieved ->
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginSignUpSignInSelectionFragment::class.java,
|
||||
FtueAuthSignUpSignInSelectionFragment::class.java,
|
||||
option = commonOption)
|
||||
is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(onboardingViewEvents)
|
||||
is OnboardingViewEvents.OnForgetPasswordClicked ->
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginResetPasswordFragment::class.java,
|
||||
FtueAuthResetPasswordFragment::class.java,
|
||||
option = commonOption)
|
||||
is OnboardingViewEvents.OnResetPasswordSendThreePidDone -> {
|
||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginResetPasswordMailConfirmationFragment::class.java,
|
||||
FtueAuthResetPasswordMailConfirmationFragment::class.java,
|
||||
option = commonOption)
|
||||
}
|
||||
is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess -> {
|
||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginResetPasswordSuccessFragment::class.java,
|
||||
FtueAuthResetPasswordSuccessFragment::class.java,
|
||||
option = commonOption)
|
||||
}
|
||||
is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone -> {
|
||||
|
@ -194,8 +194,8 @@ class OnboardingAuthVariant(
|
|||
// Pop the enter email Fragment
|
||||
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginWaitForEmailFragment::class.java,
|
||||
LoginWaitForEmailFragmentArgument(onboardingViewEvents.email),
|
||||
FtueAuthWaitForEmailFragment::class.java,
|
||||
FtueAuthWaitForEmailFragmentArgument(onboardingViewEvents.email),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption)
|
||||
}
|
||||
|
@ -203,8 +203,8 @@ class OnboardingAuthVariant(
|
|||
// Pop the enter Msisdn Fragment
|
||||
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginGenericTextInputFormFragment::class.java,
|
||||
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, onboardingViewEvents.msisdn),
|
||||
FtueAuthGenericTextInputFormFragment::class.java,
|
||||
FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, onboardingViewEvents.msisdn),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption)
|
||||
}
|
||||
|
@ -242,23 +242,23 @@ class OnboardingAuthVariant(
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun onServerSelectionDone(loginViewEvents: OnboardingViewEvents.OnServerSelectionDone) {
|
||||
when (loginViewEvents.serverType) {
|
||||
private fun onServerSelectionDone(OnboardingViewEvents: OnboardingViewEvents.OnServerSelectionDone) {
|
||||
when (OnboardingViewEvents.serverType) {
|
||||
ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow
|
||||
ServerType.EMS,
|
||||
ServerType.Other -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginServerUrlFormFragment::class.java,
|
||||
FtueAuthServerUrlFormFragment::class.java,
|
||||
option = commonOption)
|
||||
ServerType.Unknown -> Unit /* Should not happen */
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSignModeSelected(loginViewEvents: OnboardingViewEvents.OnSignModeSelected) = withState(onboardingViewModel) { state ->
|
||||
private fun onSignModeSelected(OnboardingViewEvents: OnboardingViewEvents.OnSignModeSelected) = withState(onboardingViewModel) { state ->
|
||||
// state.signMode could not be ready yet. So use value from the ViewEvent
|
||||
when (loginViewEvents.signMode) {
|
||||
when (OnboardingViewEvents.signMode) {
|
||||
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
|
||||
SignMode.SignUp -> {
|
||||
// This is managed by the LoginViewEvents
|
||||
// This is managed by the OnboardingViewEvents
|
||||
}
|
||||
SignMode.SignIn -> {
|
||||
// It depends on the LoginMode
|
||||
|
@ -267,14 +267,14 @@ class OnboardingAuthVariant(
|
|||
is LoginMode.Sso -> error("Developer error")
|
||||
is LoginMode.SsoAndPassword,
|
||||
LoginMode.Password -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginFragment::class.java,
|
||||
FtueAuthLoginFragment::class.java,
|
||||
tag = FRAGMENT_LOGIN_TAG,
|
||||
option = commonOption)
|
||||
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
|
||||
}.exhaustive
|
||||
}
|
||||
SignMode.SignInWithMatrixId -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginFragment::class.java,
|
||||
FtueAuthLoginFragment::class.java,
|
||||
tag = FRAGMENT_LOGIN_TAG,
|
||||
option = commonOption)
|
||||
}.exhaustive
|
||||
|
@ -295,7 +295,7 @@ class OnboardingAuthVariant(
|
|||
.setMessage(activity.getString(R.string.login_registration_not_supported))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginWebFragment::class.java,
|
||||
FtueAuthWebFragment::class.java,
|
||||
option = commonOption)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
|
@ -308,7 +308,7 @@ class OnboardingAuthVariant(
|
|||
.setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginWebFragment::class.java,
|
||||
FtueAuthWebFragment::class.java,
|
||||
option = commonOption)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
|
@ -338,23 +338,23 @@ class OnboardingAuthVariant(
|
|||
|
||||
when (stage) {
|
||||
is Stage.ReCaptcha -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginCaptchaFragment::class.java,
|
||||
LoginCaptchaFragmentArgument(stage.publicKey),
|
||||
FtueAuthCaptchaFragment::class.java,
|
||||
FtueAuthCaptchaFragmentArgument(stage.publicKey),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption)
|
||||
is Stage.Email -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginGenericTextInputFormFragment::class.java,
|
||||
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
|
||||
FtueAuthGenericTextInputFormFragment::class.java,
|
||||
FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption)
|
||||
is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginGenericTextInputFormFragment::class.java,
|
||||
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
|
||||
FtueAuthGenericTextInputFormFragment::class.java,
|
||||
FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption)
|
||||
is Stage.Terms -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
LoginTermsFragment::class.java,
|
||||
LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))),
|
||||
FtueAuthTermsFragment::class.java,
|
||||
FtueAuthTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))),
|
||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||
option = commonOption)
|
||||
else -> Unit // Should not happen
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* 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.app.features.onboarding.ftueauth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.transition.TransitionInflater
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.OnBackPressed
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewModel
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
/**
|
||||
* Parent Fragment for all the login/registration screens
|
||||
*/
|
||||
abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<VB>(), OnBackPressed {
|
||||
|
||||
protected val viewModel: OnboardingViewModel by activityViewModel()
|
||||
|
||||
private var isResetPasswordStarted = false
|
||||
|
||||
// Due to async, we keep a boolean to avoid displaying twice the cancellation dialog
|
||||
private var displayCancelDialog = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
context?.let {
|
||||
sharedElementEnterTransition = TransitionInflater.from(it).inflateTransition(android.R.transition.move)
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewModel.observeViewEvents {
|
||||
handleOnboardingViewEvents(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) {
|
||||
when (viewEvents) {
|
||||
is OnboardingViewEvents.Failure -> showFailure(viewEvents.throwable)
|
||||
else ->
|
||||
// This is handled by the Activity
|
||||
Unit
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
override fun showFailure(throwable: Throwable) {
|
||||
// Only the resumed Fragment can eventually show the error, to avoid multiple dialog display
|
||||
if (!isResumed) {
|
||||
return
|
||||
}
|
||||
|
||||
when (throwable) {
|
||||
is CancellationException ->
|
||||
/* Ignore this error, user has cancelled the action */
|
||||
Unit
|
||||
is Failure.ServerError ->
|
||||
if (throwable.error.code == MatrixError.M_FORBIDDEN &&
|
||||
throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(getString(R.string.login_registration_disabled))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else {
|
||||
onError(throwable)
|
||||
}
|
||||
is Failure.UnrecognizedCertificateFailure ->
|
||||
showUnrecognizedCertificateFailure(throwable)
|
||||
else ->
|
||||
onError(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnrecognizedCertificateFailure(failure: Failure.UnrecognizedCertificateFailure) {
|
||||
// Ask the user to accept the certificate
|
||||
unrecognizedCertificateDialog.show(requireActivity(),
|
||||
failure.fingerprint,
|
||||
failure.url,
|
||||
object : UnrecognizedCertificateDialog.Callback {
|
||||
override fun onAccept() {
|
||||
// User accept the certificate
|
||||
viewModel.handle(OnboardingAction.UserAcceptCertificate(failure.fingerprint))
|
||||
}
|
||||
|
||||
override fun onIgnore() {
|
||||
// Cannot happen in this case
|
||||
}
|
||||
|
||||
override fun onReject() {
|
||||
// Nothing to do in this case
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
open fun onError(throwable: Throwable) {
|
||||
super.showFailure(throwable)
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||
return when {
|
||||
displayCancelDialog && viewModel.isRegistrationStarted -> {
|
||||
// Ask for confirmation before cancelling the registration
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.login_signup_cancel_confirmation_title)
|
||||
.setMessage(R.string.login_signup_cancel_confirmation_content)
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
displayCancelDialog = false
|
||||
vectorBaseActivity.onBackPressed()
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
|
||||
true
|
||||
}
|
||||
displayCancelDialog && isResetPasswordStarted -> {
|
||||
// Ask for confirmation before cancelling the reset password
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.login_reset_password_cancel_confirmation_title)
|
||||
.setMessage(R.string.login_reset_password_cancel_confirmation_content)
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
displayCancelDialog = false
|
||||
vectorBaseActivity.onBackPressed()
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
resetViewModel()
|
||||
// Do not consume the Back event
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final override fun invalidate() = withState(viewModel) { state ->
|
||||
// True when email is sent with success to the homeserver
|
||||
isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not()
|
||||
|
||||
updateWithState(state)
|
||||
}
|
||||
|
||||
open fun updateWithState(state: OnboardingViewState) {
|
||||
// No op by default
|
||||
}
|
||||
|
||||
// Reset any modification on the viewModel by the current fragment
|
||||
abstract fun resetViewModel()
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.onboarding.ftueauth
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabsClient
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection
|
||||
import androidx.browser.customtabs.CustomTabsSession
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.hasSso
|
||||
import im.vector.app.features.login.ssoIdentityProviders
|
||||
|
||||
abstract class AbstractSSOFtueAuthFragment<VB : ViewBinding> : AbstractFtueAuthFragment<VB>() {
|
||||
|
||||
// For sso
|
||||
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
|
||||
private var customTabsClient: CustomTabsClient? = null
|
||||
private var customTabsSession: CustomTabsSession? = null
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val hasSSO = withState(viewModel) { it.loginMode.hasSso() }
|
||||
if (hasSSO) {
|
||||
val packageName = CustomTabsClient.getPackageName(requireContext(), null)
|
||||
|
||||
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
|
||||
if (packageName != null) {
|
||||
customTabsServiceConnection = object : CustomTabsServiceConnection() {
|
||||
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
|
||||
customTabsClient = client
|
||||
.also { it.warmup(0L) }
|
||||
prefetchIfNeeded()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
}
|
||||
}
|
||||
.also {
|
||||
CustomTabsClient.bindCustomTabsService(
|
||||
requireContext(),
|
||||
// Despite the API, packageName cannot be null
|
||||
packageName,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
val hasSSO = withState(viewModel) { it.loginMode.hasSso() }
|
||||
if (hasSSO) {
|
||||
customTabsServiceConnection?.let { requireContext().unbindService(it) }
|
||||
customTabsServiceConnection = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun prefetchUrl(url: String) {
|
||||
if (customTabsSession == null) {
|
||||
customTabsSession = customTabsClient?.newSession(null)
|
||||
}
|
||||
|
||||
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
|
||||
}
|
||||
|
||||
fun openInCustomTab(ssoUrl: String) {
|
||||
openUrlInChromeCustomTab(requireContext(), customTabsSession, ssoUrl)
|
||||
}
|
||||
|
||||
private fun prefetchIfNeeded() {
|
||||
withState(viewModel) { state ->
|
||||
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
|
||||
// in this case we can prefetch (not other cases for privacy concerns)
|
||||
viewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = null
|
||||
)
|
||||
?.let { prefetchUrl(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* 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.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 kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import timber.log.Timber
|
||||
import java.net.URLDecoder
|
||||
import java.util.Formatter
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class FtueAuthCaptchaFragmentArgument(
|
||||
val siteKey: String
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to confirm he is 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 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.homeServerUrl ?: 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 = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java).fromJson(json)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## shouldOverrideUrlLoading(): failed")
|
||||
}
|
||||
|
||||
val response = javascriptResponse?.response
|
||||
if (javascriptResponse?.action == "verifyCallback" && response != null) {
|
||||
viewModel.handle(OnboardingAction.CaptchaDone(response))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetLogin)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
if (!isWebViewLoaded) {
|
||||
setupWebView(state)
|
||||
isWebViewLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* 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.app.features.onboarding.ftueauth
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.args
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding
|
||||
import im.vector.app.features.login.TextInputFormFragmentMode
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.is401
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class FtueAuthGenericTextInputFormFragmentArgument(
|
||||
val mode: TextInputFormFragmentMode,
|
||||
val mandatory: Boolean,
|
||||
val extra: String = ""
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked for a text input
|
||||
*/
|
||||
class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentLoginGenericTextInputFormBinding>() {
|
||||
|
||||
private val params: FtueAuthGenericTextInputFormFragmentArgument by args()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginGenericTextInputFormBinding {
|
||||
return FragmentLoginGenericTextInputFormBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
setupUi()
|
||||
setupSubmitButton()
|
||||
setupTil()
|
||||
setupAutoFill()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginGenericTextInputFormOtherButton.setOnClickListener { onOtherButtonClicked() }
|
||||
views.loginGenericTextInputFormSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.loginGenericTextInputFormTextInput.setAutofillHints(
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS
|
||||
TextInputFormFragmentMode.SetMsisdn -> HintConstants.AUTOFILL_HINT_PHONE_NUMBER
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> HintConstants.AUTOFILL_HINT_SMS_OTP
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTil() {
|
||||
views.loginGenericTextInputFormTextInput.textChanges()
|
||||
.onEach {
|
||||
views.loginGenericTextInputFormTil.error = null
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> {
|
||||
views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title)
|
||||
views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice)
|
||||
views.loginGenericTextInputFormNotice2.setTextOrHide(null)
|
||||
views.loginGenericTextInputFormTil.hint =
|
||||
getString(if (params.mandatory) R.string.login_set_email_mandatory_hint else R.string.login_set_email_optional_hint)
|
||||
views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
||||
views.loginGenericTextInputFormOtherButton.isVisible = false
|
||||
views.loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit)
|
||||
}
|
||||
TextInputFormFragmentMode.SetMsisdn -> {
|
||||
views.loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title)
|
||||
views.loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice)
|
||||
views.loginGenericTextInputFormNotice2.setTextOrHide(getString(R.string.login_set_msisdn_notice2))
|
||||
views.loginGenericTextInputFormTil.hint =
|
||||
getString(if (params.mandatory) R.string.login_set_msisdn_mandatory_hint else R.string.login_set_msisdn_optional_hint)
|
||||
views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE
|
||||
views.loginGenericTextInputFormOtherButton.isVisible = false
|
||||
views.loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit)
|
||||
}
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
views.loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title)
|
||||
views.loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice, params.extra)
|
||||
views.loginGenericTextInputFormNotice2.setTextOrHide(null)
|
||||
views.loginGenericTextInputFormTil.hint =
|
||||
getString(R.string.login_msisdn_confirm_hint)
|
||||
views.loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
views.loginGenericTextInputFormOtherButton.isVisible = true
|
||||
views.loginGenericTextInputFormOtherButton.text = getString(R.string.login_msisdn_confirm_send_again)
|
||||
views.loginGenericTextInputFormSubmit.text = getString(R.string.login_msisdn_confirm_submit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOtherButtonClicked() {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
viewModel.handle(OnboardingAction.SendAgainThreePid)
|
||||
}
|
||||
else -> {
|
||||
// Should not happen, button is not displayed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
val text = views.loginGenericTextInputFormTextInput.text.toString()
|
||||
|
||||
if (text.isEmpty()) {
|
||||
// Perform dummy action
|
||||
viewModel.handle(OnboardingAction.RegisterDummy)
|
||||
} else {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> {
|
||||
viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Email(text)))
|
||||
}
|
||||
TextInputFormFragmentMode.SetMsisdn -> {
|
||||
getCountryCodeOrShowError(text)?.let { countryCode ->
|
||||
viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode)))
|
||||
}
|
||||
}
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
viewModel.handle(OnboardingAction.ValidateThreePid(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginGenericTextInputFormSubmit.hideKeyboard()
|
||||
views.loginGenericTextInputFormSubmit.error = null
|
||||
}
|
||||
|
||||
private fun getCountryCodeOrShowError(text: String): String? {
|
||||
// We expect an international format for the moment (see https://github.com/vector-im/riotX-android/issues/693)
|
||||
if (text.startsWith("+")) {
|
||||
try {
|
||||
val phoneNumber = PhoneNumberUtil.getInstance().parse(text, null)
|
||||
return PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
|
||||
} catch (e: NumberParseException) {
|
||||
views.loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_other)
|
||||
}
|
||||
} else {
|
||||
views.loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_not_international)
|
||||
}
|
||||
|
||||
// Error
|
||||
return null
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginGenericTextInputFormSubmit.isEnabled = false
|
||||
views.loginGenericTextInputFormTextInput.textChanges()
|
||||
.onEach {
|
||||
views.loginGenericTextInputFormSubmit.isEnabled = isInputValid(it)
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun isInputValid(input: CharSequence): Boolean {
|
||||
return if (input.isEmpty() && !params.mandatory) {
|
||||
true
|
||||
} else {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> {
|
||||
input.isEmail()
|
||||
}
|
||||
TextInputFormFragmentMode.SetMsisdn -> {
|
||||
input.isNotBlank()
|
||||
}
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
input.isNotBlank()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
when (params.mode) {
|
||||
TextInputFormFragmentMode.SetEmail -> {
|
||||
if (throwable.is401()) {
|
||||
// This is normal use case, we go to the mail waiting screen
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(viewModel.currentThreePid ?: "")))
|
||||
} else {
|
||||
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
TextInputFormFragmentMode.SetMsisdn -> {
|
||||
if (throwable.is401()) {
|
||||
// This is normal use case, we go to the enter code screen
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendMsisdnSuccess(viewModel.currentThreePid ?: "")))
|
||||
} else {
|
||||
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
when {
|
||||
throwable is Failure.SuccessError ->
|
||||
// The entered code is not correct
|
||||
views.loginGenericTextInputFormTil.error = getString(R.string.login_validation_code_is_not_correct)
|
||||
throwable.is401() ->
|
||||
// It can happen if user request again the 3pid
|
||||
Unit
|
||||
else ->
|
||||
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetLogin)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
/*
|
||||
* 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.app.features.onboarding.ftueauth
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.hidePassword
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentLoginBinding
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen:
|
||||
* In signin mode:
|
||||
* - the user is asked for login (or email) and password to sign in to a homeserver.
|
||||
* - He also can reset his password
|
||||
* In signup mode:
|
||||
* - the user is asked for login and password
|
||||
*/
|
||||
class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<FragmentLoginBinding>() {
|
||||
|
||||
private var isSignupMode = false
|
||||
|
||||
// Temporary patch for https://github.com/vector-im/riotX-android/issues/1410,
|
||||
// waiting for https://github.com/matrix-org/synapse/issues/7576
|
||||
private var isNumericOnlyUserIdForbidden = false
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginBinding {
|
||||
return FragmentLoginBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupSubmitButton()
|
||||
setupForgottenPasswordButton()
|
||||
|
||||
views.passwordField.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupForgottenPasswordButton() {
|
||||
views.forgetPasswordButton.setOnClickListener { forgetPasswordClicked() }
|
||||
}
|
||||
|
||||
private fun setupAutoFill(state: OnboardingViewState) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> {
|
||||
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
|
||||
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
|
||||
}
|
||||
SignMode.SignIn,
|
||||
SignMode.SignInWithMatrixId -> {
|
||||
views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
|
||||
views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSocialLoginButtons(state: OnboardingViewState) {
|
||||
views.loginSocialLoginButtons.mode = when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP
|
||||
SignMode.SignIn,
|
||||
SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
val login = views.loginField.text.toString()
|
||||
val password = views.passwordField.text.toString()
|
||||
|
||||
// This can be called by the IME action, so deal with empty cases
|
||||
var error = 0
|
||||
if (login.isEmpty()) {
|
||||
views.loginFieldTil.error = getString(if (isSignupMode) {
|
||||
R.string.error_empty_field_choose_user_name
|
||||
} else {
|
||||
R.string.error_empty_field_enter_user_name
|
||||
})
|
||||
error++
|
||||
}
|
||||
if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
|
||||
views.loginFieldTil.error = "The homeserver does not accept username with only digits."
|
||||
error++
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
views.passwordFieldTil.error = getString(if (isSignupMode) {
|
||||
R.string.error_empty_field_choose_password
|
||||
} else {
|
||||
R.string.error_empty_field_your_password
|
||||
})
|
||||
error++
|
||||
}
|
||||
|
||||
if (error == 0) {
|
||||
viewModel.handle(OnboardingAction.LoginOrRegister(login, password, getString(R.string.login_default_session_public_name)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginSubmit.hideKeyboard()
|
||||
views.loginFieldTil.error = null
|
||||
views.passwordFieldTil.error = null
|
||||
}
|
||||
|
||||
private fun setupUi(state: OnboardingViewState) {
|
||||
views.loginFieldTil.hint = getString(when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> R.string.login_signup_username_hint
|
||||
SignMode.SignIn -> R.string.login_signin_username_hint
|
||||
SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint
|
||||
})
|
||||
|
||||
// Handle direct signin first
|
||||
if (state.signMode == SignMode.SignInWithMatrixId) {
|
||||
views.loginServerIcon.isVisible = false
|
||||
views.loginTitle.text = getString(R.string.login_signin_matrix_id_title)
|
||||
views.loginNotice.text = getString(R.string.login_signin_matrix_id_notice)
|
||||
views.loginPasswordNotice.isVisible = true
|
||||
} else {
|
||||
val resId = when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> R.string.login_signup_to
|
||||
SignMode.SignIn -> R.string.login_connect_to
|
||||
SignMode.SignInWithMatrixId -> R.string.login_connect_to
|
||||
}
|
||||
|
||||
when (state.serverType) {
|
||||
ServerType.MatrixOrg -> {
|
||||
views.loginServerIcon.isVisible = true
|
||||
views.loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
|
||||
views.loginTitle.text = getString(resId, state.homeServerUrlFromUser.toReducedUrl())
|
||||
views.loginNotice.text = getString(R.string.login_server_matrix_org_text)
|
||||
}
|
||||
ServerType.EMS -> {
|
||||
views.loginServerIcon.isVisible = true
|
||||
views.loginServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services)
|
||||
views.loginTitle.text = getString(resId, "Element Matrix Services")
|
||||
views.loginNotice.text = getString(R.string.login_server_modular_text)
|
||||
}
|
||||
ServerType.Other -> {
|
||||
views.loginServerIcon.isVisible = false
|
||||
views.loginTitle.text = getString(resId, state.homeServerUrlFromUser.toReducedUrl())
|
||||
views.loginNotice.text = getString(R.string.login_server_other_text)
|
||||
}
|
||||
ServerType.Unknown -> Unit /* Should not happen */
|
||||
}
|
||||
views.loginPasswordNotice.isVisible = false
|
||||
|
||||
if (state.loginMode is LoginMode.SsoAndPassword) {
|
||||
views.loginSocialLoginContainer.isVisible = true
|
||||
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
|
||||
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||
override fun onProviderSelected(id: String?) {
|
||||
viewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = id
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
views.loginSocialLoginContainer.isVisible = false
|
||||
views.loginSocialLoginButtons.ssoIdentityProviders = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupButtons(state: OnboardingViewState) {
|
||||
views.forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn
|
||||
|
||||
views.loginSubmit.text = getString(when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> R.string.login_signup_submit
|
||||
SignMode.SignIn,
|
||||
SignMode.SignInWithMatrixId -> R.string.login_signin
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginSubmit.setOnClickListener { submit() }
|
||||
combine(
|
||||
views.loginField.textChanges().map { it.trim().isNotEmpty() },
|
||||
views.passwordField.textChanges().map { it.isNotEmpty() }
|
||||
) { isLoginNotEmpty, isPasswordNotEmpty ->
|
||||
isLoginNotEmpty && isPasswordNotEmpty
|
||||
}
|
||||
.onEach {
|
||||
views.loginFieldTil.error = null
|
||||
views.passwordFieldTil.error = null
|
||||
views.loginSubmit.isEnabled = it
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun forgetPasswordClicked() {
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetLogin)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
// Show M_WEAK_PASSWORD error in the password field
|
||||
if (throwable is Failure.ServerError &&
|
||||
throwable.error.code == MatrixError.M_WEAK_PASSWORD) {
|
||||
views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
} else {
|
||||
views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
isSignupMode = state.signMode == SignMode.SignUp
|
||||
isNumericOnlyUserIdForbidden = state.serverType == ServerType.MatrixOrg
|
||||
|
||||
setupUi(state)
|
||||
setupAutoFill(state)
|
||||
setupSocialLoginButtons(state)
|
||||
setupButtons(state)
|
||||
|
||||
when (state.asyncLoginAction) {
|
||||
is Loading -> {
|
||||
// Ensure password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
is Fail -> {
|
||||
val error = state.asyncLoginAction.error
|
||||
if (error is Failure.ServerError &&
|
||||
error.error.code == MatrixError.M_FORBIDDEN &&
|
||||
error.error.message.isEmpty()) {
|
||||
// Login with email, but email unknown
|
||||
views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
|
||||
} else {
|
||||
// Trick to display the error without text.
|
||||
views.loginFieldTil.error = " "
|
||||
if (error.isInvalidPassword() && spaceInPassword()) {
|
||||
views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
|
||||
} else {
|
||||
views.passwordFieldTil.error = errorFormatter.toHumanReadable(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Success is handled by the LoginActivity
|
||||
is Success -> Unit
|
||||
}
|
||||
|
||||
when (state.asyncRegistration) {
|
||||
is Loading -> {
|
||||
// Ensure password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
// Success is handled by the LoginActivity
|
||||
is Success -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if password ends or starts with spaces
|
||||
*/
|
||||
private fun spaceInPassword() = views.passwordField.text.toString().let { it.trim() != it }
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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.app.features.onboarding.ftueauth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.hidePassword
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentLoginResetPasswordBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked for email and new password to reset his password
|
||||
*/
|
||||
class FtueAuthResetPasswordFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentLoginResetPasswordBinding>() {
|
||||
|
||||
// Show warning only once
|
||||
private var showWarning = true
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordBinding {
|
||||
return FragmentLoginResetPasswordBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupSubmitButton()
|
||||
}
|
||||
|
||||
private fun setupUi(state: OnboardingViewState) {
|
||||
views.resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlFromUser.toReducedUrl())
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.resetPasswordSubmit.setOnClickListener { submit() }
|
||||
combine(
|
||||
views.resetPasswordEmail.textChanges().map { it.isEmail() },
|
||||
views.passwordField.textChanges().map { it.isNotEmpty() }
|
||||
) { isEmail, isPasswordNotEmpty ->
|
||||
isEmail && isPasswordNotEmpty
|
||||
}
|
||||
.onEach {
|
||||
views.resetPasswordEmailTil.error = null
|
||||
views.passwordFieldTil.error = null
|
||||
views.resetPasswordSubmit.isEnabled = it
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
if (showWarning) {
|
||||
showWarning = false
|
||||
// Display a warning as Riot-Web does first
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.login_reset_password_warning_title)
|
||||
.setMessage(R.string.login_reset_password_warning_content)
|
||||
.setPositiveButton(R.string.login_reset_password_warning_submit) { _, _ ->
|
||||
doSubmit()
|
||||
}
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.show()
|
||||
} else {
|
||||
doSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun doSubmit() {
|
||||
val email = views.resetPasswordEmail.text.toString()
|
||||
val password = views.passwordField.text.toString()
|
||||
|
||||
viewModel.handle(OnboardingAction.ResetPassword(email, password))
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.resetPasswordSubmit.hideKeyboard()
|
||||
views.resetPasswordEmailTil.error = null
|
||||
views.passwordFieldTil.error = null
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetResetPassword)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
setupUi(state)
|
||||
|
||||
when (state.asyncResetPassword) {
|
||||
is Loading -> {
|
||||
// Ensure new password is hidden
|
||||
views.passwordField.hidePassword()
|
||||
}
|
||||
is Fail -> {
|
||||
views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error)
|
||||
}
|
||||
is Success -> Unit
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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.app.features.onboarding.ftueauth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmationBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import org.matrix.android.sdk.api.failure.is401
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to check his email and to click on a button once it's done
|
||||
*/
|
||||
class FtueAuthResetPasswordMailConfirmationFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentLoginResetPasswordMailConfirmationBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordMailConfirmationBinding {
|
||||
return FragmentLoginResetPasswordMailConfirmationBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
views.resetPasswordMailConfirmationSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupUi(state: OnboardingViewState) {
|
||||
views.resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetPasswordEmail)
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
viewModel.handle(OnboardingAction.ResetPasswordMailConfirmed)
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetResetPassword)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
setupUi(state)
|
||||
|
||||
when (state.asyncResetMailConfirmed) {
|
||||
is Fail -> {
|
||||
// Link in email not yet clicked ?
|
||||
val message = if (state.asyncResetMailConfirmed.error.is401()) {
|
||||
getString(R.string.auth_reset_password_error_unauthorized)
|
||||
} else {
|
||||
errorFormatter.toHumanReadable(state.asyncResetMailConfirmed.error)
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
is Success -> Unit
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.onboarding.ftueauth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import im.vector.app.databinding.FragmentLoginResetPasswordSuccessBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, we confirm to the user that his password has been reset
|
||||
*/
|
||||
class FtueAuthResetPasswordSuccessFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentLoginResetPasswordSuccessBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginResetPasswordSuccessBinding {
|
||||
return FragmentLoginResetPasswordSuccessBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
views.resetPasswordSuccessSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetResetPassword)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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.app.features.onboarding.ftueauth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||
import im.vector.app.databinding.FragmentLoginServerSelectionBinding
|
||||
import im.vector.app.features.login.EMS_LINK
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import me.gujun.android.span.span
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user will choose between matrix.org, modular or other type of homeserver
|
||||
*/
|
||||
class FtueAuthServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentLoginServerSelectionBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginServerSelectionBinding {
|
||||
return FragmentLoginServerSelectionBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
initViews()
|
||||
initTextViews()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
views.loginServerChoiceEmsLearnMore.setOnClickListener { learnMore() }
|
||||
views.loginServerChoiceMatrixOrg.setOnClickListener { selectMatrixOrg() }
|
||||
views.loginServerChoiceEms.setOnClickListener { selectEMS() }
|
||||
views.loginServerChoiceOther.setOnClickListener { selectOther() }
|
||||
views.loginServerIKnowMyIdSubmit.setOnClickListener { loginWithMatrixId() }
|
||||
}
|
||||
|
||||
private fun updateSelectedChoice(state: OnboardingViewState) {
|
||||
views.loginServerChoiceMatrixOrg.isChecked = state.serverType == ServerType.MatrixOrg
|
||||
}
|
||||
|
||||
private fun initTextViews() {
|
||||
views.loginServerChoiceEmsLearnMore.text = span {
|
||||
text = getString(R.string.login_server_modular_learn_more)
|
||||
textDecorationLine = "underline"
|
||||
}
|
||||
}
|
||||
|
||||
private fun learnMore() {
|
||||
openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK)
|
||||
}
|
||||
|
||||
private fun selectMatrixOrg() {
|
||||
viewModel.handle(OnboardingAction.UpdateServerType(ServerType.MatrixOrg))
|
||||
}
|
||||
|
||||
private fun selectEMS() {
|
||||
viewModel.handle(OnboardingAction.UpdateServerType(ServerType.EMS))
|
||||
}
|
||||
|
||||
private fun selectOther() {
|
||||
viewModel.handle(OnboardingAction.UpdateServerType(ServerType.Other))
|
||||
}
|
||||
|
||||
private fun loginWithMatrixId() {
|
||||
viewModel.handle(OnboardingAction.UpdateSignMode(SignMode.SignInWithMatrixId))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetHomeServerType)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
updateSelectedChoice(state)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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.app.features.onboarding.ftueauth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.utils.ensureProtocol
|
||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||
import im.vector.app.databinding.FragmentLoginServerUrlFormBinding
|
||||
import im.vector.app.features.login.EMS_LINK
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import java.net.UnknownHostException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is prompted to enter a homeserver url
|
||||
*/
|
||||
class FtueAuthServerUrlFormFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentLoginServerUrlFormBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginServerUrlFormBinding {
|
||||
return FragmentLoginServerUrlFormBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
setupHomeServerField()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginServerUrlFormLearnMore.setOnClickListener { learnMore() }
|
||||
views.loginServerUrlFormClearHistory.setOnClickListener { clearHistory() }
|
||||
views.loginServerUrlFormSubmit.setOnClickListener { submit() }
|
||||
}
|
||||
|
||||
private fun setupHomeServerField() {
|
||||
views.loginServerUrlFormHomeServerUrl.textChanges()
|
||||
.onEach {
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = null
|
||||
views.loginServerUrlFormSubmit.isEnabled = it.isNotBlank()
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
|
||||
views.loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
views.loginServerUrlFormHomeServerUrl.dismissDropDown()
|
||||
submit()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUi(state: OnboardingViewState) {
|
||||
when (state.serverType) {
|
||||
ServerType.EMS -> {
|
||||
views.loginServerUrlFormIcon.isVisible = true
|
||||
views.loginServerUrlFormTitle.text = getString(R.string.login_connect_to_modular)
|
||||
views.loginServerUrlFormText.text = getString(R.string.login_server_url_form_modular_text)
|
||||
views.loginServerUrlFormLearnMore.isVisible = true
|
||||
views.loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_modular_hint)
|
||||
views.loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_modular_notice)
|
||||
}
|
||||
else -> {
|
||||
views.loginServerUrlFormIcon.isVisible = false
|
||||
views.loginServerUrlFormTitle.text = getString(R.string.login_server_other_title)
|
||||
views.loginServerUrlFormText.text = getString(R.string.login_connect_to_a_custom_server)
|
||||
views.loginServerUrlFormLearnMore.isVisible = false
|
||||
views.loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_other_hint)
|
||||
views.loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_common_notice)
|
||||
}
|
||||
}
|
||||
val completions = state.knownCustomHomeServersUrls + if (BuildConfig.DEBUG) listOf("http://10.0.2.2:8080") else emptyList()
|
||||
views.loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter(
|
||||
requireContext(),
|
||||
R.layout.item_completion_homeserver,
|
||||
completions
|
||||
))
|
||||
views.loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
|
||||
.takeIf { completions.isNotEmpty() }
|
||||
?: TextInputLayout.END_ICON_NONE
|
||||
}
|
||||
|
||||
private fun learnMore() {
|
||||
openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK)
|
||||
}
|
||||
|
||||
private fun clearHistory() {
|
||||
viewModel.handle(OnboardingAction.ClearHomeServerHistory)
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetHomeServerUrl)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun submit() {
|
||||
cleanupUi()
|
||||
|
||||
// Static check of homeserver url, empty, malformed, etc.
|
||||
val serverUrl = views.loginServerUrlFormHomeServerUrl.text.toString().trim().ensureProtocol()
|
||||
|
||||
when {
|
||||
serverUrl.isBlank() -> {
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server)
|
||||
}
|
||||
else -> {
|
||||
views.loginServerUrlFormHomeServerUrl.setText(serverUrl, false /* to avoid completion dialog flicker*/)
|
||||
viewModel.handle(OnboardingAction.UpdateHomeServer(serverUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupUi() {
|
||||
views.loginServerUrlFormSubmit.hideKeyboard()
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = null
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
views.loginServerUrlFormHomeServerUrlTil.error = if (throwable is Failure.NetworkConnection &&
|
||||
throwable.ioException is UnknownHostException) {
|
||||
// Invalid homeserver?
|
||||
getString(R.string.login_error_homeserver_not_found)
|
||||
} else {
|
||||
errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
setupUi(state)
|
||||
|
||||
views.loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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.app.features.onboarding.ftueauth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import im.vector.app.features.login.ssoIdentityProviders
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to sign up or to sign in to the homeserver
|
||||
*/
|
||||
class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOFtueAuthFragment<FragmentLoginSignupSigninSelectionBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSignupSigninSelectionBinding {
|
||||
return FragmentLoginSignupSigninSelectionBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginSignupSigninSubmit.setOnClickListener { submit() }
|
||||
views.loginSignupSigninSignIn.setOnClickListener { signIn() }
|
||||
}
|
||||
|
||||
private fun setupUi(state: OnboardingViewState) {
|
||||
when (state.serverType) {
|
||||
ServerType.MatrixOrg -> {
|
||||
views.loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
|
||||
views.loginSignupSigninServerIcon.isVisible = true
|
||||
views.loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl())
|
||||
views.loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text)
|
||||
}
|
||||
ServerType.EMS -> {
|
||||
views.loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services)
|
||||
views.loginSignupSigninServerIcon.isVisible = true
|
||||
views.loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular)
|
||||
views.loginSignupSigninText.text = state.homeServerUrlFromUser.toReducedUrl()
|
||||
}
|
||||
ServerType.Other -> {
|
||||
views.loginSignupSigninServerIcon.isVisible = false
|
||||
views.loginSignupSigninTitle.text = getString(R.string.login_server_other_title)
|
||||
views.loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrlFromUser.toReducedUrl())
|
||||
}
|
||||
ServerType.Unknown -> Unit /* Should not happen */
|
||||
}
|
||||
|
||||
when (state.loginMode) {
|
||||
is LoginMode.SsoAndPassword -> {
|
||||
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true
|
||||
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()?.sorted()
|
||||
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||
override fun onProviderSelected(id: String?) {
|
||||
viewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = id
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// SSO only is managed without container as well as No sso
|
||||
views.loginSignupSigninSignInSocialLoginContainer.isVisible = false
|
||||
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupButtons(state: OnboardingViewState) {
|
||||
when (state.loginMode) {
|
||||
is LoginMode.Sso -> {
|
||||
// change to only one button that is sign in with sso
|
||||
views.loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
|
||||
views.loginSignupSigninSignIn.isVisible = false
|
||||
}
|
||||
else -> {
|
||||
views.loginSignupSigninSubmit.text = getString(R.string.login_signup)
|
||||
views.loginSignupSigninSignIn.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() = withState(viewModel) { state ->
|
||||
if (state.loginMode is LoginMode.Sso) {
|
||||
viewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = null
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
} else {
|
||||
viewModel.handle(OnboardingAction.UpdateSignMode(SignMode.SignUp))
|
||||
}
|
||||
}
|
||||
|
||||
private fun signIn() {
|
||||
viewModel.handle(OnboardingAction.UpdateSignMode(SignMode.SignIn))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetSignMode)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
setupUi(state)
|
||||
setupButtons(state)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.FragmentLoginSplashBinding
|
||||
import im.vector.app.features.login.AbstractLoginFragment
|
||||
import im.vector.app.features.login.LoginAction
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import java.net.UnknownHostException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* In this screen, the user is viewing an introduction to what he can do with this application
|
||||
*/
|
||||
class FtueAuthSplashFragment @Inject constructor(
|
||||
private val vectorPreferences: VectorPreferences
|
||||
) : AbstractLoginFragment<FragmentLoginSplashBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSplashBinding {
|
||||
return FragmentLoginSplashBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.loginSplashSubmit.debouncedClicks { getStarted() }
|
||||
|
||||
if (BuildConfig.DEBUG || vectorPreferences.developerMode()) {
|
||||
views.loginSplashVersion.isVisible = true
|
||||
@SuppressLint("SetTextI18n")
|
||||
views.loginSplashVersion.text = "Version : ${BuildConfig.VERSION_NAME}\n" +
|
||||
"Branch: ${BuildConfig.GIT_BRANCH_NAME}\n" +
|
||||
"Build: ${BuildConfig.BUILD_NUMBER}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStarted() {
|
||||
loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = false))
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
if (throwable is Failure.NetworkConnection &&
|
||||
throwable.ioException is UnknownHostException) {
|
||||
// Invalid homeserver from URL config
|
||||
val url = loginViewModel.getInitialHomeServerUrl().orEmpty()
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(getString(R.string.login_error_homeserver_from_url_not_found, url))
|
||||
.setPositiveButton(R.string.login_error_homeserver_from_url_not_found_enter_manual) { _, _ ->
|
||||
loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = true))
|
||||
}
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.show()
|
||||
} else {
|
||||
super.onError(throwable)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.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.R
|
||||
import im.vector.app.databinding.FragmentLoginWaitForEmailBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.failure.is401
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class FtueAuthWaitForEmailFragmentArgument(
|
||||
val email: String
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* In this screen, the user is asked to check his emails
|
||||
*/
|
||||
class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentLoginWaitForEmailBinding>() {
|
||||
|
||||
private val params: FtueAuthWaitForEmailFragmentArgument by args()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWaitForEmailBinding {
|
||||
return FragmentLoginWaitForEmailBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupUi()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(0))
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
viewModel.handle(OnboardingAction.StopEmailValidationCheck)
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
views.loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice, params.email)
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
if (throwable.is401()) {
|
||||
// Try again, with a delay
|
||||
viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(10_000))
|
||||
} else {
|
||||
super.onError(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetLogin)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
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.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.utils.AssetReader
|
||||
import im.vector.app.databinding.FragmentLoginWebBinding
|
||||
import im.vector.app.features.login.JavascriptResponse
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import im.vector.app.features.signout.soft.SoftLogoutAction
|
||||
import im.vector.app.features.signout.soft.SoftLogoutViewModel
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import timber.log.Timber
|
||||
import java.net.URLDecoder
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This screen is displayed when the application does not support login flow or registration flow
|
||||
* of the homeserver, as a fallback to login or to create an account
|
||||
*/
|
||||
class FtueAuthWebFragment @Inject constructor(
|
||||
private val assetReader: AssetReader
|
||||
) : AbstractFtueAuthFragment<FragmentLoginWebBinding>() {
|
||||
|
||||
val softLogoutViewModel: SoftLogoutViewModel by activityViewModel()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWebBinding {
|
||||
return FragmentLoginWebBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
private var isWebViewLoaded = false
|
||||
private var isForSessionRecovery = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupToolbar(views.loginWebToolbar)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
setupTitle(state)
|
||||
|
||||
isForSessionRecovery = state.deviceId?.isNotBlank() == true
|
||||
|
||||
if (!isWebViewLoaded) {
|
||||
setupWebView(state)
|
||||
isWebViewLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTitle(state: OnboardingViewState) {
|
||||
views.loginWebToolbar.title = when (state.signMode) {
|
||||
SignMode.SignIn -> getString(R.string.login_signin)
|
||||
else -> getString(R.string.login_signup)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun setupWebView(state: OnboardingViewState) {
|
||||
views.loginWebWebView.settings.javaScriptEnabled = true
|
||||
|
||||
// Enable local storage to support SSO with Firefox accounts
|
||||
views.loginWebWebView.settings.domStorageEnabled = true
|
||||
views.loginWebWebView.settings.databaseEnabled = true
|
||||
|
||||
// Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack
|
||||
// the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK)
|
||||
views.loginWebWebView.settings.userAgentString = "Mozilla/5.0 Google"
|
||||
|
||||
// AppRTC requires third party cookies to work
|
||||
val cookieManager = android.webkit.CookieManager.getInstance()
|
||||
|
||||
// clear the cookies
|
||||
if (cookieManager == null) {
|
||||
launchWebView(state)
|
||||
} else {
|
||||
if (!cookieManager.hasCookies()) {
|
||||
launchWebView(state)
|
||||
} else {
|
||||
try {
|
||||
cookieManager.removeAllCookies { launchWebView(state) }
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, " cookieManager.removeAllCookie() fails")
|
||||
launchWebView(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchWebView(state: OnboardingViewState) {
|
||||
val url = viewModel.getFallbackUrl(state.signMode == SignMode.SignIn, state.deviceId) ?: return
|
||||
|
||||
views.loginWebWebView.loadUrl(url)
|
||||
|
||||
views.loginWebWebView.webViewClient = object : WebViewClient() {
|
||||
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler,
|
||||
error: SslError) {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setMessage(R.string.ssl_could_not_verify)
|
||||
.setPositiveButton(R.string.ssl_trust) { _, _ -> handler.proceed() }
|
||||
.setNegativeButton(R.string.ssl_do_not_trust) { _, _ -> handler.cancel() }
|
||||
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
handler.cancel()
|
||||
dialog.dismiss()
|
||||
return@OnKeyListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
|
||||
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnWebLoginError(errorCode, description, failingUrl)))
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
|
||||
views.loginWebToolbar.subtitle = url
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
// avoid infinite onPageFinished call
|
||||
if (url.startsWith("http")) {
|
||||
// Generic method to make a bridge between JS and the UIWebView
|
||||
assetReader.readAssetFile("sendObject.js")?.let { view.loadUrl(it) }
|
||||
|
||||
if (state.signMode == SignMode.SignIn) {
|
||||
// The function the fallback page calls when the login is complete
|
||||
assetReader.readAssetFile("onLogin.js")?.let { view.loadUrl(it) }
|
||||
} else {
|
||||
// MODE_REGISTER
|
||||
// The function the fallback page calls when the registration is complete
|
||||
assetReader.readAssetFile("onRegistered.js")?.let { view.loadUrl(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example of (formatted) url for MODE_LOGIN:
|
||||
*
|
||||
* <pre>
|
||||
* js:{
|
||||
* "action":"onLogin",
|
||||
* "credentials":{
|
||||
* "user_id":"@user:matrix.org",
|
||||
* "access_token":"[ACCESS_TOKEN]",
|
||||
* "home_server":"matrix.org",
|
||||
* "device_id":"[DEVICE_ID]",
|
||||
* "well_known":{
|
||||
* "m.homeserver":{
|
||||
* "base_url":"https://matrix.org/"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* @param view
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
|
||||
if (url == null) return super.shouldOverrideUrlLoading(view, url as String?)
|
||||
|
||||
if (url.startsWith("js:")) {
|
||||
var json = url.substring(3)
|
||||
var javascriptResponse: JavascriptResponse? = null
|
||||
|
||||
try {
|
||||
// URL decode
|
||||
json = URLDecoder.decode(json, "UTF-8")
|
||||
val adapter = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java)
|
||||
javascriptResponse = adapter.fromJson(json)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed")
|
||||
}
|
||||
|
||||
// succeeds to parse parameters
|
||||
if (javascriptResponse != null) {
|
||||
val action = javascriptResponse.action
|
||||
|
||||
if (state.signMode == SignMode.SignIn) {
|
||||
if (action == "onLogin") {
|
||||
javascriptResponse.credentials?.let { notifyViewModel(it) }
|
||||
}
|
||||
} else {
|
||||
// MODE_REGISTER
|
||||
// check the required parameters
|
||||
if (action == "onRegistered") {
|
||||
javascriptResponse.credentials?.let { notifyViewModel(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return super.shouldOverrideUrlLoading(view, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyViewModel(credentials: Credentials) {
|
||||
if (isForSessionRecovery) {
|
||||
softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials))
|
||||
} else {
|
||||
viewModel.handle(OnboardingAction.WebLoginSuccess(credentials))
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetLogin)
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||
return when {
|
||||
toolbarButton -> super.onBackPressed(toolbarButton)
|
||||
views.loginWebWebView.canGoBack() -> views.loginWebWebView.goBack().run { true }
|
||||
else -> super.onBackPressed(toolbarButton)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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.ftueauth.AbstractFtueAuthFragment
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class FtueAuthTermsFragmentArgument(
|
||||
val localizedFlowDataLoginTerms: List<LocalizedFlowDataLoginTerms>
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* LoginTermsFragment displays the list of policies the user has to accept
|
||||
*/
|
||||
class FtueAuthTermsFragment @Inject constructor(
|
||||
private val policyController: PolicyController
|
||||
) : AbstractFtueAuthFragment<FragmentLoginTermsBinding>(),
|
||||
PolicyController.PolicyControllerListener {
|
||||
|
||||
private val params: FtueAuthTermsFragmentArgument 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)
|
||||
|
||||
// Button is enabled only if all checkboxes are checked
|
||||
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.AcceptTerms)
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
policyController.homeServer = state.homeServerUrlFromUser.toReducedUrl()
|
||||
renderState()
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetLogin)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue