Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pre-login mobile app exploration #86

Merged
merged 10 commits into from
Dec 4, 2023

Conversation

omerhabib26
Copy link
Contributor

@omerhabib26 omerhabib26 commented Nov 6, 2023

Description
This PR adds support for pre-login exploration.

  • Feature is implemented behind a feature flag preLoginExperienceEnabled
  • User will be able to navigate to Signup, SignIn, DiscoveryCourses & SearchCourses screen
  • Back button is added on required screens for back navigation
  • Navigation from SignIn to Signup is removed
  • Screen Orientation supported

Here is the video of the implemented feature

prelogin-experience.mp4

config.yaml Outdated
Comment on lines 49 to 50
#feature flag for activating pre_login experience
preLoginExperienceEnabled: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the configurations are in SCREAMING_SNAKE_CASE better to adopt.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should decide on the standards first as the above mentioned flags are also written in CAMEL_CASE

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 122 to 131
when {
org.openedx.core.BuildConfig.PRE_LOGIN_EXPERIENCE_ENABLED ->
addFragment(PreAuthFragment())

else -> addFragment(SignInFragment())
}
}

shouldShowWhatsNew() -> addFragment(WhatsNewFragment())
corePreferencesManager.user != null -> addFragment(MainFragment())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, better to wrap all the cases with {} even for the single liners, to make the code format consistent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pending.

