From 77fc6cbed2ddea05780c05a641edd942ad68c515 Mon Sep 17 00:00:00 2001 From: Kirill Izmaylov Date: Tue, 27 Feb 2024 13:27:30 +0300 Subject: [PATCH] feat: eula on registration and sign in (#236) --- .../main/java/org/openedx/app/di/AppModule.kt | 2 + .../java/org/openedx/app/di/ScreenModule.kt | 5 +- .../auth/presentation/AgreementProvider.kt | 44 ++++++++ .../openedx/auth/presentation/AuthRouter.kt | 2 + .../presentation/signin/SignInFragment.kt | 38 ++----- .../auth/presentation/signin/SignInUIState.kt | 3 + .../presentation/signin/SignInViewModel.kt | 44 +++++++- .../presentation/signin/compose/SignInView.kt | 17 ++- .../presentation/signup/SignUpFragment.kt | 3 + .../auth/presentation/signup/SignUpUIState.kt | 3 + .../presentation/signup/SignUpViewModel.kt | 103 ++++++++++++------ .../presentation/signup/compose/SignUpView.kt | 85 ++++++++++----- .../openedx/auth/presentation/ui/AuthUI.kt | 14 ++- .../auth/presentation/ui/CheckboxField.kt | 62 +++++++++++ auth/src/main/res/values/strings.xml | 6 + .../signin/SignInViewModelTest.kt | 31 ++++++ .../signup/SignUpViewModelTest.kt | 29 ++++- .../java/org/openedx/core/ApiConstants.kt | 6 +- .../java/org/openedx/core/config/Config.kt | 5 + .../core/domain/model/RegistrationField.kt | 26 ++++- .../openedx/core/extension/TextConverter.kt | 9 +- .../java/org/openedx/core/ui/ComposeCommon.kt | 3 +- 22 files changed, 427 insertions(+), 113 deletions(-) create mode 100644 auth/src/main/java/org/openedx/auth/presentation/AgreementProvider.kt create mode 100644 auth/src/main/java/org/openedx/auth/presentation/ui/CheckboxField.kt diff --git a/app/src/main/java/org/openedx/app/di/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt index 403f50d0c..f798d8559 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -18,6 +18,7 @@ import org.openedx.app.data.storage.PreferencesManager import org.openedx.app.room.AppDatabase import org.openedx.app.room.DATABASE_NAME import org.openedx.app.system.notifier.AppNotifier +import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthRouter import org.openedx.auth.presentation.sso.FacebookAuthHelper @@ -168,6 +169,7 @@ val appModule = module { single { get() } single { get() } + factory { AgreementProvider(get(), get()) } factory { FacebookAuthHelper() } factory { GoogleAuthHelper(get()) } factory { MicrosoftAuthHelper() } diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index a69c5f01e..c74d007a5 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -75,13 +75,16 @@ val screenModule = module { get(), get(), get(), + get(), + get(), + get(), courseId, infoType, ) } viewModel { (courseId: String?, infoType: String?) -> - SignUpViewModel(get(), get(), get(), get(), get(), get(), get(), courseId, infoType) + SignUpViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), courseId, infoType) } viewModel { RestorePasswordViewModel(get(), get(), get(), get()) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/AgreementProvider.kt b/auth/src/main/java/org/openedx/auth/presentation/AgreementProvider.kt new file mode 100644 index 000000000..0141df227 --- /dev/null +++ b/auth/src/main/java/org/openedx/auth/presentation/AgreementProvider.kt @@ -0,0 +1,44 @@ +package org.openedx.auth.presentation + +import androidx.compose.ui.text.intl.Locale +import org.openedx.auth.R +import org.openedx.core.config.Config +import org.openedx.core.system.ResourceManager + +class AgreementProvider( + private val config: Config, + private val resourceManager: ResourceManager, +) { + internal fun getAgreement(isSignIn: Boolean): String? { + val agreementConfig = config.getAgreement(Locale.current.language) + if (agreementConfig.eulaUrl.isBlank()) return null + val platformName = config.getPlatformName() + val agreementRes = if (isSignIn) { + R.string.auth_agreement_signin_in + } else { + R.string.auth_agreement_creating_account + } + val eula = resourceManager.getString( + R.string.auth_cdata_template, + agreementConfig.eulaUrl, + "$platformName ${resourceManager.getString(R.string.auth_agreement_eula)}" + ) + val tos = resourceManager.getString( + R.string.auth_cdata_template, + agreementConfig.tosUrl, + "$platformName ${resourceManager.getString(R.string.auth_agreement_tos)}" + ) + val privacy = resourceManager.getString( + R.string.auth_cdata_template, + agreementConfig.privacyPolicyUrl, + "$platformName ${resourceManager.getString(R.string.auth_agreement_privacy)}" + ) + return resourceManager.getString( + agreementRes, + eula, + tos, + config.getPlatformName(), + privacy, + ) + } +} diff --git a/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt b/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt index a9a8357b7..9b1266119 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt @@ -20,5 +20,7 @@ interface AuthRouter { fun navigateToNativeDiscoverCourses(fm: FragmentManager, querySearch: String) + fun navigateToWebContent(fm: FragmentManager, title: String, url: String) + fun clearBackStack(fm: FragmentManager) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt index fb613125f..fabd8a40b 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt @@ -11,14 +11,11 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf import androidx.fragment.app.Fragment -import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.auth.data.model.AuthType -import org.openedx.auth.presentation.AuthRouter import org.openedx.auth.presentation.signin.compose.LoginScreen import org.openedx.core.AppUpdateState -import org.openedx.core.presentation.global.WhatsNewGlobalManager import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme @@ -31,8 +28,6 @@ class SignInFragment : Fragment() { requireArguments().getString(ARG_INFO_TYPE, "") ) } - private val router: AuthRouter by inject() - private val whatsNewGlobalManager by inject() override fun onCreateView( inflater: LayoutInflater, @@ -61,41 +56,27 @@ class SignInFragment : Fragment() { ) AuthEvent.ForgotPasswordClick -> { - viewModel.forgotPasswordClickedEvent() - router.navigateToRestorePassword(parentFragmentManager) + viewModel.navigateToForgotPassword(parentFragmentManager) } AuthEvent.RegisterClick -> { - viewModel.signUpClickedEvent() - router.navigateToSignUp(parentFragmentManager, null, null) + viewModel.navigateToSignUp(parentFragmentManager) } AuthEvent.BackClick -> { requireActivity().supportFragmentManager.popBackStackImmediate() } + + is AuthEvent.OpenLink -> viewModel.openLink( + parentFragmentManager, + event.links, + event.link + ) } }, ) LaunchedEffect(state.loginSuccess) { - val isNeedToShowWhatsNew = - whatsNewGlobalManager.shouldShowWhatsNew() - if (state.loginSuccess) { - router.clearBackStack(parentFragmentManager) - if (isNeedToShowWhatsNew) { - router.navigateToWhatsNew( - parentFragmentManager, - viewModel.courseId, - viewModel.infoType - ) - } else { - router.navigateToMain( - parentFragmentManager, - viewModel.courseId, - viewModel.infoType - ) - } - } - + viewModel.proceedWhatsNew(parentFragmentManager) } } else { AppUpgradeRequiredScreen( @@ -125,6 +106,7 @@ class SignInFragment : Fragment() { internal sealed interface AuthEvent { data class SignIn(val login: String, val password: String) : AuthEvent data class SocialSignIn(val authType: AuthType) : AuthEvent + data class OpenLink(val links: Map, val link: String) : AuthEvent object RegisterClick : AuthEvent object ForgotPasswordClick : AuthEvent object BackClick : AuthEvent diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt index 829d376f1..9ce5cfc98 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt @@ -1,5 +1,7 @@ package org.openedx.auth.presentation.signin +import org.openedx.core.domain.model.RegistrationField + /** * Data class to store UI state of the SignIn screen * @@ -18,4 +20,5 @@ internal data class SignInUIState( val isLogistrationEnabled: Boolean = false, val showProgress: Boolean = false, val loginSuccess: Boolean = false, + val agreement: RegistrationField? = null, ) diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt index d47950341..cf1c6afa5 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt @@ -1,6 +1,7 @@ package org.openedx.auth.presentation.signin import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope @@ -14,7 +15,9 @@ import org.openedx.auth.R import org.openedx.auth.data.model.AuthType import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.domain.model.SocialAuthResponse +import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics +import org.openedx.auth.presentation.AuthRouter import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.BaseViewModel import org.openedx.core.SingleEventLiveData @@ -22,7 +25,9 @@ import org.openedx.core.UIMessage import org.openedx.core.Validator import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.model.createHonorCodeField import org.openedx.core.extension.isInternetError +import org.openedx.core.presentation.global.WhatsNewGlobalManager import org.openedx.core.system.EdxError import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.AppUpgradeEvent @@ -38,6 +43,9 @@ class SignInViewModel( private val appUpgradeNotifier: AppUpgradeNotifier, private val analytics: AuthAnalytics, private val oAuthHelper: OAuthHelper, + private val router: AuthRouter, + private val whatsNewGlobalManager: WhatsNewGlobalManager, + agreementProvider: AgreementProvider, config: Config, val courseId: String?, val infoType: String?, @@ -52,6 +60,7 @@ class SignInViewModel( isMicrosoftAuthEnabled = config.getMicrosoftConfig().isEnabled(), isSocialAuthEnabled = config.isSocialAuthEnabled(), isLogistrationEnabled = config.isPreLoginExperienceEnabled(), + agreement = agreementProvider.getAgreement(isSignIn = true)?.createHonorCodeField(), ) ) internal val uiState: StateFlow = _uiState @@ -124,11 +133,13 @@ class SignInViewModel( } } - fun signUpClickedEvent() { + fun navigateToSignUp(parentFragmentManager: FragmentManager) { + router.navigateToSignUp(parentFragmentManager, null, null) analytics.signUpClickedEvent() } - fun forgotPasswordClickedEvent() { + fun navigateToForgotPassword(parentFragmentManager: FragmentManager) { + router.navigateToRestorePassword(parentFragmentManager) analytics.forgotPasswordClickedEvent() } @@ -177,4 +188,33 @@ class SignInViewModel( } } ?: onUnknownError() } + + fun openLink(fragmentManager: FragmentManager, links: Map, link: String) { + links.forEach { (key, value) -> + if (value == link) { + router.navigateToWebContent(fragmentManager, key, value) + return + } + } + } + + fun proceedWhatsNew(parentFragmentManager: FragmentManager) { + val isNeedToShowWhatsNew = whatsNewGlobalManager.shouldShowWhatsNew() + if (uiState.value.loginSuccess) { + router.clearBackStack(parentFragmentManager) + if (isNeedToShowWhatsNew) { + router.navigateToWhatsNew( + parentFragmentManager, + courseId, + infoType + ) + } else { + router.navigateToMain( + parentFragmentManager, + courseId, + infoType + ) + } + } + } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt index 0abcf0bf9..c40884d6f 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt @@ -59,8 +59,10 @@ import org.openedx.auth.presentation.signin.SignInUIState import org.openedx.auth.presentation.ui.LoginTextField import org.openedx.auth.presentation.ui.SocialAuthView import org.openedx.core.UIMessage +import org.openedx.core.extension.TextConverter import org.openedx.core.ui.BackBtn import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.HyperlinkText import org.openedx.core.ui.OpenEdXButton import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType @@ -185,6 +187,20 @@ internal fun LoginScreen( state, onEvent, ) + state.agreement?.let { + Spacer(modifier = Modifier.height(24.dp)) + val linkedText = + TextConverter.htmlTextToLinkedText(state.agreement.label) + HyperlinkText( + modifier = Modifier.testTag("txt_${state.agreement.name}"), + fullText = linkedText.text, + hyperLinks = linkedText.links, + linkTextColor = MaterialTheme.appColors.primary, + action = { link -> + onEvent(AuthEvent.OpenLink(linkedText.links, link)) + }, + ) + } } } } @@ -349,7 +365,6 @@ private fun SignInScreenPreview() { } } - @Preview(name = "NEXUS_9_Light", device = Devices.NEXUS_9, uiMode = UI_MODE_NIGHT_NO) @Preview(name = "NEXUS_9_Night", device = Devices.NEXUS_9, uiMode = UI_MODE_NIGHT_YES) @Composable diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt index 2408202df..fa27d7d60 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt @@ -70,6 +70,9 @@ class SignUpFragment : Fragment() { }, onFieldUpdated = { key, value -> viewModel.updateField(key, value) + }, + onHyperLinkClick = { links, link -> + viewModel.openLink(parentFragmentManager, links, link) } ) diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpUIState.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpUIState.kt index 23e0458d9..0f7873b78 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpUIState.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpUIState.kt @@ -6,6 +6,9 @@ import org.openedx.core.system.notifier.AppUpgradeEvent data class SignUpUIState( val allFields: List = emptyList(), + val requiredFields: List = emptyList(), + val optionalFields: List = emptyList(), + val agreementFields: List = emptyList(), val isFacebookAuthEnabled: Boolean = false, val isGoogleAuthEnabled: Boolean = false, val isMicrosoftAuthEnabled: Boolean = false, diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt index e8ca67e93..2b7cdc09f 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt @@ -1,6 +1,7 @@ package org.openedx.auth.presentation.signup import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.BufferOverflow @@ -14,20 +15,23 @@ import kotlinx.coroutines.withContext import org.openedx.auth.data.model.AuthType import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.domain.model.SocialAuthResponse +import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics +import org.openedx.auth.presentation.AuthRouter import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.ApiConstants import org.openedx.core.BaseViewModel -import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.RegistrationField import org.openedx.core.domain.model.RegistrationFieldType +import org.openedx.core.domain.model.createHonorCodeField import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.core.utils.Logger +import org.openedx.core.R as coreR class SignUpViewModel( private val interactor: AuthInteractor, @@ -35,8 +39,10 @@ class SignUpViewModel( private val analytics: AuthAnalytics, private val preferencesManager: CorePreferences, private val appUpgradeNotifier: AppUpgradeNotifier, + private val agreementProvider: AgreementProvider, private val oAuthHelper: OAuthHelper, private val config: Config, + private val router: AuthRouter, val courseId: String?, val infoType: String?, ) : BaseViewModel() { @@ -69,35 +75,63 @@ class SignUpViewModel( _uiState.update { it.copy(isLoading = true) } viewModelScope.launch { try { - val allFields = interactor.getRegistrationFields() - _uiState.update { state -> - state.copy( - allFields = allFields, - isLoading = false, - ) - } + updateFields(interactor.getRegistrationFields()) } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.emit( UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) + resourceManager.getString(coreR.string.core_error_no_connection) ) ) } else { _uiMessage.emit( UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) + resourceManager.getString(coreR.string.core_error_unknown_error) ) ) } + } finally { + _uiState.update { state -> + state.copy(isLoading = false) + } } } } + private fun updateFields(allFields: List) { + val mutableAllFields = allFields.toMutableList() + val requiredFields = mutableListOf() + val optionalFields = mutableListOf() + val agreementFields = mutableListOf() + val agreementText = agreementProvider.getAgreement(isSignIn = false) + if (agreementText != null) { + val honourCode = allFields.find { it.name == ApiConstants.RegistrationFields.HONOR_CODE } + val marketingEmails = allFields.find { it.name == ApiConstants.RegistrationFields.MARKETING_EMAILS } + mutableAllFields.remove(honourCode) + requiredFields.addAll(mutableAllFields.filter { it.required }) + optionalFields.addAll(mutableAllFields.filter { !it.required }) + requiredFields.remove(marketingEmails) + optionalFields.remove(marketingEmails) + marketingEmails?.let { agreementFields.add(it) } + agreementFields.add(agreementText.createHonorCodeField()) + } else { + requiredFields.addAll(mutableAllFields.filter { it.required }) + optionalFields.addAll(mutableAllFields.filter { !it.required }) + } + _uiState.update { state -> + state.copy( + allFields = mutableAllFields, + requiredFields = requiredFields, + optionalFields = optionalFields, + agreementFields = agreementFields, + ) + } + } + fun register() { analytics.createAccountClickedEvent("") val mapFields = uiState.value.allFields.associate { it.name to it.placeholder } + - mapOf(ApiConstants.HONOR_CODE to true.toString()) + mapOf(ApiConstants.RegistrationFields.HONOR_CODE to true.toString()) val resultMap = mapFields.toMutableMap() uiState.value.allFields.filter { !it.required }.forEach { (k, _) -> if (mapFields[k].isNullOrEmpty()) { @@ -137,13 +171,13 @@ class SignUpViewModel( if (e.isInternetError()) { _uiMessage.emit( UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) + resourceManager.getString(coreR.string.core_error_no_connection) ) ) } else { _uiMessage.emit( UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_unknown_error) + resourceManager.getString(coreR.string.core_error_unknown_error) ) ) } @@ -178,18 +212,18 @@ class SignUpViewModel( runCatching { interactor.loginSocial(socialAuth.accessToken, socialAuth.authType) }.onFailure { + val fields = uiState.value.allFields.toMutableList() + .filter { field -> field.type != RegistrationFieldType.PASSWORD } + updateField(ApiConstants.NAME, socialAuth.name) + updateField(ApiConstants.EMAIL, socialAuth.email) + setErrorInstructions(emptyMap()) _uiState.update { - val fields = it.allFields.toMutableList() - .filter { field -> field.type != RegistrationFieldType.PASSWORD } - updateField(ApiConstants.NAME, socialAuth.name) - updateField(ApiConstants.EMAIL, socialAuth.email) - setErrorInstructions(emptyMap()) it.copy( isLoading = false, socialAuth = socialAuth, - allFields = fields ) } + updateFields(fields) }.onSuccess { setUserId() analytics.userLoginEvent(socialAuth.authType.methodName) @@ -208,12 +242,8 @@ class SignUpViewModel( updatedFields.add(it.copy(errorInstructions = "")) } } - _uiState.update { state -> - state.copy( - allFields = updatedFields, - isLoading = false, - ) - } + updateFields(updatedFields) + _uiState.update { it.copy(isLoading = false) } } private fun collectAppUpgradeEvent() { @@ -231,15 +261,22 @@ class SignUpViewModel( } fun updateField(key: String, value: String) { - _uiState.update { - val updatedFields = uiState.value.allFields.toMutableList().map { field -> - if (field.name == key) { - field.copy(placeholder = value) - } else { - field - } + val updatedFields = uiState.value.allFields.toMutableList().map { field -> + if (field.name == key) { + field.copy(placeholder = value) + } else { + field + } + } + updateFields(updatedFields) + } + + fun openLink(fragmentManager: FragmentManager, links: Map, link: String) { + links.forEach { (key, value) -> + if (value == link) { + router.navigateToWebContent(fragmentManager, key, value) + return } - it.copy(allFields = updatedFields) } } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/compose/SignUpView.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/compose/SignUpView.kt index 0658396e2..2852ab5fe 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/compose/SignUpView.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/compose/SignUpView.kt @@ -97,6 +97,7 @@ internal fun SignUpView( onBackClick: () -> Unit, onFieldUpdated: (String, String) -> Unit, onRegisterClick: (authType: AuthType) -> Unit, + onHyperLinkClick: (Map, String) -> Unit, ) { val scaffoldState = rememberScaffoldState() val focusManager = LocalFocusManager.current @@ -137,9 +138,6 @@ internal fun SignUpView( val isImeVisible by isImeVisibleState() - val fields = uiState.allFields.filter { it.required } - val optionalFields = uiState.allFields.filter { !it.required } - LaunchedEffect(uiState.validationError) { if (uiState.validationError) { coroutine.launch { @@ -294,7 +292,6 @@ internal fun SignUpView( modifier = Modifier .fillMaxHeight() .background(MaterialTheme.appColors.background), - verticalArrangement = Arrangement.spacedBy(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { if (uiState.isLoading) { @@ -350,7 +347,7 @@ internal fun SignUpView( } } RequiredFields( - fields = fields, + fields = uiState.requiredFields, showErrorMap = showErrorMap, selectableNamesMap = selectableNamesMap, onSelectClick = { serverName, field, list -> @@ -369,7 +366,7 @@ internal fun SignUpView( }, onFieldUpdated = onFieldUpdated ) - if (optionalFields.isNotEmpty()) { + if (uiState.optionalFields.isNotEmpty()) { ExpandableText( modifier = Modifier.testTag("txt_optional_field"), isExpanded = showOptionalFields, @@ -377,32 +374,55 @@ internal fun SignUpView( showOptionalFields = !showOptionalFields } ) - Surface(color = MaterialTheme.appColors.background) { - AnimatedVisibility(visible = showOptionalFields) { - OptionalFields( - fields = optionalFields, - showErrorMap = showErrorMap, - selectableNamesMap = selectableNamesMap, - onSelectClick = { serverName, field, list -> - keyboardController?.hide() - serverFieldName.value = - serverName - expandedList = list - coroutine.launch { - if (bottomSheetScaffoldState.isVisible) { - bottomSheetScaffoldState.hide() - } else { - bottomDialogTitle = field.label - showErrorMap[field.name] = false - bottomSheetScaffoldState.show() - } + AnimatedVisibility(visible = showOptionalFields) { + OptionalFields( + fields = uiState.optionalFields, + showErrorMap = showErrorMap, + selectableNamesMap = selectableNamesMap, + onSelectClick = { serverName, field, list -> + keyboardController?.hide() + serverFieldName.value = + serverName + expandedList = list + coroutine.launch { + if (bottomSheetScaffoldState.isVisible) { + bottomSheetScaffoldState.hide() + } else { + bottomDialogTitle = field.label + showErrorMap[field.name] = false + bottomSheetScaffoldState.show() } - }, - onFieldUpdated = onFieldUpdated, - ) - } + } + }, + onFieldUpdated = onFieldUpdated, + ) } } + if (uiState.agreementFields.isNotEmpty()) { + OptionalFields( + fields = uiState.agreementFields, + showErrorMap = showErrorMap, + selectableNamesMap = selectableNamesMap, + onSelectClick = { serverName, field, list -> + keyboardController?.hide() + serverFieldName.value = serverName + expandedList = list + coroutine.launch { + if (bottomSheetScaffoldState.isVisible) { + bottomSheetScaffoldState.hide() + } else { + bottomDialogTitle = field.label + showErrorMap[field.name] = false + bottomSheetScaffoldState.show() + } + } + }, + onFieldUpdated = onFieldUpdated, + hyperLinkAction = { links, link -> + onHyperLinkClick(links, link) + }, + ) + } if (uiState.isButtonLoading) { Box( @@ -460,6 +480,7 @@ private fun RegistrationScreenPreview() { onBackClick = {}, onRegisterClick = {}, onFieldUpdated = { _, _ -> }, + onHyperLinkClick = { _, _ -> }, ) } } @@ -472,12 +493,16 @@ private fun RegistrationScreenTabletPreview() { SignUpView( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), uiState = SignUpUIState( - allFields = listOf(field, field, field.copy(required = false)), + allFields = listOf(field), + requiredFields = listOf(field, field), + optionalFields = listOf(field, field), + agreementFields = listOf(field), ), uiMessage = null, onBackClick = {}, onRegisterClick = {}, onFieldUpdated = { _, _ -> }, + onHyperLinkClick = { _, _ -> }, ) } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt b/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt index a16e77505..4f98ea50c 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt @@ -95,7 +95,9 @@ fun RequiredFields( } RegistrationFieldType.CHECKBOX -> { - //Text("checkbox") + CheckboxField(text = field.label, defaultValue = field.defaultValue) { + onFieldUpdated(field.name, it.toString()) + } } RegistrationFieldType.SELECT -> { @@ -139,6 +141,7 @@ fun OptionalFields( selectableNamesMap: MutableMap, onSelectClick: (String, RegistrationField, List) -> Unit, onFieldUpdated: (String, String) -> Unit, + hyperLinkAction: ((Map, String) -> Unit)? = null, ) { Column { fields.forEach { field -> @@ -167,12 +170,17 @@ fun OptionalFields( HyperlinkText( fullText = linkedText.text, hyperLinks = linkedText.links, - linkTextColor = MaterialTheme.appColors.primary + linkTextColor = MaterialTheme.appColors.primary, + action = { + hyperLinkAction?.invoke(linkedText.links, it) + }, ) } RegistrationFieldType.CHECKBOX -> { - //Text("checkbox") + CheckboxField(text = field.label, defaultValue = field.defaultValue) { + onFieldUpdated(field.name, it.toString()) + } } RegistrationFieldType.SELECT -> { diff --git a/auth/src/main/java/org/openedx/auth/presentation/ui/CheckboxField.kt b/auth/src/main/java/org/openedx/auth/presentation/ui/CheckboxField.kt new file mode 100644 index 000000000..b134cb59a --- /dev/null +++ b/auth/src/main/java/org/openedx/auth/presentation/ui/CheckboxField.kt @@ -0,0 +1,62 @@ +package org.openedx.auth.presentation.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Checkbox +import androidx.compose.material.CheckboxDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import org.openedx.core.ui.noRippleClickable +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appTypography + +@Composable +internal fun CheckboxField( + text: String, + defaultValue: Boolean, + onValueChanged: (Boolean) -> Unit +) { + var checkedState by remember { mutableStateOf(defaultValue) } + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = checkedState, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.appColors.primary, + uncheckedColor = MaterialTheme.appColors.textFieldText + ), + onCheckedChange = { + checkedState = it + onValueChanged(it) + } + ) + Text( + modifier = Modifier.noRippleClickable { + checkedState = !checkedState + onValueChanged(checkedState) + }, + text = text, + style = MaterialTheme.appTypography.bodySmall, + ) + } +} + +@Preview(widthDp = 375, uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(widthDp = 375, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun CheckboxFieldPreview() { + OpenEdXTheme { + CheckboxField( + text = "Test", + defaultValue = true, + ) {} + } +} diff --git a/auth/src/main/res/values/strings.xml b/auth/src/main/res/values/strings.xml index 85eb3a47f..4f8ce12d8 100644 --- a/auth/src/main/res/values/strings.xml +++ b/auth/src/main/res/values/strings.xml @@ -33,4 +33,10 @@ Continue with Microsoft You\'ve successfully signed in with %s. We just need a little more information before you start learning with %s. + End User Licence Agreement + Terms of Service and Honor Code + Privacy Policy + By creating an account, you agree to the %1$s and %2$s and you acknowledge that %3$s and each Member process your personal data in accordance with the %4$s. + By signing in to this app, you agree to the %1$s and %2$s and you acknowledge that %3$s and each Member process your personal data in accordance with the %4$s. + %2$s]]> diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index 6b04b596b..98278be4d 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -23,7 +23,9 @@ import org.junit.Test import org.junit.rules.TestRule import org.openedx.auth.R import org.openedx.auth.domain.interactor.AuthInteractor +import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics +import org.openedx.auth.presentation.AuthRouter import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.UIMessage import org.openedx.core.Validator @@ -33,6 +35,7 @@ import org.openedx.core.config.GoogleConfig import org.openedx.core.config.MicrosoftConfig import org.openedx.core.data.model.User import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.presentation.global.WhatsNewGlobalManager import org.openedx.core.system.EdxError import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.AppUpgradeNotifier @@ -54,7 +57,10 @@ class SignInViewModelTest { private val interactor = mockk() private val analytics = mockk() private val appUpgradeNotifier = mockk() + private val agreementProvider = mockk() private val oAuthHelper = mockk() + private val router = mockk() + private val whatsNewGlobalManager = mockk() private val invalidCredential = "Invalid credentials" private val noInternet = "Slow or no internet connection" @@ -73,6 +79,7 @@ class SignInViewModelTest { every { resourceManager.getString(R.string.auth_invalid_email_username) } returns invalidEmailOrUsername every { resourceManager.getString(R.string.auth_invalid_password) } returns invalidPassword every { appUpgradeNotifier.notifier } returns emptyFlow() + every { agreementProvider.getAgreement(true) } returns null every { config.isPreLoginExperienceEnabled() } returns false every { config.isSocialAuthEnabled() } returns false every { config.getFacebookConfig() } returns FacebookConfig() @@ -98,7 +105,10 @@ class SignInViewModelTest { analytics = analytics, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, + whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", ) @@ -126,7 +136,10 @@ class SignInViewModelTest { analytics = analytics, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, + whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", ) @@ -156,7 +169,10 @@ class SignInViewModelTest { analytics = analytics, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, + whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", ) @@ -185,7 +201,10 @@ class SignInViewModelTest { analytics = analytics, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, + whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", ) @@ -216,7 +235,10 @@ class SignInViewModelTest { analytics = analytics, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, + whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", ) @@ -248,7 +270,10 @@ class SignInViewModelTest { analytics = analytics, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, + whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", ) @@ -281,7 +306,10 @@ class SignInViewModelTest { analytics = analytics, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, + whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", ) @@ -314,7 +342,10 @@ class SignInViewModelTest { analytics = analytics, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, + whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", ) diff --git a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt index f93447bb8..448889b5b 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt @@ -1,6 +1,7 @@ package org.openedx.auth.presentation.signup import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.compose.ui.text.intl.Locale import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -26,7 +27,9 @@ import org.junit.Test import org.junit.rules.TestRule import org.openedx.auth.data.model.ValidationFields import org.openedx.auth.domain.interactor.AuthInteractor +import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics +import org.openedx.auth.presentation.AuthRouter import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.ApiConstants import org.openedx.core.R @@ -37,13 +40,13 @@ import org.openedx.core.config.GoogleConfig import org.openedx.core.config.MicrosoftConfig import org.openedx.core.data.model.User import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.model.AgreementUrls import org.openedx.core.domain.model.RegistrationField import org.openedx.core.domain.model.RegistrationFieldType import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.AppUpgradeNotifier import java.net.UnknownHostException - @ExperimentalCoroutinesApi class SignUpViewModelTest { @@ -57,7 +60,9 @@ class SignUpViewModelTest { private val interactor = mockk() private val analytics = mockk() private val appUpgradeNotifier = mockk() + private val agreementProvider = mockk() private val oAuthHelper = mockk() + private val router = mockk() //region parameters @@ -107,10 +112,13 @@ class SignUpViewModelTest { every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong every { appUpgradeNotifier.notifier } returns emptyFlow() + every { agreementProvider.getAgreement(false) } returns null every { config.isSocialAuthEnabled() } returns false + every { config.getAgreement(Locale.current.language) } returns AgreementUrls() every { config.getFacebookConfig() } returns FacebookConfig() every { config.getGoogleConfig() } returns GoogleConfig() every { config.getMicrosoftConfig() } returns MicrosoftConfig() + every { config.getMicrosoftConfig() } returns MicrosoftConfig() } @After @@ -127,7 +135,9 @@ class SignUpViewModelTest { preferencesManager = preferencesManager, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, courseId = "", infoType = "", ) @@ -168,7 +178,9 @@ class SignUpViewModelTest { preferencesManager = preferencesManager, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, courseId = "", infoType = "", ) @@ -215,7 +227,9 @@ class SignUpViewModelTest { preferencesManager = preferencesManager, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, courseId = "", infoType = "", ) @@ -242,7 +256,6 @@ class SignUpViewModelTest { assertEquals(somethingWrong, (deferred.await() as? UIMessage.SnackBarMessage)?.message) } - @Test fun `success register`() = runTest { val viewModel = SignUpViewModel( @@ -252,7 +265,9 @@ class SignUpViewModelTest { preferencesManager = preferencesManager, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, courseId = "", infoType = "", ) @@ -300,7 +315,9 @@ class SignUpViewModelTest { preferencesManager = preferencesManager, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, courseId = "", infoType = "", ) @@ -312,7 +329,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.getRegistrationFields() } verify(exactly = 1) { appUpgradeNotifier.notifier } - assertTrue(viewModel.uiState.value.isLoading) + assertFalse(viewModel.uiState.value.isLoading) assertEquals(noInternet, (deferred.await() as? UIMessage.SnackBarMessage)?.message) } @@ -325,7 +342,9 @@ class SignUpViewModelTest { preferencesManager = preferencesManager, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, courseId = "", infoType = "", ) @@ -337,7 +356,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.getRegistrationFields() } verify(exactly = 1) { appUpgradeNotifier.notifier } - assertTrue(viewModel.uiState.value.isLoading) + assertFalse(viewModel.uiState.value.isLoading) assertEquals(somethingWrong, (deferred.await() as? UIMessage.SnackBarMessage)?.message) } @@ -350,7 +369,9 @@ class SignUpViewModelTest { preferencesManager = preferencesManager, appUpgradeNotifier = appUpgradeNotifier, oAuthHelper = oAuthHelper, + agreementProvider = agreementProvider, config = config, + router = router, courseId = "", infoType = "", ) diff --git a/core/src/main/java/org/openedx/core/ApiConstants.kt b/core/src/main/java/org/openedx/core/ApiConstants.kt index 558df5434..786d63cc4 100644 --- a/core/src/main/java/org/openedx/core/ApiConstants.kt +++ b/core/src/main/java/org/openedx/core/ApiConstants.kt @@ -20,7 +20,6 @@ object ApiConstants { const val ACCESS_TOKEN = "access_token" const val CLIENT_ID = "client_id" const val EMAIL = "email" - const val HONOR_CODE = "honor_code" const val NAME = "name" const val PASSWORD = "password" const val PROVIDER = "provider" @@ -30,4 +29,9 @@ object ApiConstants { const val AUTH_TYPE_MICROSOFT = "azuread-oauth2" const val COURSE_KEY = "course_key" + + object RegistrationFields { + const val HONOR_CODE = "honor_code" + const val MARKETING_EMAILS = "marketing_emails_opt_in" + } } diff --git a/core/src/main/java/org/openedx/core/config/Config.kt b/core/src/main/java/org/openedx/core/config/Config.kt index cc1f37269..c6fb8f6e2 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -47,6 +47,10 @@ class Config(context: Context) { return getString(FEEDBACK_EMAIL_ADDRESS, "") } + fun getPlatformName(): String { + return getString(PLATFORM_NAME, "") + } + fun getAgreement(locale: String): AgreementUrls { val agreement = getObjectOrNewInstance(AGREEMENT_URLS, AgreementUrlsConfig::class.java).mapToDomain() @@ -168,6 +172,7 @@ class Config(context: Context) { private const val COURSE_BANNER_ENABLED = "COURSE_BANNER_ENABLED" private const val COURSE_TOP_TAB_BAR_ENABLED = "COURSE_TOP_TAB_BAR_ENABLED" private const val COURSE_UNIT_PROGRESS_ENABLED = "COURSE_UNIT_PROGRESS_ENABLED" + private const val PLATFORM_NAME = "PLATFORM_NAME" } enum class ViewType { diff --git a/core/src/main/java/org/openedx/core/domain/model/RegistrationField.kt b/core/src/main/java/org/openedx/core/domain/model/RegistrationField.kt index 20b1af6eb..e4892edb5 100644 --- a/core/src/main/java/org/openedx/core/domain/model/RegistrationField.kt +++ b/core/src/main/java/org/openedx/core/domain/model/RegistrationField.kt @@ -2,6 +2,7 @@ package org.openedx.core.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize +import org.openedx.core.ApiConstants data class RegistrationField( val name: String, @@ -13,7 +14,8 @@ data class RegistrationField( val required: Boolean, val restrictions: Restrictions, val options: List