Comment on lines 122 to 120
when {
org.openedx.core.BuildConfig.PRE_LOGIN_EXPERIENCE_ENABLED ->
addFragment(PreAuthFragment())

else -> addFragment(SignInFragment())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently only have binary conditions so better to use if-else.

searchValue = textFieldValue,
keyboardActions = {
focusManager.clearFocus()
onSearchClick(textFieldValue.text)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The learner should navigate to CourseSearchFragment directly when the user clicks the search button.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, there is the infinite search progress on CourseSearchFragment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let us decide as there is no decision on how we gonna handle the backstack.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As decided in the meeting current flow is expected.

private fun PreLoginScreen(
onSearchClick: (query: String) -> Unit,
onRegisterClick: () -> Unit,
onLoginClick: () -> Unit,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, onSigninClick is more appropriate here.

Comment on lines 316 to 330
if (BuildConfig.PRE_LOGIN_EXPERIENCE_ENABLED.not()) {
Text(
modifier = Modifier.noRippleClickable {
onRegisterClick()
},
text = stringResource(id = R.string.auth_register),
color = MaterialTheme.appColors.primary,
style = MaterialTheme.appTypography.labelLarge
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, we can revert this to allow the learner to navigate to Registration screen from Signin screen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was part of the ticket scope

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="pre_auth_title" tools:ignore="ExtraTranslation">Courses and Programs from the world\'s best universities in your pocket.</string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P should be in the small letter to match with the production.

Comment on lines 120 to 131
LaunchedEffect(key1 = uiState) {
if (querySearch.isNotEmpty()) {
router.navigateToCourseSearch(
requireActivity().supportFragmentManager, querySearch
)
arguments?.let {
it.putString(ARG_SEARCH_QUERY, "")
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need once the learner directly navigates to search course screen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current flow is good for now.

Comment on lines 134 to 136
LaunchedEffect(key1 = uiState) {
viewModel.search(querySearch)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be the cause of infinite progress.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 7 to 18
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, we should decide coding standards for the repo, to reduce such optimizations and code changes.
thoughts @volodymyr-chekyrta, @k1rill, @HamzaIsrar12

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, better to go with the default studio Formatter | Linter. Open for suggestions.

@volodymyr-chekyrta
Copy link
Contributor

Agree with the feedback provided by @farhan-arshad-dev.
It would be beneficial to address those concerns before moving forward.

contentDescription = null,
modifier = Modifier
.padding(top = 32.dp, bottom = 32.dp)
.width(170.dp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

batter to update with .wrapContentWidth().

painter = painterResource(id = org.openedx.core.R.drawable.core_ic_logo),
contentDescription = null,
modifier = Modifier
.padding(top = 32.dp, bottom = 32.dp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The top space should be 90dp in total, to match with the production.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bottom space should be 20dp in total, to match with the production.

text = stringResource(id = R.string.pre_auth_title),
color = MaterialTheme.appColors.textPrimary,
style = MaterialTheme.appTypography.headlineSmall,
modifier = Modifier.padding(bottom = 32.dp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bottom space should be 40dp in total, to match with the production.

style = MaterialTheme.appTypography.titleMedium,
text = stringResource(id = R.string.pre_auth_search_title),
)
SearchBar(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bottom space should be 10dp in total.

@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SignInScreenPreview() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to add the preview for Landscape|Tablet mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preview added for Tablet Mode, no need for landscape mode.

Copy link
Contributor

@farhan-arshad-dev farhan-arshad-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some concerns for @volodymyr-chekyrta, and some code-related nits, other than that LGTM.

@@ -183,6 +211,21 @@ internal fun DiscoveryScreen(

HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState)

if (BuildConfig.PRE_LOGIN_EXPERIENCE_ENABLED) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also need to check user is not null, otherwise app shows the back button on the Maindashboard screen.

Comment on lines 122 to 131
when {
org.openedx.core.BuildConfig.PRE_LOGIN_EXPERIENCE_ENABLED ->
addFragment(PreAuthFragment())

else -> addFragment(SignInFragment())
}
}

shouldShowWhatsNew() -> addFragment(WhatsNewFragment())
corePreferencesManager.user != null -> addFragment(MainFragment())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pending.

@@ -379,6 +376,10 @@ private fun CourseSearchScreen(
}
}
}
var isPageLoaded = rememberSaveable { true }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace with val

import org.openedx.core.ui.theme.appColors
import org.openedx.core.ui.theme.appTypography

class PreAuthFragment : Fragment() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously we called this screen Logistration, IMO, better to update the package, and class name accordingly.

@volodymyr-chekyrta
Copy link
Contributor

@k1rill @farhan-arshad-dev @HamzaIsrar12,
let's focus on this PR, waiting for your review/approval.

app/src/main/java/org/openedx/app/AppActivity.kt Outdated Show resolved Hide resolved

@Composable
private fun PreLoginScreen(
onSearchClick: (query: String) -> Unit,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's better to use sealed interface here for example:

sealed interface OnClick {
   data class Search(query: String): OnClick
   object Registration: OnClick
....
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, we are not following the mentioned approach.
@volodymyr-chekyrta your thoughts on this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will leave this as nit

@@ -197,7 +203,21 @@ private fun LoginScreen(
uiMessage = uiMessage,
scaffoldState = scaffoldState
)

if (BuildConfig.PRE_LOGIN_EXPERIENCE_ENABLED) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to move config out of UI, for example in the UI state and init it in viewModel

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @k1rill, we can do that but in my opinion, are there any benefits to taking such a long route for a single flag? is there inspiration or reference available?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@omerhabib26 we are trying to build Clean architecture with SOLID principles. UI shouldn't work with logic directly. Config can't be changed runtime at this moment, so we should init it once on the ViewModel init. Also when the new configuration will be merged, we have to update this code, it's better to update it once inside viewModel, instead of searching all entries. Much easier to make this route long while development, than to change it a lot of times while maintaining the code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@k1rill k1rill self-requested a review November 26, 2023 09:25
@Preview(name = "NEXUS_9_Light", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "NEXUS_9_Night", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SignInScreenPreview() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename SignInScreen. nit: Also there are 2 different names on this screen, fragment is called Logistration, and main Composable is called PreLoginScreen

@k1rill k1rill self-requested a review November 26, 2023 09:31
@omerhabib26
Copy link
Contributor Author

@k1rill ready for another pass

k1rill
k1rill previously approved these changes Nov 28, 2023
Copy link
Contributor

@k1rill k1rill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@omerhabib26 good job!

@omerhabib26
Copy link
Contributor Author

@volodymyr-chekyrta the PR is approved now you can merge it. 🚀

@@ -44,7 +45,11 @@ class SignInViewModel(
val appUpgradeEvent: LiveData<AppUpgradeEvent>
get() = _appUpgradeEvent

private val _isLogistrationEnabled = SingleEventLiveData<Boolean>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SingleEventLiveData leads to the app crash, better to update with MutableLiveData.

@@ -110,10 +117,34 @@ class DiscoveryFragment : Fragment() {
requireParentFragment().parentFragmentManager,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following steps lead to the app crash.

  • Open the app user redirect to the Logistration screen.
  • Click on the Explore all courses redirect to the Discover screen.
  • Click on the course card, 💥 app crashed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not the part of the ticket scope but I have fixed it

@@ -46,6 +47,9 @@ class DiscoveryViewModel(
val appUpgradeEvent: LiveData<AppUpgradeEvent>
get() = _appUpgradeEvent

private val _isLogistrationEnabled = SingleEventLiveData<Boolean>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SingleEventLiveData leads to the app crash at some point.

@volodymyr-chekyrta
Copy link
Contributor

@omerhabib26, I found one problem, could you please check why it is crashing?
The video is attached below.
https://github.com/openedx/openedx-app-android/assets/127732735/26e61930-5179-4912-a2ca-d318a02b2774

@volodymyr-chekyrta
Copy link
Contributor

Also, there is a crash when trying to open any course.
java.lang.IllegalStateException: Fragment DiscoveryFragment{a096e35} (d8cc59a3-8b4b-4a42-9f65-50d120962281 id=0x7f0a00b8 tag=DiscoveryFragment) is not a child Fragment, it is directly attached to org.openedx.app.AppActivity@c8a8e38
https://github.com/openedx/openedx-app-android/assets/127732735/6524ad19-a10d-417a-9ba2-a11d1a045879

@volodymyr-chekyrta
Copy link
Contributor

Same problem as in iOS - the user sees an error message when trying to enroll.
Perhaps should redirect the user to log in or hide the "Enroll" button.

Screenshot_20231130_131802

@omerhabib26
Copy link
Contributor Author

Same problem as in iOS - the user sees an error message when trying to enroll. Perhaps should redirect the user to log in or hide the "Enroll" button.

Screenshot_20231130_131802

This is a known behavior and is not a part of the ticket scope.

Copy link
Contributor

@farhan-arshad-dev farhan-arshad-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an improvement, other than that looks good to me.

Comment on lines 69 to 70
val isLogistrationEnabled by viewModel.isLogistrationEnabled.observeAsState(false)
val canShowBackButton = isLogistrationEnabled && corePreferencesManager.user == null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no other use of corePreferencesManager so better to move inside the DiscoveryViewModel.

import org.openedx.core.ui.theme.appColors
import org.openedx.core.ui.theme.appTypography

class Logistration : Fragment() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep consistency with other fragments, it would be great to rename it to LogistrationFragment.
Same to the file name.

@volodymyr-chekyrta
Copy link
Contributor

No other issues; we can merge it after renaming the Logistration file and resolving conflicts.

- Feature is implemented behind a feature flag `preLoginExperienceEnabled`
- User will be able to navigate to Signup, Signin, DiscoveryCourses & SearchCourses screen
- Back button is added on required screens for back navigation
- Navigation from SignIn to Signup is removed

fix: LEARNER-9663
@omerhabib26
Copy link
Contributor Author

@volodymyr-chekyrta @k1rill @farhan-arshad-dev PR is updated according to the new Config architecture. please have a look

Copy link
Contributor

@volodymyr-chekyrta volodymyr-chekyrta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀

Copy link
Contributor

@farhan-arshad-dev farhan-arshad-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some Nits
No need file config.yaml, can be deleted.

After that, I'll approve the PR.

Comment on lines 49 to 50
private val _isLogistrationEnabled = MutableLiveData<Boolean>()
val isLogistrationEnabled: LiveData<Boolean> = _isLogistrationEnabled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to replace this with the getter.

@@ -51,6 +50,10 @@ class Config(context: Context) {
return getBoolean(WHATS_NEW_ENABLED, false)
}

fun isPreLoginExperienceEnabled(): Boolean {
return getBoolean(PRE_LOGIN_EXPERIENCE_ENABLED, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to change the default value with false.

@volodymyr-chekyrta volodymyr-chekyrta merged commit 44f4d2d into openedx:develop Dec 4, 2023
2 checks passed
@omerhabib26 omerhabib26 deleted the omer/LEARNER-9663 branch December 4, 2023 14:02
k1rill pushed a commit to touchapp/openedx-app-android that referenced this pull request Dec 4, 2023
* feat: pre-login mobile app exploration

- Feature is implemented behind a feature flag `preLoginExperienceEnabled`
- User will be able to navigate to Signup, Signin, DiscoveryCourses & SearchCourses screen
- Back button is added on required screens for back navigation
- Navigation from SignIn to Signup is removed

fix: LEARNER-9663

* fix: Address PR comments

* fix: Address PR comments - 2

* fix: Address PR comments

* fix: resolve test cases

* fix: Address PR comments + crash fix

* fix: App crash on Discovery Fragment

* fix: small code improvement

* fix: Resolve conflicts + Update code according to new config architecture

* fix: addressed minor nits
@miankhalid miankhalid linked an issue Dec 12, 2023 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Android] Pre-Login Mobile App Exploration
4 participants