diff --git a/Documentation/APIs_Compatibility.md b/Documentation/APIs_Compatibility.md
new file mode 100644
index 000000000..2867b07df
--- /dev/null
+++ b/Documentation/APIs_Compatibility.md
@@ -0,0 +1,11 @@
+# APIs Compatibility
+
+This documentation offers guidance on a workaround for utilizing mobile APIs with earlier versions of Open edX releases.
+
+In December 2023, the [FC-0031 project](https://github.com/openedx/edx-platform/issues/33304) introduced new APIs, and the Open edX mobile apps were transitioned to utilize them.
+
+If your platform version is older than December 2023, follow the instructions below:
+
+1. Setup the [mobile-api-extensions](https://github.com/raccoongang/mobile-api-extensions) plugin to your platform.
+The Plugin contains extended Open edX APIs for mobile applications.
+2. Roll back the modifications that brought in the new APIs [42f518a](https://github.com/openedx/openedx-app-android/commit/42f518a264d4300c8c2ca349072addd7d16ff91a).
diff --git a/Documentation/ConfigurationManagement.md b/Documentation/ConfigurationManagement.md
new file mode 100644
index 000000000..1aed9a5ab
--- /dev/null
+++ b/Documentation/ConfigurationManagement.md
@@ -0,0 +1,114 @@
+# Configuration Management
+
+This documentation provides a comprehensive solution for integrating and managing configuration files in Open edX Android project.
+
+## Features
+- Parsing config.yaml files
+- Adding essential keys to `AndroidManifest.xml` (e.g. Microsoft keys)
+- Generating Android build config fields.
+- Generating config.json file to use the configuration fields at runtime.
+- Generating google-services.json with Firebase keys.
+
+Inside the `Config.kt`, parsing and populating relevant keys and classes are done, e.g. `AgreementUrlsConfig.kt` and `FirebaseConfig.kt`.
+
+## Getting Started
+
+### Configuration Setup
+
+Edit the `config_settings.yaml` in the `default_config` folder. It should contain data as follows:
+
+```yaml
+config_directory: '{path_to_config_folder}'
+config_mapping:
+ prod: 'prod'
+ stage: 'stage'
+ dev: 'dev'
+# These mappings are configurable, e.g. dev: 'prod_test'
+```
+
+- `config_directory` provides the path of the config directory.
+- `config_mappings` provides mappings that can be utilized to map the Android Build Variant to a defined folder within the config directory, and it will be referenced.
+
+Note: You can specify `config_directory` to any folder outside the repository to store the configs as a separate project.
+
+### Configuration Files
+In the `default_config` folder, select your environment folder: prod, stage, dev or any other you have created.
+Open `config.yaml` and fill in the required fields.
+
+Example:
+
+```yaml
+API_HOST_URL: 'https://mylmsexample.com'
+APPLICATION_ID: 'org.openedx.app'
+ENVIRONMENT_DISPLAY_NAME: 'MyLMSExample'
+FEEDBACK_EMAIL_ADDRESS: 'support@mylmsexample.com'
+OAUTH_CLIENT_ID: 'YOUR_OAUTH_CLIENT_ID'
+
+PLATFORM_NAME: "MyLMS"
+TOKEN_TYPE: "JWT"
+
+FIREBASE:
+ ENABLED: false
+ ANALYTICS_SOURCE: ''
+ CLOUD_MESSAGING_ENABLED: false
+ PROJECT_NUMBER: ''
+ PROJECT_ID: ''
+ APPLICATION_ID: ''
+ API_KEY: ''
+
+MICROSOFT:
+ ENABLED: false
+ CLIENT_ID: 'microsoftClientID'
+```
+
+Also, all envirenment folders contain a `file_mappings.yaml` file that points to the config files to be parsed.
+
+By modifying `file_mappings.yaml`, you can achieve splitting of the base `config.yaml` or add additional configuration files.
+
+Example:
+
+```yaml
+android:
+ files:
+ - auth_client.yaml
+ - config.yaml
+ - feature_flags.yaml
+```
+
+## Available Third-Party Services
+- **Firebase:** Analytics, Crashlytics, Cloud Messaging
+- **Google:** Sign in and Sign up via Google
+- **Microsoft:** Sign in and Sign up via Microsoft
+- **Facebook:** Sign in and Sign up via Facebook
+- **Branch:** Deeplinks
+- **Braze:** Could Messaging
+- **SegmentIO:** Analytics
+
+## Available Feature Flags
+- **PRE_LOGIN_EXPERIENCE_ENABLED:** Enables the pre login courses discovery experience.
+- **WHATS_NEW_ENABLED:** Enables the "What's New" feature to present the latest changes to the user.
+- **SOCIAL_AUTH_ENABLED:** Enables SSO buttons on the SignIn and SignUp screens.
+- **COURSE_NESTED_LIST_ENABLED:** Enables an alternative visual representation for the course structure.
+- **COURSE_BANNER_ENABLED:** Enables the display of the course image on the Course Home screen.
+- **COURSE_TOP_TAB_BAR_ENABLED:** Enables an alternative navigation on the Course Home screen.
+- **COURSE_UNIT_PROGRESS_ENABLED:** Enables the display of the unit progress within the courseware.
+
+## Future Support
+- To add config related to some other service, create a class, e.g. `ServiceNameConfig.kt`, to be able to populate related fields.
+- Create a `function` in the `Config.kt` to be able to parse and use the newly created service from the main Config.
+
+Example:
+
+```Kotlin
+fun getServiceNameConfig(): ServiceNameConfig {
+ return getObjectOrNewInstance(SERVICE_NAME_KEY, ServiceNameConfig::class.java)
+}
+```
+
+```yaml
+SERVICE_NAME:
+ ENABLED: false
+ KEY: ''
+```
+
+The `default_config` directory is added to the project to provide an idea of how to write config YAML files.
diff --git a/README.md b/README.md
index 1d4039253..265d49683 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# EducationX Android
+# Open edX Android
-Modern vision of the mobile application for the Open EdX platform from Raccoon Gang.
+Modern vision of the mobile application for the Open edX platform from Raccoon Gang.
[Documentation](Documentation/Documentation.md)
@@ -14,17 +14,16 @@ Modern vision of the mobile application for the Open EdX platform from Raccoon G
3. Choose ``openedx-app-android``.
-4. Configure the [config.yaml](default_config/dev/config.yaml) with URLs and OAuth credentials for your Open edX instance.
+4. Configure `config_settings.yaml` inside `default_config` and `config.yaml` inside sub direcroties to point to your Open edX configuration. [Configuration Docuementation](./Documentation/ConfigurationManagement.md)
5. Select the build variant ``develop``, ``stage``, or ``prod``.
6. Click the **Run** button.
-## API plugin
+## API
+This project targets on the latest Open edX release and rely on the relevant mobile APIs.
-This project uses custom APIs to improve performance and reduce the number of requests to the server.
-
-You can find the plugin with the API and installation guide [here](https://github.com/raccoongang/mobile-api-extensions).
+If your platform version is older than December 2023, please follow the instructions to use the [API Plugin](./Documentation/APIs_Compatibility.md).
## License
diff --git a/app/.gitignore b/app/.gitignore
index 42afabfd2..2abde4aab 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1 +1,2 @@
-/build
\ No newline at end of file
+/build
+/google-services.json
diff --git a/app/build.gradle b/app/build.gradle
index a0f268eb6..2b0ab4f74 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,14 +1,29 @@
-plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.android'
- id 'kotlin-parcelize'
- id 'kotlin-kapt'
- id 'com.google.firebase.crashlytics'
-}
-
def config = configHelper.fetchConfig()
def appId = config.getOrDefault("APPLICATION_ID", "org.openedx.app")
def platformName = config.getOrDefault("PLATFORM_NAME", "OpenEdx").toLowerCase()
+def firebaseConfig = config.get('FIREBASE')
+def firebaseEnabled = firebaseConfig?.getOrDefault('ENABLED', false)
+
+apply plugin: 'com.android.application'
+apply plugin: 'org.jetbrains.kotlin.android'
+apply plugin: 'kotlin-parcelize'
+apply plugin: 'kotlin-kapt'
+if (firebaseEnabled) {
+ apply plugin: 'com.google.gms.google-services'
+ apply plugin: 'com.google.firebase.crashlytics'
+
+ tasks.register('generateGoogleServicesJson') {
+ configHelper.generateGoogleServicesJson(appId)
+ }
+
+ preBuild.dependsOn(generateGoogleServicesJson)
+} else {
+ tasks.register('removeGoogleServicesJson') {
+ configHelper.removeGoogleServicesJson()
+ }
+
+ preBuild.dependsOn(removeGoogleServicesJson)
+}
android {
compileSdk 34
@@ -31,12 +46,18 @@ android {
productFlavors {
prod {
dimension 'env'
+ setupBranchConfigFields(it)
+ setupFirebaseConfigFields(it)
}
develop {
dimension 'env'
+ setupBranchConfigFields(it)
+ setupFirebaseConfigFields(it)
}
stage {
dimension 'env'
+ setupBranchConfigFields(it)
+ setupFirebaseConfigFields(it)
}
}
@@ -57,8 +78,10 @@ android {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- firebaseCrashlytics {
- mappingFileUploadEnabled false
+ if (firebaseEnabled) {
+ firebaseCrashlytics {
+ mappingFileUploadEnabled false
+ }
}
}
}
@@ -72,6 +95,7 @@ android {
buildFeatures {
viewBinding true
compose true
+ buildConfig true
}
composeOptions {
kotlinCompilerExtensionVersion = "$compose_compiler_version"
@@ -106,6 +130,22 @@ dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1'
+ // Segment Library
+ implementation "com.segment.analytics.kotlin:android:1.14.2"
+ // Segment's Firebase integration
+ implementation 'com.segment.analytics.kotlin.destinations:firebase:1.5.2'
+ // Braze SDK Integration
+ implementation "com.braze:braze-segment-kotlin:1.4.2"
+ implementation "com.braze:android-sdk-ui:30.2.0"
+
+ // Firebase Cloud Messaging Integration for Braze
+ implementation 'com.google.firebase:firebase-messaging-ktx:23.4.1'
+
+ // Branch SDK Integration
+ implementation 'io.branch.sdk.android:library:5.9.0'
+ implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1'
+ implementation "com.android.installreferrer:installreferrer:2.2"
+
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
@@ -115,4 +155,38 @@ dependencies {
testImplementation "io.mockk:mockk-android:$mockk_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
testImplementation "androidx.arch.core:core-testing:$android_arch_version"
-}
\ No newline at end of file
+}
+
+private def setupBranchConfigFields(buildType) {
+ def branchConfig = configHelper.fetchConfig().get("BRANCH")
+ def branchKey = ""
+ def branchUriScheme = ""
+ def branchHost = ""
+ def branchAlternateHost = ""
+
+ if (branchConfig && branchConfig.get("ENABLED")) {
+ branchKey = branchConfig.getOrDefault("KEY", "")
+ branchUriScheme = branchConfig.getOrDefault("URI_SCHEME", "")
+ branchHost = branchConfig.getOrDefault("HOST", "")
+ branchAlternateHost = branchConfig.getOrDefault("ALTERNATE_HOST", "")
+
+ // Validation: Throw exception if any field is empty
+ if (branchKey.isEmpty() || branchUriScheme.isEmpty() || branchHost.isEmpty() ||
+ branchAlternateHost.isEmpty()) {
+ throw new IllegalStateException("One or more Branch configuration fields are empty.")
+ }
+ }
+
+ buildType.resValue "string", "branch_key", branchKey
+ buildType.resValue "string", "branch_uri_scheme", branchUriScheme
+ buildType.resValue "string", "branch_host", branchHost
+ buildType.resValue "string", "branch_alternate_host", branchAlternateHost
+}
+
+private def setupFirebaseConfigFields(buildType) {
+ def firebaseConfig = configHelper.fetchConfig().get('FIREBASE')
+ def firebaseEnabled = firebaseConfig?.getOrDefault('ENABLED', false)
+ def cloudMessagingEnabled = firebaseConfig?.getOrDefault('CLOUD_MESSAGING_ENABLED', false)
+
+ buildType.manifestPlaceholders = [fcmEnabled: firebaseEnabled && cloudMessagingEnabled]
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b5581c340..8020f6b74 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
@@ -12,6 +13,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/openedx/app/AnalyticsManager.kt b/app/src/main/java/org/openedx/app/AnalyticsManager.kt
index 4f152677c..356a23459 100644
--- a/app/src/main/java/org/openedx/app/AnalyticsManager.kt
+++ b/app/src/main/java/org/openedx/app/AnalyticsManager.kt
@@ -1,43 +1,56 @@
package org.openedx.app
import android.content.Context
-import android.os.Bundle
-import androidx.core.os.bundleOf
import org.openedx.app.analytics.Analytics
import org.openedx.app.analytics.FirebaseAnalytics
+import org.openedx.app.analytics.SegmentAnalytics
import org.openedx.auth.presentation.AuthAnalytics
import org.openedx.core.config.Config
+import org.openedx.core.presentation.CoreAnalytics
+import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics
import org.openedx.course.presentation.CourseAnalytics
-import org.openedx.dashboard.presentation.dashboard.DashboardAnalytics
+import org.openedx.dashboard.presentation.DashboardAnalytics
import org.openedx.discovery.presentation.DiscoveryAnalytics
import org.openedx.discussion.presentation.DiscussionAnalytics
import org.openedx.profile.presentation.ProfileAnalytics
+import org.openedx.whatsnew.presentation.WhatsNewAnalytics
class AnalyticsManager(
context: Context,
config: Config,
-) : DashboardAnalytics, AuthAnalytics, AppAnalytics,
- DiscoveryAnalytics, ProfileAnalytics, CourseAnalytics, DiscussionAnalytics {
+) : AppAnalytics, AppReviewAnalytics, AuthAnalytics, CoreAnalytics, CourseAnalytics,
+ DashboardAnalytics, DiscoveryAnalytics, DiscussionAnalytics, ProfileAnalytics,
+ WhatsNewAnalytics {
private val services: ArrayList = arrayListOf()
init {
// Initialise all the analytics libraries here
- if (config.getFirebaseConfig().projectId.isNotEmpty()) {
+ if (config.getFirebaseConfig().enabled) {
addAnalyticsTracker(FirebaseAnalytics(context = context))
}
+ val segmentConfig = config.getSegmentConfig()
+ if (segmentConfig.enabled && segmentConfig.segmentWriteKey.isNotBlank()) {
+ addAnalyticsTracker(SegmentAnalytics(context = context, config = config))
+ }
}
private fun addAnalyticsTracker(analytic: Analytics) {
services.add(analytic)
}
- private fun logEvent(event: Event, params: Bundle = bundleOf()) {
+ private fun logEvent(event: Event, params: Map = mapOf()) {
services.forEach { analytics ->
analytics.logEvent(event.eventName, params)
}
}
+ override fun logEvent(event: String, params: Map) {
+ services.forEach { analytics ->
+ analytics.logEvent(event, params)
+ }
+ }
+
private fun setUserId(userId: Long) {
services.forEach { analytics ->
analytics.logUserId(userId)
@@ -45,76 +58,16 @@ class AnalyticsManager(
}
override fun dashboardCourseClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.DASHBOARD_COURSE_CLICKED,
- bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
- }
-
- override fun userLoginEvent(method: String) {
- logEvent(
- Event.USER_LOGIN,
- bundleOf(
- Key.METHOD.keyName to method
- )
- )
- }
-
- override fun signUpClickedEvent() {
- logEvent(Event.SIGN_UP_CLICKED)
- }
-
- override fun createAccountClickedEvent(provider: String) {
- logEvent(
- Event.CREATE_ACCOUNT_CLICKED,
- bundleOf(Key.PROVIDER.keyName to provider)
- )
- }
-
- override fun registrationSuccessEvent(provider: String) {
- logEvent(
- Event.REGISTRATION_SUCCESS,
- bundleOf(Key.PROVIDER.keyName to provider)
- )
- }
-
- override fun forgotPasswordClickedEvent() {
- logEvent(Event.FORGOT_PASSWORD_CLICKED)
- }
-
- override fun resetPasswordClickedEvent(success: Boolean) {
- logEvent(
- Event.RESET_PASSWORD_CLICKED, bundleOf(
- Key.SUCCESS.keyName to success
- )
- )
+ logEvent(Event.DASHBOARD_COURSE_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ })
}
override fun logoutEvent(force: Boolean) {
- logEvent(
- Event.USER_LOGOUT, bundleOf(
- Key.FORCE.keyName to force
- )
- )
- }
-
- override fun discoveryTabClickedEvent() {
- logEvent(Event.DISCOVERY_TAB_CLICKED)
- }
-
- override fun dashboardTabClickedEvent() {
- logEvent(Event.DASHBOARD_TAB_CLICKED)
- }
-
- override fun programsTabClickedEvent() {
- logEvent(Event.PROGRAMS_TAB_CLICKED)
- }
-
- override fun profileTabClickedEvent() {
- logEvent(Event.PROFILE_TAB_CLICKED)
+ logEvent(Event.USER_LOGOUT, buildMap {
+ put(Key.FORCE.keyName, force)
+ })
}
override fun setUserIdForSession(userId: Long) {
@@ -126,328 +79,120 @@ class AnalyticsManager(
}
override fun discoveryCourseSearchEvent(label: String, coursesCount: Int) {
- logEvent(
- Event.DISCOVERY_COURSE_SEARCH, bundleOf(
- Key.LABEL.keyName to label,
- Key.COURSE_COUNT.keyName to coursesCount
- )
- )
+ logEvent(Event.DISCOVERY_COURSE_SEARCH, buildMap {
+ put(Key.LABEL.keyName, label)
+ put(Key.COURSE_COUNT.keyName, coursesCount)
+ })
}
override fun discoveryCourseClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.DISCOVERY_COURSE_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
- }
-
- override fun profileEditClickedEvent() {
- logEvent(Event.PROFILE_EDIT_CLICKED)
- }
-
- override fun profileEditDoneClickedEvent() {
- logEvent(Event.PROFILE_EDIT_DONE_CLICKED)
- }
-
- override fun profileDeleteAccountClickedEvent() {
- logEvent(Event.PROFILE_DELETE_ACCOUNT_CLICKED)
- }
-
- override fun profileVideoSettingsClickedEvent() {
- logEvent(Event.PROFILE_VIDEO_SETTINGS_CLICKED)
- }
-
- override fun privacyPolicyClickedEvent() {
- logEvent(Event.PRIVACY_POLICY_CLICKED)
- }
-
- override fun termsOfUseClickedEvent() {
- logEvent(Event.TERMS_OF_USE_CLICKED)
- }
-
- override fun cookiePolicyClickedEvent() {
- logEvent(Event.COOKIE_POLICY_CLICKED)
- }
-
- override fun dataSellClickedEvent() {
- logEvent(Event.DATE_SELL_CLICKED)
- }
-
- override fun faqClickedEvent() {
- logEvent(Event.FAQ_CLICKED)
- }
-
- override fun emailSupportClickedEvent() {
- logEvent(Event.EMAIL_SUPPORT_CLICKED)
- }
-
- override fun courseEnrollClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.COURSE_ENROLL_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- )
- )
- }
-
- override fun courseEnrollSuccessEvent(courseId: String, courseName: String) {
- logEvent(
- Event.COURSE_ENROLL_SUCCESS, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- )
- )
- }
-
- override fun viewCourseClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.VIEW_COURSE_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- )
- )
- }
-
- override fun resumeCourseTappedEvent(courseId: String, courseName: String, blockId: String) {
- logEvent(
- Event.RESUME_COURSE_TAPPED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- Key.BLOCK_ID.keyName to blockId
- )
- )
+ logEvent(Event.DISCOVERY_COURSE_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ })
}
override fun sequentialClickedEvent(
- courseId: String,
- courseName: String,
- blockId: String,
- blockName: String
- ) {
- logEvent(
- Event.SEQUENTIAL_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- Key.BLOCK_ID.keyName to blockId,
- Key.BLOCK_NAME.keyName to blockName,
- )
- )
- }
-
- override fun verticalClickedEvent(
- courseId: String,
- courseName: String,
- blockId: String,
- blockName: String
+ courseId: String, courseName: String, blockId: String, blockName: String,
) {
- logEvent(
- Event.VERTICAL_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- Key.BLOCK_ID.keyName to blockId,
- Key.BLOCK_NAME.keyName to blockName,
- )
- )
+ logEvent(Event.SEQUENTIAL_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ put(Key.BLOCK_ID.keyName, blockId)
+ put(Key.BLOCK_NAME.keyName, blockName)
+ })
}
override fun nextBlockClickedEvent(
- courseId: String,
- courseName: String,
- blockId: String,
- blockName: String
+ courseId: String, courseName: String, blockId: String, blockName: String,
) {
- logEvent(
- Event.NEXT_BLOCK_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- Key.BLOCK_ID.keyName to blockId,
- Key.BLOCK_NAME.keyName to blockName,
- )
- )
+ logEvent(Event.NEXT_BLOCK_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ put(Key.BLOCK_ID.keyName, blockId)
+ put(Key.BLOCK_NAME.keyName, blockName)
+ })
}
override fun prevBlockClickedEvent(
- courseId: String,
- courseName: String,
- blockId: String,
- blockName: String
+ courseId: String, courseName: String, blockId: String, blockName: String,
) {
- logEvent(
- Event.PREV_BLOCK_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- Key.BLOCK_ID.keyName to blockId,
- Key.BLOCK_NAME.keyName to blockName,
- )
- )
+ logEvent(Event.PREV_BLOCK_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ put(Key.BLOCK_ID.keyName, blockId)
+ put(Key.BLOCK_NAME.keyName, blockName)
+ })
}
override fun finishVerticalClickedEvent(
- courseId: String,
- courseName: String,
- blockId: String,
- blockName: String
+ courseId: String, courseName: String, blockId: String, blockName: String,
) {
- logEvent(
- Event.FINISH_VERTICAL_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- Key.BLOCK_ID.keyName to blockId,
- Key.BLOCK_NAME.keyName to blockName,
- )
- )
+ logEvent(Event.FINISH_VERTICAL_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ put(Key.BLOCK_ID.keyName, blockId)
+ put(Key.BLOCK_NAME.keyName, blockName)
+ })
}
override fun finishVerticalNextClickedEvent(
- courseId: String,
- courseName: String,
- blockId: String,
- blockName: String
+ courseId: String, courseName: String, blockId: String, blockName: String,
) {
- logEvent(
- Event.FINISH_VERTICAL_NEXT_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- Key.BLOCK_ID.keyName to blockId,
- Key.BLOCK_NAME.keyName to blockName,
- )
- )
+ logEvent(Event.FINISH_VERTICAL_NEXT_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ put(Key.BLOCK_ID.keyName, blockId)
+ put(Key.BLOCK_NAME.keyName, blockName)
+ })
}
override fun finishVerticalBackClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.FINISH_VERTICAL_BACK_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
- }
-
- override fun courseTabClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.COURSE_TAB_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
- }
-
- override fun videoTabClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.VIDEO_TAB_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
- }
-
- override fun discussionTabClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.DISCUSSION_TAB_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
- }
-
- override fun datesTabClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.DATES_TAB_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
- }
-
- override fun handoutsTabClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.HANDOUTS_TAB_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
+ logEvent(Event.FINISH_VERTICAL_BACK_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ })
}
override fun discussionAllPostsClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.DISCUSSION_ALL_POSTS_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
+ logEvent(Event.DISCUSSION_ALL_POSTS_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ })
}
override fun discussionFollowingClickedEvent(courseId: String, courseName: String) {
- logEvent(
- Event.DISCUSSION_FOLLOWING_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName
- )
- )
+ logEvent(Event.DISCUSSION_FOLLOWING_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ })
}
override fun discussionTopicClickedEvent(
- courseId: String,
- courseName: String,
- topicId: String,
- topicName: String
+ courseId: String, courseName: String, topicId: String, topicName: String,
) {
- logEvent(
- Event.DISCUSSION_TOPIC_CLICKED, bundleOf(
- Key.COURSE_ID.keyName to courseId,
- Key.COURSE_NAME.keyName to courseName,
- Key.TOPIC_ID.keyName to topicId,
- Key.TOPIC_NAME.keyName to topicName
- )
- )
+ logEvent(Event.DISCUSSION_TOPIC_CLICKED, buildMap {
+ put(Key.COURSE_ID.keyName, courseId)
+ put(Key.COURSE_NAME.keyName, courseName)
+ put(Key.TOPIC_ID.keyName, topicId)
+ put(Key.TOPIC_NAME.keyName, topicName)
+ })
}
-
}
-private enum class Event(val eventName: String) {
- USER_LOGIN("User_Login"),
- SIGN_UP_CLICKED("Sign_up_Clicked"),
- CREATE_ACCOUNT_CLICKED("Create_Account_Clicked"),
- REGISTRATION_SUCCESS("Registration_Success"),
+enum class Event(val eventName: String) {
USER_LOGOUT("User_Logout"),
- FORGOT_PASSWORD_CLICKED("Forgot_password_Clicked"),
- RESET_PASSWORD_CLICKED("Reset_password_Clicked"),
- DISCOVERY_TAB_CLICKED("Main_Discovery_tab_Clicked"),
- DASHBOARD_TAB_CLICKED("Main_Dashboard_tab_Clicked"),
- PROGRAMS_TAB_CLICKED("Main_Programs_tab_Clicked"),
- PROFILE_TAB_CLICKED("Main_Profile_tab_Clicked"),
DISCOVERY_SEARCH_BAR_CLICKED("Discovery_Search_Bar_Clicked"),
DISCOVERY_COURSE_SEARCH("Discovery_Courses_Search"),
DISCOVERY_COURSE_CLICKED("Discovery_Course_Clicked"),
DASHBOARD_COURSE_CLICKED("Dashboard_Course_Clicked"),
- PROFILE_EDIT_CLICKED("Profile_Edit_Clicked"),
- PROFILE_EDIT_DONE_CLICKED("Profile_Edit_Done_Clicked"),
- PROFILE_DELETE_ACCOUNT_CLICKED("Profile_Delete_Account_Clicked"),
- PROFILE_VIDEO_SETTINGS_CLICKED("Profile_Video_settings_Clicked"),
- PRIVACY_POLICY_CLICKED("Privacy_Policy_Clicked"),
- TERMS_OF_USE_CLICKED("Terms_Of_Use_Clicked"),
- COOKIE_POLICY_CLICKED("Cookie_Policy_Clicked"),
- DATE_SELL_CLICKED("Data_Sell_Clicked"),
- FAQ_CLICKED("FAQ_Clicked"),
- EMAIL_SUPPORT_CLICKED("Email_Support_Clicked"),
- COURSE_ENROLL_CLICKED("Course_Enroll_Clicked"),
- COURSE_ENROLL_SUCCESS("Course_Enroll_Success"),
- VIEW_COURSE_CLICKED("View_Course_Clicked"),
- RESUME_COURSE_TAPPED("Resume_Course_Tapped"),
+
SEQUENTIAL_CLICKED("Sequential_Clicked"),
- VERTICAL_CLICKED("Vertical_Clicked"),
NEXT_BLOCK_CLICKED("Next_Block_Clicked"),
PREV_BLOCK_CLICKED("Prev_Block_Clicked"),
FINISH_VERTICAL_CLICKED("Finish_Vertical_Clicked"),
FINISH_VERTICAL_NEXT_CLICKED("Finish_Vertical_Next_section_Clicked"),
FINISH_VERTICAL_BACK_CLICKED("Finish_Vertical_Back_to_outline_Clicked"),
- COURSE_TAB_CLICKED("Course_Outline_Course_tab_Clicked"),
- VIDEO_TAB_CLICKED("Course_Outline_Videos_tab_Clicked"),
- DISCUSSION_TAB_CLICKED("Course_Outline_Discussion_tab_Clicked"),
- DATES_TAB_CLICKED("Course_Outline_Dates_tab_Clicked"),
- HANDOUTS_TAB_CLICKED("Course_Outline_Handouts_tab_Clicked"),
DISCUSSION_ALL_POSTS_CLICKED("Discussion_All_Posts_Clicked"),
DISCUSSION_FOLLOWING_CLICKED("Discussion_Following_Clicked"),
DISCUSSION_TOPIC_CLICKED("Discussion_Topic_Clicked"),
@@ -460,9 +205,6 @@ private enum class Key(val keyName: String) {
BLOCK_NAME("block_name"),
TOPIC_ID("topic_id"),
TOPIC_NAME("topic_name"),
- METHOD("method"),
- SUCCESS("success"),
- PROVIDER("provider"),
FORCE("force"),
LABEL("label"),
COURSE_COUNT("courses_count"),
diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt
index 8646c7491..5ab0d0b0e 100644
--- a/app/src/main/java/org/openedx/app/AppActivity.kt
+++ b/app/src/main/java/org/openedx/app/AppActivity.kt
@@ -1,5 +1,6 @@
package org.openedx.app
+import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle
@@ -12,6 +13,8 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import androidx.window.layout.WindowMetricsCalculator
+import io.branch.referral.Branch
+import io.branch.referral.Branch.BranchUniversalReferralInitListener
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.openedx.app.databinding.ActivityAppBinding
@@ -23,6 +26,7 @@ import org.openedx.core.presentation.global.InsetHolder
import org.openedx.core.presentation.global.WindowSizeHolder
import org.openedx.core.ui.WindowSize
import org.openedx.core.ui.WindowType
+import org.openedx.core.utils.Logger
import org.openedx.profile.presentation.ProfileRouter
import org.openedx.whatsnew.WhatsNewManager
import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment
@@ -45,6 +49,8 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
private val corePreferencesManager by inject()
private val profileRouter by inject()
+ private val branchLogger = Logger(BRANCH_TAG)
+
private var _insetTop = 0
private var _insetBottom = 0
private var _insetCutout = 0
@@ -63,6 +69,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
installSplashScreen()
binding = ActivityAppBinding.inflate(layoutInflater)
lifecycle.addObserver(viewModel)
+ viewModel.logAppLaunchEvent()
setContentView(binding.root)
val container = binding.rootLayout
@@ -134,6 +141,43 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
}
}
+ override fun onStart() {
+ super.onStart()
+
+ if (viewModel.isBranchEnabled) {
+ val callback = BranchUniversalReferralInitListener { _, linkProperties, error ->
+ if (linkProperties != null) {
+ branchLogger.i { "Branch init complete." }
+ branchLogger.i { linkProperties.controlParams.toString() }
+ } else if (error != null) {
+ branchLogger.e { "Branch init failed. Caused by -" + error.message }
+ }
+ }
+
+ Branch.sessionBuilder(this)
+ .withCallback(callback)
+ .withData(this.intent.data)
+ .init()
+ }
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ this.intent = intent
+
+ if (viewModel.isBranchEnabled) {
+ if (intent?.getBooleanExtra(BRANCH_FORCE_NEW_SESSION, false) == true) {
+ Branch.sessionBuilder(this).withCallback { referringParams, error ->
+ if (error != null) {
+ branchLogger.e { error.message }
+ } else if (referringParams != null) {
+ branchLogger.i { referringParams.toString() }
+ }
+ }.reInit()
+ }
+ }
+ }
+
private fun addFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction()
.add(R.id.container, fragment)
@@ -173,5 +217,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
const val TOP_INSET = "topInset"
const val BOTTOM_INSET = "bottomInset"
const val CUTOUT_INSET = "cutoutInset"
+ const val BRANCH_TAG = "Branch"
+ const val BRANCH_FORCE_NEW_SESSION = "branch_force_new_session"
}
}
diff --git a/app/src/main/java/org/openedx/app/AppAnalytics.kt b/app/src/main/java/org/openedx/app/AppAnalytics.kt
index 1114ccd8e..51278ef13 100644
--- a/app/src/main/java/org/openedx/app/AppAnalytics.kt
+++ b/app/src/main/java/org/openedx/app/AppAnalytics.kt
@@ -2,9 +2,33 @@ package org.openedx.app
interface AppAnalytics {
fun logoutEvent(force: Boolean)
- fun discoveryTabClickedEvent()
- fun dashboardTabClickedEvent()
- fun programsTabClickedEvent()
- fun profileTabClickedEvent()
fun setUserIdForSession(userId: Long)
-}
\ No newline at end of file
+ fun logEvent(event: String, params: Map)
+}
+
+enum class AppAnalyticsEvent(val eventName: String, val biValue: String) {
+ LAUNCH(
+ "Launch",
+ "edx.bi.app.launch"
+ ),
+ DISCOVER(
+ "MainDashboard:Discover",
+ "edx.bi.app.main_dashboard.discover"
+ ),
+ MY_COURSES(
+ "MainDashboard:My Courses",
+ "edx.bi.app.main_dashboard.my_course"
+ ),
+ MY_PROGRAMS(
+ "MainDashboard:My Programs",
+ "edx.bi.app.main_dashboard.my_program"
+ ),
+ PROFILE(
+ "MainDashboard:Profile",
+ "edx.bi.app.main_dashboard.profile"
+ ),
+}
+
+enum class AppAnalyticsKey(val key: String) {
+ NAME("name"),
+}
diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt
index 15f7be5a0..474f4a8e9 100644
--- a/app/src/main/java/org/openedx/app/AppRouter.kt
+++ b/app/src/main/java/org/openedx/app/AppRouter.kt
@@ -13,21 +13,25 @@ import org.openedx.core.presentation.course.CourseViewMode
import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter
import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment
import org.openedx.core.presentation.global.webview.WebContentFragment
+import org.openedx.core.presentation.settings.VideoQualityFragment
+import org.openedx.core.presentation.settings.VideoQualityType
import org.openedx.course.presentation.CourseRouter
import org.openedx.course.presentation.container.CourseContainerFragment
import org.openedx.course.presentation.container.NoAccessCourseContainerFragment
-import org.openedx.course.presentation.detail.CourseDetailsFragment
import org.openedx.course.presentation.handouts.HandoutsType
import org.openedx.course.presentation.handouts.HandoutsWebViewFragment
-import org.openedx.course.presentation.info.CourseInfoFragment
import org.openedx.course.presentation.section.CourseSectionFragment
import org.openedx.course.presentation.unit.container.CourseUnitContainerFragment
import org.openedx.course.presentation.unit.video.VideoFullScreenFragment
import org.openedx.course.presentation.unit.video.YoutubeVideoFullScreenFragment
+import org.openedx.course.settings.download.DownloadQueueFragment
import org.openedx.dashboard.presentation.DashboardRouter
-import org.openedx.dashboard.presentation.program.ProgramFragment
import org.openedx.discovery.presentation.DiscoveryRouter
import org.openedx.discovery.presentation.NativeDiscoveryFragment
+import org.openedx.discovery.presentation.WebViewDiscoveryFragment
+import org.openedx.discovery.presentation.detail.CourseDetailsFragment
+import org.openedx.discovery.presentation.info.CourseInfoFragment
+import org.openedx.discovery.presentation.program.ProgramFragment
import org.openedx.discovery.presentation.search.CourseSearchFragment
import org.openedx.discussion.domain.model.DiscussionComment
import org.openedx.discussion.domain.model.Thread
@@ -43,7 +47,6 @@ import org.openedx.profile.presentation.anothers_account.AnothersProfileFragment
import org.openedx.profile.presentation.delete.DeleteProfileFragment
import org.openedx.profile.presentation.edit.EditProfileFragment
import org.openedx.profile.presentation.profile.ProfileFragment
-import org.openedx.profile.presentation.settings.video.VideoQualityFragment
import org.openedx.profile.presentation.settings.video.VideoSettingsFragment
import org.openedx.whatsnew.WhatsNewRouter
import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment
@@ -52,37 +55,45 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
ProfileRouter, AppUpgradeRouter, WhatsNewRouter {
//region AuthRouter
- override fun navigateToMain(fm: FragmentManager, courseId: String?) {
+ override fun navigateToMain(fm: FragmentManager, courseId: String?, infoType: String?) {
fm.popBackStack()
fm.beginTransaction()
- .replace(R.id.container, MainFragment.newInstance(courseId))
+ .replace(R.id.container, MainFragment.newInstance(courseId, infoType))
.commit()
}
- override fun navigateToSignIn(fm: FragmentManager, courseId: String?) {
- replaceFragmentWithBackStack(fm, SignInFragment.newInstance(courseId))
+ override fun navigateToSignIn(fm: FragmentManager, courseId: String?, infoType: String?) {
+ replaceFragmentWithBackStack(fm, SignInFragment.newInstance(courseId, infoType))
}
- override fun navigateToSignUp(fm: FragmentManager, courseId: String?) {
- replaceFragmentWithBackStack(fm, SignUpFragment.newInstance(courseId))
+ override fun navigateToSignUp(fm: FragmentManager, courseId: String?, infoType: String?) {
+ replaceFragmentWithBackStack(fm, SignUpFragment.newInstance(courseId, infoType))
}
override fun navigateToLogistration(fm: FragmentManager, courseId: String?) {
replaceFragmentWithBackStack(fm, LogistrationFragment.newInstance(courseId))
}
+ override fun navigateToDownloadQueue(fm: FragmentManager, descendants: List) {
+ replaceFragmentWithBackStack(fm, DownloadQueueFragment.newInstance(descendants))
+ }
+
override fun navigateToRestorePassword(fm: FragmentManager) {
replaceFragmentWithBackStack(fm, RestorePasswordFragment())
}
- override fun navigateToDiscoverCourses(fm: FragmentManager, querySearch: String) {
+ override fun navigateToNativeDiscoverCourses(fm: FragmentManager, querySearch: String) {
replaceFragmentWithBackStack(fm, NativeDiscoveryFragment.newInstance(querySearch))
}
- override fun navigateToWhatsNew(fm: FragmentManager, courseId: String?) {
+ override fun navigateToWebDiscoverCourses(fm: FragmentManager, querySearch: String) {
+ replaceFragmentWithBackStack(fm, WebViewDiscoveryFragment.newInstance(querySearch))
+ }
+
+ override fun navigateToWhatsNew(fm: FragmentManager, courseId: String?, infoType: String?) {
fm.popBackStack()
fm.beginTransaction()
- .replace(R.id.container, WhatsNewFragment.newInstance(courseId))
+ .replace(R.id.container, WhatsNewFragment.newInstance(courseId, infoType))
.commit()
}
@@ -123,15 +134,16 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
override fun navigateToCourseOutline(
fm: FragmentManager,
courseId: String,
- courseTitle: String
+ courseTitle: String,
+ enrollmentMode: String,
) {
replaceFragmentWithBackStack(
fm,
- CourseContainerFragment.newInstance(courseId, courseTitle)
+ CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode)
)
}
- override fun navigateToProgramInfo(fm: FragmentManager, pathId: String) {
+ override fun navigateToEnrolledProgramInfo(fm: FragmentManager, pathId: String) {
replaceFragmentWithBackStack(fm, ProgramFragment.newInstance(pathId))
}
@@ -320,8 +332,8 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
replaceFragmentWithBackStack(fm, VideoSettingsFragment())
}
- override fun navigateToVideoQuality(fm: FragmentManager) {
- replaceFragmentWithBackStack(fm, VideoQualityFragment())
+ override fun navigateToVideoQuality(fm: FragmentManager, videoQualityType: VideoQualityType) {
+ replaceFragmentWithBackStack(fm, VideoQualityFragment.newInstance(videoQualityType.name))
}
override fun navigateToDeleteAccount(fm: FragmentManager) {
diff --git a/app/src/main/java/org/openedx/app/AppViewModel.kt b/app/src/main/java/org/openedx/app/AppViewModel.kt
index 9092f603c..1febbd15a 100644
--- a/app/src/main/java/org/openedx/app/AppViewModel.kt
+++ b/app/src/main/java/org/openedx/app/AppViewModel.kt
@@ -20,7 +20,7 @@ class AppViewModel(
private val room: RoomDatabase,
private val preferencesManager: CorePreferences,
private val dispatcher: CoroutineDispatcher,
- private val analytics: AppAnalytics
+ private val analytics: AppAnalytics,
) : BaseViewModel() {
private val _logoutUser = SingleEventLiveData()
@@ -31,6 +31,8 @@ class AppViewModel(
private var logoutHandledAt: Long = 0
+ val isBranchEnabled get() = config.getBranchConfig().enabled
+
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
setUserId()
@@ -49,10 +51,18 @@ class AppViewModel(
}
}
+ fun logAppLaunchEvent() {
+ analytics.logEvent(
+ event = AppAnalyticsEvent.LAUNCH.eventName,
+ params = buildMap {
+ put(AppAnalyticsKey.NAME.key, AppAnalyticsEvent.LAUNCH.biValue)
+ }
+ )
+ }
+
private fun setUserId() {
preferencesManager.user?.let {
analytics.setUserIdForSession(it.id)
}
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt b/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt
index d99fef53b..d8ca717d4 100644
--- a/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt
+++ b/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt
@@ -11,15 +11,20 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.fragment.app.Fragment
import org.openedx.core.ui.theme.appColors
import org.openedx.core.ui.theme.appTypography
class InDevelopmentFragment : Fragment() {
+ @OptIn(ExperimentalComposeUiApi::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -27,7 +32,11 @@ class InDevelopmentFragment : Fragment() {
) = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
- Scaffold {
+ Scaffold(
+ modifier = Modifier.semantics {
+ testTagsAsResourceId = true
+ },
+ ) {
Box(
modifier = Modifier
.fillMaxSize()
@@ -36,6 +45,7 @@ class InDevelopmentFragment : Fragment() {
contentAlignment = Alignment.Center
) {
Text(
+ modifier = Modifier.testTag("txt_in_development"),
text = "Will be available soon",
style = MaterialTheme.appTypography.headlineMedium
)
@@ -43,4 +53,4 @@ class InDevelopmentFragment : Fragment() {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt
index 2021e038f..a798c4a3f 100644
--- a/app/src/main/java/org/openedx/app/MainFragment.kt
+++ b/app/src/main/java/org/openedx/app/MainFragment.kt
@@ -13,20 +13,21 @@ import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.openedx.app.adapter.MainNavigationFragmentAdapter
import org.openedx.app.databinding.FragmentMainBinding
+import org.openedx.core.config.Config
import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment
import org.openedx.core.presentation.global.viewBinding
-import org.openedx.dashboard.presentation.dashboard.DashboardFragment
-import org.openedx.dashboard.presentation.program.ProgramFragment
+import org.openedx.dashboard.presentation.DashboardFragment
import org.openedx.discovery.presentation.DiscoveryNavigator
import org.openedx.discovery.presentation.DiscoveryRouter
+import org.openedx.discovery.presentation.program.ProgramFragment
import org.openedx.profile.presentation.profile.ProfileFragment
class MainFragment : Fragment(R.layout.fragment_main) {
private val binding by viewBinding(FragmentMainBinding::bind)
- private val analytics by inject()
private val viewModel by viewModel()
private val router by inject()
+ private val config by inject()
private lateinit var adapter: MainNavigationFragmentAdapter
@@ -47,27 +48,29 @@ class MainFragment : Fragment(R.layout.fragment_main) {
binding.bottomNavView.setOnItemSelectedListener {
when (it.itemId) {
R.id.fragmentHome -> {
- analytics.discoveryTabClickedEvent()
+ viewModel.logDiscoveryTabClickedEvent()
binding.viewPager.setCurrentItem(0, false)
}
R.id.fragmentDashboard -> {
- analytics.dashboardTabClickedEvent()
+ viewModel.logMyCoursesTabClickedEvent()
binding.viewPager.setCurrentItem(1, false)
}
R.id.fragmentPrograms -> {
- analytics.programsTabClickedEvent()
+ viewModel.logMyProgramsTabClickedEvent()
binding.viewPager.setCurrentItem(2, false)
}
R.id.fragmentProfile -> {
- analytics.profileTabClickedEvent()
+ viewModel.logProfileTabClickedEvent()
binding.viewPager.setCurrentItem(3, false)
}
}
true
}
+ // Trigger click event for the first tab on initial load
+ binding.bottomNavView.selectedItemId = binding.bottomNavView.selectedItemId
viewModel.isBottomBarEnabled.observe(viewLifecycleOwner) { isBottomBarEnabled ->
enableBottomBar(isBottomBarEnabled)
@@ -82,12 +85,19 @@ class MainFragment : Fragment(R.layout.fragment_main) {
}
requireArguments().apply {
- this.getString(ARG_COURSE_ID, null)?.let {
- if (it.isNotBlank()) {
- router.navigateToCourseDetail(parentFragmentManager, it)
+ getString(ARG_COURSE_ID).takeIf { it.isNullOrBlank().not() }?.let { courseId ->
+ val infoType = getString(ARG_INFO_TYPE)
+
+ if (config.getDiscoveryConfig().isViewTypeWebView() && infoType != null) {
+ router.navigateToCourseInfo(parentFragmentManager, courseId, infoType)
+ } else {
+ router.navigateToCourseDetail(parentFragmentManager, courseId)
}
+
+ // Clear arguments after navigation
+ putString(ARG_COURSE_ID, "")
+ putString(ARG_INFO_TYPE, "")
}
- this.putString(ARG_COURSE_ID, null)
}
}
@@ -121,10 +131,12 @@ class MainFragment : Fragment(R.layout.fragment_main) {
companion object {
private const val ARG_COURSE_ID = "courseId"
- fun newInstance(courseId: String? = null): MainFragment {
+ private const val ARG_INFO_TYPE = "info_type"
+ fun newInstance(courseId: String? = null, infoType: String? = null): MainFragment {
val fragment = MainFragment()
fragment.arguments = bundleOf(
- ARG_COURSE_ID to courseId
+ ARG_COURSE_ID to courseId,
+ ARG_INFO_TYPE to infoType
)
return fragment
}
diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt
index 3b36cc2be..6a30533ea 100644
--- a/app/src/main/java/org/openedx/app/MainViewModel.kt
+++ b/app/src/main/java/org/openedx/app/MainViewModel.kt
@@ -12,12 +12,13 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.openedx.core.BaseViewModel
import org.openedx.core.config.Config
-import org.openedx.dashboard.notifier.DashboardEvent
-import org.openedx.dashboard.notifier.DashboardNotifier
+import org.openedx.core.system.notifier.DiscoveryNotifier
+import org.openedx.core.system.notifier.NavigationToDiscovery
class MainViewModel(
private val config: Config,
- private val notifier: DashboardNotifier,
+ private val notifier: DiscoveryNotifier,
+ private val analytics: AppAnalytics,
) : BaseViewModel() {
private val _isBottomBarEnabled = MutableLiveData(true)
@@ -35,7 +36,7 @@ class MainViewModel(
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
notifier.notifier.onEach {
- if (it is DashboardEvent.NavigationToDiscovery) {
+ if (it is NavigationToDiscovery) {
_navigateToDiscovery.emit(true)
}
}.distinctUntilChanged().launchIn(viewModelScope)
@@ -44,4 +45,28 @@ class MainViewModel(
fun enableBottomBar(enable: Boolean) {
_isBottomBarEnabled.value = enable
}
+
+ fun logDiscoveryTabClickedEvent() {
+ logEvent(AppAnalyticsEvent.DISCOVER)
+ }
+
+ fun logMyCoursesTabClickedEvent() {
+ logEvent(AppAnalyticsEvent.MY_COURSES)
+ }
+
+ fun logMyProgramsTabClickedEvent() {
+ logEvent(AppAnalyticsEvent.MY_PROGRAMS)
+ }
+
+ fun logProfileTabClickedEvent() {
+ logEvent(AppAnalyticsEvent.PROFILE)
+ }
+
+ private fun logEvent(event: AppAnalyticsEvent) {
+ analytics.logEvent(event.eventName,
+ buildMap {
+ put(AppAnalyticsKey.NAME.key, event.biValue)
+ }
+ )
+ }
}
diff --git a/app/src/main/java/org/openedx/app/OpenEdXApp.kt b/app/src/main/java/org/openedx/app/OpenEdXApp.kt
index 9f1f95977..7d1b81d32 100644
--- a/app/src/main/java/org/openedx/app/OpenEdXApp.kt
+++ b/app/src/main/java/org/openedx/app/OpenEdXApp.kt
@@ -1,9 +1,10 @@
package org.openedx.app
import android.app.Application
-import com.google.firebase.FirebaseOptions
-import com.google.firebase.ktx.Firebase
-import com.google.firebase.ktx.initialize
+import com.braze.Braze
+import com.braze.configuration.BrazeConfig
+import com.google.firebase.FirebaseApp
+import io.branch.referral.Branch
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
@@ -26,16 +27,29 @@ class OpenEdXApp : Application() {
screenModule
)
}
- val firebaseConfig = config.getFirebaseConfig()
- if (firebaseConfig.enabled) {
- val options = FirebaseOptions.Builder()
- .setProjectId(firebaseConfig.projectId)
- .setApplicationId(firebaseConfig.applicationId)
- .setApiKey(firebaseConfig.apiKey)
- .setGcmSenderId(firebaseConfig.gcmSenderId)
+ if (config.getFirebaseConfig().enabled) {
+ FirebaseApp.initializeApp(this)
+ }
+
+ if (config.getBranchConfig().enabled) {
+ if (BuildConfig.DEBUG) {
+ Branch.enableTestMode()
+ Branch.enableLogging()
+ }
+ Branch.getAutoInstance(this)
+ }
+
+ if (config.getBrazeConfig().isEnabled && config.getFirebaseConfig().enabled) {
+ val isCloudMessagingEnabled = config.getFirebaseConfig().isCloudMessagingEnabled &&
+ config.getBrazeConfig().isPushNotificationsEnabled
+
+ val brazeConfig = BrazeConfig.Builder()
+ .setIsFirebaseCloudMessagingRegistrationEnabled(isCloudMessagingEnabled)
+ .setFirebaseCloudMessagingSenderIdKey(config.getFirebaseConfig().projectNumber)
+ .setHandlePushDeepLinksAutomatically(true)
+ .setIsFirebaseMessagingServiceOnNewTokenRegistrationEnabled(true)
.build()
- Firebase.initialize(this, options)
+ Braze.configure(this, brazeConfig)
}
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/openedx/app/analytics/Analytics.kt b/app/src/main/java/org/openedx/app/analytics/Analytics.kt
index ed34ec41a..01ac01860 100644
--- a/app/src/main/java/org/openedx/app/analytics/Analytics.kt
+++ b/app/src/main/java/org/openedx/app/analytics/Analytics.kt
@@ -1,9 +1,7 @@
package org.openedx.app.analytics
-import android.os.Bundle
-
interface Analytics {
- fun logScreenEvent(screenName: String, bundle: Bundle)
- fun logEvent(eventName: String, bundle: Bundle)
+ fun logScreenEvent(screenName: String, params: Map)
+ fun logEvent(eventName: String, params: Map)
fun logUserId(userId: Long)
}
diff --git a/app/src/main/java/org/openedx/app/analytics/FirebaseAnalytics.kt b/app/src/main/java/org/openedx/app/analytics/FirebaseAnalytics.kt
index 6e4db40a0..503f3d1ef 100644
--- a/app/src/main/java/org/openedx/app/analytics/FirebaseAnalytics.kt
+++ b/app/src/main/java/org/openedx/app/analytics/FirebaseAnalytics.kt
@@ -1,30 +1,35 @@
package org.openedx.app.analytics
import android.content.Context
-import android.os.Bundle
-import android.util.Log
import com.google.firebase.analytics.FirebaseAnalytics
+import org.openedx.core.extension.toBundle
+import org.openedx.core.utils.Logger
class FirebaseAnalytics(context: Context) : Analytics {
- private var tracker: FirebaseAnalytics? = null
+ private val logger = Logger(TAG)
+ private var tracker: FirebaseAnalytics
init {
tracker = FirebaseAnalytics.getInstance(context)
- Log.d("Analytics", "Firebase Builder Initialised")
+ logger.d { "Firebase Analytics Builder Initialised" }
}
- override fun logScreenEvent(screenName: String, bundle: Bundle) {
- Log.d("Analytics", "Firebase log Screen Event: $screenName + $bundle")
+ override fun logScreenEvent(screenName: String, params: Map) {
+ logger.d { "Firebase Analytics log Screen Event: $screenName + $params" }
}
- override fun logEvent(eventName: String, bundle: Bundle) {
- tracker?.logEvent(eventName, bundle)
- Log.d("Analytics", "Firebase log Event $eventName: $bundle")
+ override fun logEvent(eventName: String, params: Map) {
+ tracker.logEvent(eventName, params.toBundle())
+ logger.d { "Firebase Analytics log Event $eventName: $params" }
}
override fun logUserId(userId: Long) {
- tracker?.setUserId(userId.toString())
- Log.d("Analytics", "Firebase User Id log Event")
+ tracker.setUserId(userId.toString())
+ logger.d { "Firebase Analytics User Id log Event" }
+ }
+
+ private companion object {
+ const val TAG = "FirebaseAnalytics"
}
}
diff --git a/app/src/main/java/org/openedx/app/analytics/SegmentAnalytics.kt b/app/src/main/java/org/openedx/app/analytics/SegmentAnalytics.kt
new file mode 100644
index 000000000..3a9532a71
--- /dev/null
+++ b/app/src/main/java/org/openedx/app/analytics/SegmentAnalytics.kt
@@ -0,0 +1,56 @@
+package org.openedx.app.analytics
+
+import android.content.Context
+import com.segment.analytics.kotlin.destinations.braze.BrazeDestination
+import com.segment.analytics.kotlin.destinations.firebase.FirebaseDestination
+import org.openedx.app.BuildConfig
+import org.openedx.core.config.Config
+import org.openedx.core.utils.Logger
+import com.segment.analytics.kotlin.android.Analytics as SegmentAnalyticsBuilder
+import com.segment.analytics.kotlin.core.Analytics as SegmentTracker
+
+class SegmentAnalytics(context: Context, config: Config) : Analytics {
+
+ private val logger = Logger(TAG)
+ private var tracker: SegmentTracker
+
+ init {
+ // Create an analytics client with the given application context and Segment write key.
+ tracker = SegmentAnalyticsBuilder(config.getSegmentConfig().segmentWriteKey, context) {
+ // Automatically track Lifecycle events
+ trackApplicationLifecycleEvents = true
+ flushAt = 20
+ flushInterval = 30
+ }
+ if (config.getFirebaseConfig().isSegmentAnalyticsSource()) {
+ tracker.add(plugin = FirebaseDestination(context = context))
+ }
+
+ if (config.getFirebaseConfig()
+ .isSegmentAnalyticsSource() && config.getBrazeConfig().isEnabled
+ ) {
+ tracker.add(plugin = BrazeDestination(context))
+ }
+ SegmentTracker.debugLogsEnabled = BuildConfig.DEBUG
+ logger.d { "Segment Analytics Builder Initialised" }
+ }
+
+ override fun logScreenEvent(screenName: String, params: Map) {
+ logger.d { "Segment Analytics log Screen Event: $screenName + $params" }
+ tracker.screen(screenName, params)
+ }
+
+ override fun logEvent(eventName: String, params: Map) {
+ logger.d { "Segment Analytics log Event $eventName: $params" }
+ tracker.track(eventName, params)
+ }
+
+ override fun logUserId(userId: Long) {
+ logger.d { "Segment Analytics User Id log Event: $userId" }
+ tracker.identify(userId.toString())
+ }
+
+ private companion object {
+ const val TAG = "SegmentAnalytics"
+ }
+}
diff --git a/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt b/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt
index 7b1e203e2..c91b27184 100644
--- a/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt
+++ b/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt
@@ -24,8 +24,11 @@ class HeadersInterceptor(
}
addHeader("Accept", "application/json")
+
+ val httpAgent = System.getProperty("http.agent") ?: ""
addHeader(
- "User-Agent", System.getProperty("http.agent") + " " +
+ "User-Agent",
+ httpAgent + " " +
context.getString(org.openedx.core.R.string.app_name) + "/" +
BuildConfig.APPLICATION_ID + "/" +
BuildConfig.VERSION_NAME
diff --git a/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt b/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt
index f17677f19..3cc6b82ae 100644
--- a/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt
+++ b/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt
@@ -68,10 +68,12 @@ class OauthRefreshTokenAuthenticator(
return null
}
- val errorCode = getErrorCode(response.peekBody(200).string())
+ val errorCode = getErrorCode(response.peekBody(Long.MAX_VALUE).string())
if (errorCode != null) {
when (errorCode) {
- TOKEN_EXPIRED_ERROR_MESSAGE, JWT_TOKEN_EXPIRED -> {
+ TOKEN_EXPIRED_ERROR_MESSAGE,
+ JWT_TOKEN_EXPIRED,
+ -> {
try {
val newAuth = refreshAccessToken(refreshToken)
if (newAuth != null) {
@@ -98,7 +100,10 @@ class OauthRefreshTokenAuthenticator(
}
}
- TOKEN_NONEXISTENT_ERROR_MESSAGE, TOKEN_INVALID_GRANT_ERROR_MESSAGE, JWT_INVALID_TOKEN -> {
+ TOKEN_NONEXISTENT_ERROR_MESSAGE,
+ TOKEN_INVALID_GRANT_ERROR_MESSAGE,
+ JWT_INVALID_TOKEN,
+ -> {
// Retry request with the current access_token if the original access_token used in
// request does not match the current access_token. This case can occur when
// asynchronous calls are made and are attempting to refresh the access_token where
@@ -118,7 +123,10 @@ class OauthRefreshTokenAuthenticator(
}
}
- DISABLED_USER_ERROR_MESSAGE, JWT_DISABLED_USER_ERROR_MESSAGE -> {
+ DISABLED_USER_ERROR_MESSAGE,
+ JWT_DISABLED_USER_ERROR_MESSAGE,
+ JWT_USER_EMAIL_MISMATCH,
+ -> {
runBlocking {
appNotifier.send(LogoutEvent())
}
@@ -241,6 +249,8 @@ class OauthRefreshTokenAuthenticator(
private const val JWT_TOKEN_EXPIRED = "Token has expired."
private const val JWT_INVALID_TOKEN = "Invalid token."
private const val JWT_DISABLED_USER_ERROR_MESSAGE = "User account is disabled."
+ private const val JWT_USER_EMAIL_MISMATCH =
+ "Failing JWT authentication due to jwt user email mismatch with lms user email."
private const val FIELD_ERROR_CODE = "error_code"
private const val FIELD_DETAIL = "detail"
diff --git a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt
index bd7eb17e5..603876d54 100644
--- a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt
+++ b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt
@@ -6,13 +6,17 @@ import org.openedx.app.BuildConfig
import org.openedx.core.data.model.User
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.data.storage.InAppReviewPreferences
+import org.openedx.core.domain.model.AppConfig
+import org.openedx.core.domain.model.VideoQuality
import org.openedx.core.domain.model.VideoSettings
+import org.openedx.core.extension.replaceSpace
+import org.openedx.course.data.storage.CoursePreferences
import org.openedx.profile.data.model.Account
import org.openedx.profile.data.storage.ProfilePreferences
import org.openedx.whatsnew.data.storage.WhatsNewPreferences
class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences,
- WhatsNewPreferences, InAppReviewPreferences {
+ WhatsNewPreferences, InAppReviewPreferences, CoursePreferences {
private val sharedPreferences =
context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)
@@ -23,7 +27,9 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences
}.apply()
}
- private fun getString(key: String): String = sharedPreferences.getString(key, "") ?: ""
+ private fun getString(key: String, defValue: String = ""): String {
+ return sharedPreferences.getString(key, defValue) ?: defValue
+ }
private fun saveLong(key: String, value: Long) {
sharedPreferences.edit().apply {
@@ -39,7 +45,9 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences
}.apply()
}
- private fun getBoolean(key: String): Boolean = sharedPreferences.getBoolean(key, false)
+ private fun getBoolean(key: String, defValue: Boolean = false): Boolean {
+ return sharedPreferences.getBoolean(key, defValue)
+ }
override fun clear() {
sharedPreferences.edit().apply {
@@ -90,13 +98,32 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences
override var videoSettings: VideoSettings
set(value) {
- val videoSettingsJson = Gson().toJson(value)
- saveString(VIDEO_SETTINGS, videoSettingsJson)
+ saveBoolean(VIDEO_SETTINGS_WIFI_DOWNLOAD_ONLY, value.wifiDownloadOnly)
+ saveString(VIDEO_SETTINGS_STREAMING_QUALITY, value.videoStreamingQuality.name)
+ saveString(VIDEO_SETTINGS_DOWNLOAD_QUALITY, value.videoDownloadQuality.name)
+ }
+ get() {
+ val wifiDownloadOnly = getBoolean(VIDEO_SETTINGS_WIFI_DOWNLOAD_ONLY, defValue = true)
+ val streamingQualityString =
+ getString(VIDEO_SETTINGS_STREAMING_QUALITY, defValue = VideoQuality.AUTO.name)
+ val downloadQualityString =
+ getString(VIDEO_SETTINGS_DOWNLOAD_QUALITY, defValue = VideoQuality.AUTO.name)
+
+ return VideoSettings(
+ wifiDownloadOnly = wifiDownloadOnly,
+ videoStreamingQuality = VideoQuality.valueOf(streamingQualityString),
+ videoDownloadQuality = VideoQuality.valueOf(downloadQualityString)
+ )
+ }
+
+ override var appConfig: AppConfig
+ set(value) {
+ val appConfigJson = Gson().toJson(value)
+ saveString(APP_CONFIG, appConfigJson)
}
get() {
- val videoSettingsString = getString(VIDEO_SETTINGS)
- return Gson().fromJson(videoSettingsString, VideoSettings::class.java)
- ?: VideoSettings.default
+ val appConfigString = getString(APP_CONFIG)
+ return Gson().fromJson(appConfigString, AppConfig::class.java)
}
override var lastWhatsNewVersion: String
@@ -119,22 +146,31 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences
?: InAppReviewPreferences.VersionName.default
}
-
override var wasPositiveRated: Boolean
set(value) {
saveBoolean(APP_WAS_POSITIVE_RATED, value)
}
get() = getBoolean(APP_WAS_POSITIVE_RATED)
+ override fun setCalendarSyncEventsDialogShown(courseName: String) {
+ saveBoolean(courseName.replaceSpace("_"), true)
+ }
+
+ override fun isCalendarSyncEventsDialogShown(courseName: String): Boolean =
+ getBoolean(courseName.replaceSpace("_"))
+
companion object {
private const val ACCESS_TOKEN = "access_token"
private const val REFRESH_TOKEN = "refresh_token"
private const val EXPIRES_IN = "expires_in"
private const val USER = "user"
private const val ACCOUNT = "account"
- private const val VIDEO_SETTINGS = "video_settings"
private const val LAST_WHATS_NEW_VERSION = "last_whats_new_version"
private const val LAST_REVIEW_VERSION = "last_review_version"
private const val APP_WAS_POSITIVE_RATED = "app_was_positive_rated"
+ private const val VIDEO_SETTINGS_WIFI_DOWNLOAD_ONLY = "video_settings_wifi_download_only"
+ private const val VIDEO_SETTINGS_STREAMING_QUALITY = "video_settings_streaming_quality"
+ private const val VIDEO_SETTINGS_DOWNLOAD_QUALITY = "video_settings_download_quality"
+ private const val APP_CONFIG = "app_config"
}
}
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 c5a267ece..dc0a70335 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
@@ -25,12 +26,14 @@ import org.openedx.auth.presentation.sso.GoogleAuthHelper
import org.openedx.auth.presentation.sso.MicrosoftAuthHelper
import org.openedx.auth.presentation.sso.OAuthHelper
import org.openedx.core.config.Config
+import org.openedx.core.data.model.CourseEnrollments
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.data.storage.InAppReviewPreferences
-import org.openedx.core.interfaces.EnrollInCourseInteractor
import org.openedx.core.module.DownloadWorkerController
import org.openedx.core.module.TranscriptManager
import org.openedx.core.module.download.FileDownloader
+import org.openedx.core.presentation.CoreAnalytics
+import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics
import org.openedx.core.presentation.dialog.appreview.AppReviewManager
import org.openedx.core.presentation.global.AppData
import org.openedx.core.presentation.global.WhatsNewGlobalManager
@@ -40,11 +43,14 @@ import org.openedx.core.system.ResourceManager
import org.openedx.core.system.connection.NetworkConnection
import org.openedx.core.system.notifier.AppUpgradeNotifier
import org.openedx.core.system.notifier.CourseNotifier
-import org.openedx.course.domain.interactor.CourseInteractor
-import org.openedx.dashboard.notifier.DashboardNotifier
+import org.openedx.core.system.notifier.DiscoveryNotifier
+import org.openedx.core.system.notifier.DownloadNotifier
+import org.openedx.core.system.notifier.VideoNotifier
+import org.openedx.course.data.storage.CoursePreferences
import org.openedx.course.presentation.CourseAnalytics
import org.openedx.course.presentation.CourseRouter
-import org.openedx.dashboard.presentation.dashboard.DashboardAnalytics
+import org.openedx.course.presentation.calendarsync.CalendarManager
+import org.openedx.dashboard.presentation.DashboardAnalytics
import org.openedx.dashboard.presentation.DashboardRouter
import org.openedx.discovery.presentation.DiscoveryAnalytics
import org.openedx.discovery.presentation.DiscoveryRouter
@@ -58,6 +64,7 @@ import org.openedx.profile.system.notifier.ProfileNotifier
import org.openedx.whatsnew.WhatsNewManager
import org.openedx.whatsnew.WhatsNewRouter
import org.openedx.whatsnew.data.storage.WhatsNewPreferences
+import org.openedx.whatsnew.presentation.WhatsNewAnalytics
val appModule = module {
@@ -67,19 +74,27 @@ val appModule = module {
single { get() }
single { get() }
single { get() }
+ single { get() }
single { ResourceManager(get()) }
single { AppCookieManager(get(), get()) }
single { ReviewManagerFactory.create(get()) }
+ single { CalendarManager(get(), get(), get()) }
- single { GsonBuilder().create() }
+ single {
+ GsonBuilder()
+ .registerTypeAdapter(CourseEnrollments::class.java, CourseEnrollments.Deserializer())
+ .create()
+ }
single { AppNotifier() }
single { CourseNotifier() }
single { DiscussionNotifier() }
single { ProfileNotifier() }
single { AppUpgradeNotifier() }
- single { DashboardNotifier() }
+ single { DownloadNotifier() }
+ single { VideoNotifier() }
+ single { DiscoveryNotifier() }
single { AppRouter() }
single { get() }
@@ -147,18 +162,20 @@ val appModule = module {
single { get() }
single { AnalyticsManager(get(), get()) }
- single { get() }
- single { get() }
single { get() }
- single { get() }
- single { get() }
+ single { get() }
+ single { get() }
+ single { get() }
single { get() }
+ single { get() }
+ single { get() }
single { get() }
+ single { get() }
+ single { get() }
+ factory { AgreementProvider(get(), get()) }
factory { FacebookAuthHelper() }
factory { GoogleAuthHelper(get()) }
factory { MicrosoftAuthHelper() }
factory { OAuthHelper(get(), get(), get()) }
-
- factory { CourseInteractor(get()) }
}
diff --git a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt
index b74deefbb..c281d0465 100644
--- a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt
+++ b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt
@@ -12,6 +12,7 @@ import org.openedx.core.BuildConfig
import org.openedx.core.config.Config
import org.openedx.core.data.api.CookiesApi
import org.openedx.core.data.api.CourseApi
+import org.openedx.discovery.data.api.DiscoveryApi
import org.openedx.discussion.data.api.DiscussionApi
import org.openedx.profile.data.api.ProfileApi
import retrofit2.Retrofit
@@ -51,6 +52,7 @@ val networkingModule = module {
single { provideApi(get()) }
single { provideApi(get()) }
single { provideApi(get()) }
+ single { provideApi(get()) }
}
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 9ccac7be7..b4547a583 100644
--- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt
+++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt
@@ -7,34 +7,38 @@ import org.openedx.app.AppViewModel
import org.openedx.app.MainViewModel
import org.openedx.auth.data.repository.AuthRepository
import org.openedx.auth.domain.interactor.AuthInteractor
+import org.openedx.auth.presentation.logistration.LogistrationViewModel
import org.openedx.auth.presentation.restore.RestorePasswordViewModel
import org.openedx.auth.presentation.signin.SignInViewModel
import org.openedx.auth.presentation.signup.SignUpViewModel
import org.openedx.core.Validator
import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectDialogViewModel
+import org.openedx.core.presentation.settings.VideoQualityViewModel
import org.openedx.course.data.repository.CourseRepository
import org.openedx.course.domain.interactor.CourseInteractor
import org.openedx.course.presentation.container.CourseContainerViewModel
import org.openedx.course.presentation.dates.CourseDatesViewModel
-import org.openedx.course.presentation.detail.CourseDetailsViewModel
import org.openedx.course.presentation.handouts.HandoutsViewModel
-import org.openedx.course.presentation.info.CourseInfoViewModel
import org.openedx.course.presentation.outline.CourseOutlineViewModel
import org.openedx.course.presentation.section.CourseSectionViewModel
import org.openedx.course.presentation.unit.container.CourseUnitContainerViewModel
import org.openedx.course.presentation.unit.html.HtmlUnitViewModel
+import org.openedx.course.presentation.unit.video.BaseVideoViewModel
import org.openedx.course.presentation.unit.video.EncodedVideoUnitViewModel
import org.openedx.course.presentation.unit.video.VideoUnitViewModel
import org.openedx.course.presentation.unit.video.VideoViewModel
import org.openedx.course.presentation.videos.CourseVideoViewModel
+import org.openedx.course.settings.download.DownloadQueueViewModel
import org.openedx.dashboard.data.repository.DashboardRepository
import org.openedx.dashboard.domain.interactor.DashboardInteractor
-import org.openedx.dashboard.presentation.dashboard.DashboardViewModel
-import org.openedx.dashboard.presentation.program.ProgramViewModel
+import org.openedx.dashboard.presentation.DashboardViewModel
import org.openedx.discovery.data.repository.DiscoveryRepository
import org.openedx.discovery.domain.interactor.DiscoveryInteractor
import org.openedx.discovery.presentation.NativeDiscoveryViewModel
import org.openedx.discovery.presentation.WebViewDiscoveryViewModel
+import org.openedx.discovery.presentation.detail.CourseDetailsViewModel
+import org.openedx.discovery.presentation.info.CourseInfoViewModel
+import org.openedx.discovery.presentation.program.ProgramViewModel
import org.openedx.discovery.presentation.search.CourseSearchViewModel
import org.openedx.discussion.data.repository.DiscussionRepository
import org.openedx.discussion.domain.interactor.DiscussionInteractor
@@ -52,19 +56,28 @@ import org.openedx.profile.presentation.anothers_account.AnothersProfileViewMode
import org.openedx.profile.presentation.delete.DeleteProfileViewModel
import org.openedx.profile.presentation.edit.EditProfileViewModel
import org.openedx.profile.presentation.profile.ProfileViewModel
-import org.openedx.profile.presentation.settings.video.VideoQualityViewModel
import org.openedx.profile.presentation.settings.video.VideoSettingsViewModel
import org.openedx.whatsnew.presentation.whatsnew.WhatsNewViewModel
val screenModule = module {
viewModel { AppViewModel(get(), get(), get(), get(), get(named("IODispatcher")), get()) }
- viewModel { MainViewModel(get(), get()) }
+ viewModel { MainViewModel(get(), get(), get()) }
factory { AuthRepository(get(), get(), get()) }
factory { AuthInteractor(get()) }
factory { Validator() }
- viewModel { (courseId: String?) ->
+
+ viewModel { (courseId: String) ->
+ LogistrationViewModel(
+ courseId,
+ get(),
+ get(),
+ get(),
+ )
+ }
+
+ viewModel { (courseId: String?, infoType: String?) ->
SignInViewModel(
get(),
get(),
@@ -74,22 +87,48 @@ val screenModule = module {
get(),
get(),
get(),
+ get(),
+ get(),
+ get(),
courseId,
+ infoType,
)
}
- viewModel { (courseId: String?) ->
- SignUpViewModel(get(), get(), get(), get(), get(), get(), get(), courseId)
+
+ viewModel { (courseId: String?, infoType: String?) ->
+ SignUpViewModel(
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ courseId,
+ infoType
+ )
}
viewModel { RestorePasswordViewModel(get(), get(), get(), get()) }
factory { DashboardRepository(get(), get(), get()) }
factory { DashboardInteractor(get()) }
- viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get(), get()) }
+ viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) }
- factory { DiscoveryRepository(get(), get()) }
+ factory { DiscoveryRepository(get(), get(), get()) }
factory { DiscoveryInteractor(get()) }
viewModel { NativeDiscoveryViewModel(get(), get(), get(), get(), get(), get(), get()) }
- viewModel { WebViewDiscoveryViewModel(get(), get(), get()) }
+ viewModel { (querySearch: String) ->
+ WebViewDiscoveryViewModel(
+ querySearch,
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ )
+ }
factory { ProfileRepository(get(), get(), get(), get(), get()) }
factory { ProfileInteractor(get()) }
@@ -109,14 +148,27 @@ val screenModule = module {
)
}
viewModel { (account: Account) -> EditProfileViewModel(get(), get(), get(), get(), account) }
- viewModel { VideoSettingsViewModel(get(), get()) }
- viewModel { VideoQualityViewModel(get(), get()) }
- viewModel { DeleteProfileViewModel(get(), get(), get(), get()) }
+ viewModel { VideoSettingsViewModel(get(), get(), get(), get()) }
+ viewModel { (qualityType: String) -> VideoQualityViewModel(qualityType, get(), get(), get()) }
+ viewModel { DeleteProfileViewModel(get(), get(), get(), get(), get()) }
viewModel { (username: String) -> AnothersProfileViewModel(get(), get(), username) }
single { CourseRepository(get(), get(), get(), get()) }
factory { CourseInteractor(get()) }
- viewModel { CourseInfoViewModel(get(), get(), get(), get(), get(), get()) }
+ viewModel { (pathId: String, infoType: String) ->
+ CourseInfoViewModel(
+ pathId,
+ infoType,
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ )
+ }
viewModel { (courseId: String) ->
CourseDetailsViewModel(
courseId,
@@ -129,16 +181,20 @@ val screenModule = module {
get()
)
}
- viewModel { (courseId: String, courseTitle: String) ->
+ viewModel { (courseId: String, courseTitle: String, enrollmentMode: String) ->
CourseContainerViewModel(
courseId,
courseTitle,
+ enrollmentMode,
+ get(),
+ get(),
+ get(),
+ get(),
get(),
get(),
get(),
get(),
get(),
- get()
)
}
viewModel { (courseId: String) ->
@@ -152,11 +208,14 @@ val screenModule = module {
get(),
get(),
get(),
- get()
+ get(),
+ get(),
)
}
viewModel { (courseId: String) ->
CourseSectionViewModel(
+ courseId,
+ get(),
get(),
get(),
get(),
@@ -165,16 +224,16 @@ val screenModule = module {
get(),
get(),
get(),
- courseId
)
}
- viewModel { (courseId: String) ->
+ viewModel { (courseId: String, unitId: String) ->
CourseUnitContainerViewModel(
+ courseId,
+ unitId,
get(),
get(),
get(),
get(),
- courseId
)
}
viewModel { (courseId: String) ->
@@ -188,11 +247,23 @@ val screenModule = module {
get(),
get(),
get(),
+ get(),
+ get(),
+ get(),
+ )
+ }
+ viewModel { (courseId: String) -> BaseVideoViewModel(courseId, get()) }
+ viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get(), get()) }
+ viewModel { (courseId: String) ->
+ VideoUnitViewModel(
+ courseId,
+ get(),
+ get(),
+ get(),
+ get(),
get()
)
}
- viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get()) }
- viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get()) }
viewModel { (courseId: String, blockId: String) ->
EncodedVideoUnitViewModel(
courseId,
@@ -202,32 +273,49 @@ val screenModule = module {
get(),
get(),
get(),
- get()
+ get(),
+ get(),
)
}
- viewModel { (courseId: String, isSelfPaced: Boolean) ->
+ viewModel { (courseId: String, courseName: String, isSelfPaced: Boolean, enrollmentMode: String) ->
CourseDatesViewModel(
courseId,
+ courseName,
isSelfPaced,
+ enrollmentMode,
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
get(),
get(),
- get()
)
}
viewModel { (courseId: String, handoutsType: String) ->
HandoutsViewModel(
courseId,
- get(),
handoutsType,
- get()
+ get(),
+ get(),
+ get(),
)
}
viewModel { CourseSearchViewModel(get(), get(), get(), get(), get()) }
viewModel { SelectDialogViewModel(get()) }
- single { DiscussionRepository(get(), get()) }
+ single { DiscussionRepository(get(), get(), get()) }
factory { DiscussionInteractor(get()) }
- viewModel { (courseId: String) -> DiscussionTopicsViewModel(get(), get(), get(), courseId) }
+ viewModel { (courseId: String) ->
+ DiscussionTopicsViewModel(
+ get(),
+ get(),
+ get(),
+ get(),
+ courseId
+ )
+ }
viewModel { (courseId: String, topicId: String, threadType: String) ->
DiscussionThreadsViewModel(
get(),
@@ -264,7 +352,28 @@ val screenModule = module {
)
}
- viewModel { (courseId: String?) -> WhatsNewViewModel(courseId, get()) }
+ viewModel { (courseId: String?, infoType: String?) ->
+ WhatsNewViewModel(
+ courseId,
+ infoType,
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ )
+ }
+
+ viewModel { (descendants: List) ->
+ DownloadQueueViewModel(
+ descendants,
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ )
+ }
viewModel { HtmlUnitViewModel(get(), get(), get(), get()) }
viewModel { ProgramViewModel(get(), get(), get(), get(), get(), get(), get()) }
diff --git a/app/src/main/java/org/openedx/app/room/AppDatabase.kt b/app/src/main/java/org/openedx/app/room/AppDatabase.kt
index f28bd8192..be320bae7 100644
--- a/app/src/main/java/org/openedx/app/room/AppDatabase.kt
+++ b/app/src/main/java/org/openedx/app/room/AppDatabase.kt
@@ -3,7 +3,6 @@ package org.openedx.app.room
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
-import org.openedx.core.data.model.room.CourseEntity
import org.openedx.core.data.model.room.CourseStructureEntity
import org.openedx.core.data.model.room.discovery.EnrolledCourseEntity
import org.openedx.core.module.db.DownloadDao
@@ -12,6 +11,7 @@ import org.openedx.course.data.storage.CourseConverter
import org.openedx.course.data.storage.CourseDao
import org.openedx.dashboard.data.DashboardDao
import org.openedx.discovery.data.converter.DiscoveryConverter
+import org.openedx.discovery.data.model.room.CourseEntity
import org.openedx.discovery.data.storage.DiscoveryDao
const val DATABASE_VERSION = 1
diff --git a/auth/build.gradle b/auth/build.gradle
index 02b94a587..7cf4d0a86 100644
--- a/auth/build.gradle
+++ b/auth/build.gradle
@@ -58,9 +58,14 @@ dependencies {
implementation "androidx.credentials:credentials:1.2.0"
implementation "androidx.credentials:credentials-play-services-auth:1.2.0"
implementation "com.facebook.android:facebook-login:16.2.0"
- implementation "com.google.android.gms:play-services-auth:20.7.0"
+ implementation "com.google.android.gms:play-services-auth:21.0.0"
implementation "com.google.android.libraries.identity.googleid:googleid:1.1.0"
- implementation 'com.microsoft.identity.client:msal:4.9.0'
+ implementation("com.microsoft.identity.client:msal:4.9.0") {
+ //Workaround for the error Failed to resolve: 'io.opentelemetry:opentelemetry-bom' for AS Iguana
+ exclude(group: "io.opentelemetry")
+ }
+ implementation("io.opentelemetry:opentelemetry-api:1.18.0")
+ implementation("io.opentelemetry:opentelemetry-context:1.18.0")
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
diff --git a/auth/src/androidTest/java/org/openedx/auth/presentation/signup/RegistrationScreenTest.kt b/auth/src/androidTest/java/org/openedx/auth/presentation/signup/RegistrationScreenTest.kt
deleted file mode 100644
index 845e39034..000000000
--- a/auth/src/androidTest/java/org/openedx/auth/presentation/signup/RegistrationScreenTest.kt
+++ /dev/null
@@ -1,165 +0,0 @@
-package org.openedx.auth.presentation.signup
-
-import androidx.activity.ComponentActivity
-import androidx.compose.ui.semantics.ProgressBarRangeInfo
-import androidx.compose.ui.test.*
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import org.openedx.auth.R
-import org.openedx.core.domain.model.RegistrationField
-import org.openedx.core.domain.model.RegistrationFieldType
-import org.openedx.core.ui.WindowSize
-import org.openedx.core.ui.WindowType
-import org.junit.Rule
-import org.junit.Test
-
-class RegistrationScreenTest {
-
- @get:Rule
- val composeTestRule = createAndroidComposeRule()
-
- //region mockField
- private val option = RegistrationField.Option("def", "Bachelor", "Android")
-
- private val mockField = RegistrationField(
- "Fullname",
- "Fullname",
- RegistrationFieldType.TEXT,
- "Fullname",
- instructions = "Enter your fullname",
- exposed = false,
- required = true,
- restrictions = RegistrationField.Restrictions(),
- options = listOf(option, option),
- errorInstructions = ""
- )
- //endregion
-
-
- @Test
- fun signUpLoadingFields() {
- composeTestRule.setContent {
- org.openedx.auth.presentation.signup.RegistrationScreen(
- windowSize = WindowSize(WindowType.Compact, WindowType.Compact),
- uiState = org.openedx.auth.presentation.signup.SignUpUIState.Loading,
- uiMessage = null,
- isButtonClicked = false,
- validationError = false,
- onBackClick = {},
- onRegisterClick = {}
- )
- }
- with(composeTestRule) {
- onRoot().printToLog("ROOT_TAG")
-
- onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo(0f, 0f..0f))).assertExists()
- onNode(hasScrollAction().and(hasAnyChild(hasText(activity.getString(R.string.auth_sign_up))))).assertDoesNotExist()
- }
- }
-
- @Test
- fun signUpNoOptionalFields() {
- composeTestRule.setContent {
- org.openedx.auth.presentation.signup.RegistrationScreen(
- windowSize = WindowSize(WindowType.Compact, WindowType.Compact),
- uiState = org.openedx.auth.presentation.signup.SignUpUIState.Fields(
- fields = listOf(
- mockField,
- mockField.copy(name = "Age", label = "Age", errorInstructions = "error")
- ),
- optionalFields = emptyList()
- ),
- uiMessage = null,
- isButtonClicked = false,
- validationError = false,
- onBackClick = {},
- onRegisterClick = {}
- )
- }
- with(composeTestRule) {
- onNode(hasText("Fullname").and(hasSetTextAction())).assertExists()
-
- onNode(hasText("Age").and(hasSetTextAction())).assertExists()
-
- onNodeWithText(activity.getString(R.string.auth_show_optional_fields)).assertDoesNotExist()
-
- onNode(hasText(activity.getString(R.string.auth_create_account)).and(hasClickAction())).assertExists()
- }
- }
-
- @Test
- fun signUpHasOptionalFields() {
- composeTestRule.setContent {
- org.openedx.auth.presentation.signup.RegistrationScreen(
- windowSize = WindowSize(WindowType.Compact, WindowType.Compact),
- uiState = org.openedx.auth.presentation.signup.SignUpUIState.Fields(
- fields = listOf(
- mockField,
- mockField.copy(name = "Age", label = "Age", errorInstructions = "error")
- ),
- optionalFields = listOf(mockField)
- ),
- uiMessage = null,
- isButtonClicked = false,
- validationError = false,
- onBackClick = {},
- onRegisterClick = {}
- )
- }
- with(composeTestRule) {
- onNode(hasText("Age").and(hasSetTextAction())).assertExists()
-
- onNodeWithText(activity.getString(R.string.auth_show_optional_fields)).assertExists()
-
- onNode(hasText(activity.getString(R.string.auth_create_account)).and(hasClickAction())).assertExists()
- }
- }
-
- @Test
- fun signUpFieldsWithError() {
- composeTestRule.setContent {
- org.openedx.auth.presentation.signup.RegistrationScreen(
- windowSize = WindowSize(WindowType.Compact, WindowType.Compact),
- uiState = org.openedx.auth.presentation.signup.SignUpUIState.Fields(
- fields = listOf(
- mockField,
- mockField.copy(name = "Age", label = "Age", errorInstructions = "error")
- ),
- optionalFields = emptyList()
- ),
- uiMessage = null,
- isButtonClicked = false,
- validationError = false,
- onBackClick = {},
- onRegisterClick = {}
- )
- }
- with(composeTestRule) {
- onNode(hasText("error")).assertExists()
- }
- }
-
- @Test
- fun signUpCreateAccountClicked() {
- composeTestRule.setContent {
- org.openedx.auth.presentation.signup.RegistrationScreen(
- windowSize = WindowSize(WindowType.Compact, WindowType.Compact),
- uiState = org.openedx.auth.presentation.signup.SignUpUIState.Fields(
- fields = listOf(
- mockField,
- mockField.copy(name = "Age", label = "Age", errorInstructions = "error")
- ),
- optionalFields = listOf(mockField)
- ),
- uiMessage = null,
- isButtonClicked = true,
- validationError = false,
- onBackClick = {},
- onRegisterClick = {}
- )
- }
- with(composeTestRule) {
- onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo(0f, 0f..0f))).assertExists()
- onNode(hasText(activity.getString(R.string.auth_create_account)).and(hasClickAction())).assertDoesNotExist()
- }
- }
-}
\ No newline at end of file
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/AuthAnalytics.kt b/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt
index 669c749b4..e87ad9674 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt
@@ -1,11 +1,60 @@
package org.openedx.auth.presentation
interface AuthAnalytics {
- fun userLoginEvent(method: String)
- fun signUpClickedEvent()
- fun createAccountClickedEvent(provider: String)
- fun registrationSuccessEvent(provider: String)
- fun forgotPasswordClickedEvent()
- fun resetPasswordClickedEvent(success: Boolean)
fun setUserIdForSession(userId: Long)
-}
\ No newline at end of file
+ fun logEvent(event: String, params: Map)
+}
+
+enum class AuthAnalyticsEvent(val eventName: String, val biValue: String) {
+ DISCOVERY_COURSES_SEARCH(
+ "Logistration:Courses Search",
+ "edx.bi.app.logistration.courses_search"
+ ),
+ EXPLORE_ALL_COURSES(
+ "Logistration:Explore All Courses",
+ "edx.bi.app.logistration.explore.all.courses"
+ ),
+ REGISTER_CLICKED(
+ "Logistration:Register Clicked",
+ "edx.bi.app.logistration.register.clicked"
+ ),
+ CREATE_ACCOUNT_CLICKED(
+ "Logistration:Create Account Clicked",
+ "edx.bi.app.logistration.user.create_account.clicked"
+ ),
+ REGISTER_SUCCESS(
+ "Logistration:Register Success",
+ "edx.bi.app.user.register.success"
+ ),
+ SIGN_IN_CLICKED(
+ "Logistration:Sign In Clicked",
+ "edx.bi.app.logistration.signin.clicked"
+ ),
+ USER_SIGN_IN_CLICKED(
+ "Logistration:User Sign In Clicked",
+ "edx.bi.app.logistration.user.signin.clicked"
+ ),
+ SIGN_IN_SUCCESS(
+ "Logistration:Sign In Success",
+ "edx.bi.app.user.signin.success"
+ ),
+ FORGOT_PASSWORD_CLICKED(
+ "Logistration:Forgot Password Clicked",
+ "edx.bi.app.logistration.forgot_password.clicked"
+ ),
+ RESET_PASSWORD_CLICKED(
+ "Logistration:Reset Password Clicked",
+ "edx.bi.app.user.reset_password.clicked"
+ ),
+ RESET_PASSWORD_SUCCESS(
+ "Logistration:Reset Password Success",
+ "edx.bi.app.user.reset_password.success"
+ ),
+}
+
+enum class AuthAnalyticsKey(val key: String) {
+ NAME("name"),
+ SEARCH_QUERY("search_query"),
+ SUCCESS("success"),
+ METHOD("method"),
+}
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 ff73b7a44..9b1266119 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt
@@ -4,19 +4,23 @@ import androidx.fragment.app.FragmentManager
interface AuthRouter {
- fun navigateToMain(fm: FragmentManager, courseId: String?)
+ fun navigateToMain(fm: FragmentManager, courseId: String?, infoType: String?)
- fun navigateToSignIn(fm: FragmentManager, courseId: String?)
+ fun navigateToSignIn(fm: FragmentManager, courseId: String?, infoType: String?)
fun navigateToLogistration(fm: FragmentManager, courseId: String?)
- fun navigateToSignUp(fm: FragmentManager, courseId: String?)
+ fun navigateToSignUp(fm: FragmentManager, courseId: String?, infoType: String?)
fun navigateToRestorePassword(fm: FragmentManager)
- fun navigateToWhatsNew(fm: FragmentManager, courseId: String? = null)
+ fun navigateToWhatsNew(fm: FragmentManager, courseId: String? = null, infoType: String? = null)
- fun navigateToDiscoverCourses(fm: FragmentManager, querySearch: String)
+ fun navigateToWebDiscoverCourses(fm: FragmentManager, querySearch: String)
+
+ 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/logistration/LogistrationFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt
index 6379b246c..738364c34 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt
@@ -38,9 +38,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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.R
-import org.openedx.auth.presentation.AuthRouter
import org.openedx.core.ui.AuthButtonsPanel
import org.openedx.core.ui.SearchBar
import org.openedx.core.ui.displayCutoutForLandscape
@@ -52,7 +52,9 @@ import org.openedx.core.ui.theme.compose.LogistrationLogoView
class LogistrationFragment : Fragment() {
- private val router: AuthRouter by inject()
+ private val viewModel: LogistrationViewModel by viewModel {
+ parametersOf(arguments?.getString(ARG_COURSE_ID, "") ?: "")
+ }
override fun onCreateView(
inflater: LayoutInflater,
@@ -62,16 +64,15 @@ class LogistrationFragment : Fragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
OpenEdXTheme {
- val courseId = arguments?.getString(ARG_COURSE_ID, "")
LogistrationScreen(
onSignInClick = {
- router.navigateToSignIn(parentFragmentManager, courseId)
+ viewModel.navigateToSignIn(parentFragmentManager)
},
onRegisterClick = {
- router.navigateToSignUp(parentFragmentManager, courseId)
+ viewModel.navigateToSignUp(parentFragmentManager)
},
onSearchClick = { querySearch ->
- router.navigateToDiscoverCourses(parentFragmentManager, querySearch)
+ viewModel.navigateToDiscovery(parentFragmentManager, querySearch)
}
)
}
@@ -153,6 +154,7 @@ private fun LogistrationScreen(
label = stringResource(id = R.string.pre_auth_search_hint),
requestFocus = false,
searchValue = textFieldValue,
+ clearOnSubmit = true,
keyboardActions = {
focusManager.clearFocus()
onSearchClick(textFieldValue.text)
diff --git a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt
new file mode 100644
index 000000000..e48a5e8be
--- /dev/null
+++ b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt
@@ -0,0 +1,65 @@
+package org.openedx.auth.presentation.logistration
+
+import androidx.fragment.app.FragmentManager
+import org.openedx.auth.presentation.AuthAnalytics
+import org.openedx.auth.presentation.AuthAnalyticsEvent
+import org.openedx.auth.presentation.AuthAnalyticsKey
+import org.openedx.auth.presentation.AuthRouter
+import org.openedx.core.BaseViewModel
+import org.openedx.core.config.Config
+import org.openedx.core.extension.takeIfNotEmpty
+
+class LogistrationViewModel(
+ private val courseId: String,
+ private val router: AuthRouter,
+ private val config: Config,
+ private val analytics: AuthAnalytics,
+) : BaseViewModel() {
+
+ private val discoveryTypeWebView get() = config.getDiscoveryConfig().isViewTypeWebView()
+
+ fun navigateToSignIn(parentFragmentManager: FragmentManager) {
+ router.navigateToSignIn(parentFragmentManager, courseId, null)
+ logEvent(AuthAnalyticsEvent.SIGN_IN_CLICKED)
+ }
+
+ fun navigateToSignUp(parentFragmentManager: FragmentManager) {
+ router.navigateToSignUp(parentFragmentManager, courseId, null)
+ logEvent(AuthAnalyticsEvent.REGISTER_CLICKED)
+ }
+
+ fun navigateToDiscovery(parentFragmentManager: FragmentManager, querySearch: String) {
+ if (discoveryTypeWebView) {
+ router.navigateToWebDiscoverCourses(
+ parentFragmentManager,
+ querySearch
+ )
+ } else {
+ router.navigateToNativeDiscoverCourses(
+ parentFragmentManager,
+ querySearch
+ )
+ }
+ querySearch.takeIfNotEmpty()?.let {
+ logEvent(
+ event = AuthAnalyticsEvent.DISCOVERY_COURSES_SEARCH,
+ params = buildMap {
+ put(AuthAnalyticsKey.SEARCH_QUERY.key, querySearch)
+ }
+ )
+ } ?: logEvent(event = AuthAnalyticsEvent.EXPLORE_ALL_COURSES)
+ }
+
+ private fun logEvent(
+ event: AuthAnalyticsEvent,
+ params: Map = emptyMap(),
+ ) {
+ analytics.logEvent(
+ event = event.eventName,
+ params = buildMap {
+ put(AuthAnalyticsKey.NAME.key, event.biValue)
+ putAll(params)
+ }
+ )
+ }
+}
diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt
index dd530bfa0..18cf169bc 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt
@@ -81,7 +81,7 @@ class RestorePasswordFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
- savedInstanceState: Bundle?
+ savedInstanceState: Bundle?,
) = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
@@ -123,7 +123,7 @@ private fun RestorePasswordScreen(
uiState: RestorePasswordUIState,
uiMessage: UIMessage?,
onBackClick: () -> Unit,
- onRestoreButtonClick: (String) -> Unit
+ onRestoreButtonClick: (String) -> Unit,
) {
val scaffoldState = rememberScaffoldState()
val scrollState = rememberScrollState()
@@ -289,7 +289,7 @@ private fun RestorePasswordScreen(
}
} else {
OpenEdXButton(
- width = buttonWidth.testTag("btn_reset_password"),
+ modifier = buttonWidth.testTag("btn_reset_password"),
text = stringResource(id = authR.string.auth_reset_password),
onClick = {
onRestoreButtonClick(email)
@@ -337,7 +337,7 @@ private fun RestorePasswordScreen(
)
Spacer(Modifier.height(48.dp))
OpenEdXButton(
- width = buttonWidth,
+ modifier = buttonWidth,
text = stringResource(id = R.string.core_sign_in),
onClick = {
onBackClick()
diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt
index 427f2f263..b21c694da 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt
@@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.openedx.auth.domain.interactor.AuthInteractor
import org.openedx.auth.presentation.AuthAnalytics
+import org.openedx.auth.presentation.AuthAnalyticsEvent
+import org.openedx.auth.presentation.AuthAnalyticsKey
import org.openedx.core.BaseViewModel
import org.openedx.core.R
import org.openedx.core.SingleEventLiveData
@@ -21,7 +23,7 @@ class RestorePasswordViewModel(
private val interactor: AuthInteractor,
private val resourceManager: ResourceManager,
private val analytics: AuthAnalytics,
- private val appUpgradeNotifier: AppUpgradeNotifier
+ private val appUpgradeNotifier: AppUpgradeNotifier,
) : BaseViewModel() {
private val _uiState = MutableLiveData()
@@ -41,28 +43,29 @@ class RestorePasswordViewModel(
}
fun passwordReset(email: String) {
+ logEvent(AuthAnalyticsEvent.RESET_PASSWORD_CLICKED)
_uiState.value = RestorePasswordUIState.Loading
viewModelScope.launch {
try {
if (email.isNotEmpty() && email.isEmailValid()) {
if (interactor.passwordReset(email)) {
_uiState.value = RestorePasswordUIState.Success(email)
- analytics.resetPasswordClickedEvent(true)
+ logResetPasswordEvent(true)
} else {
_uiState.value = RestorePasswordUIState.Initial
_uiMessage.value =
UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))
- analytics.resetPasswordClickedEvent(false)
+ logResetPasswordEvent(false)
}
} else {
_uiState.value = RestorePasswordUIState.Initial
_uiMessage.value =
UIMessage.SnackBarMessage(resourceManager.getString(org.openedx.auth.R.string.auth_invalid_email))
- analytics.resetPasswordClickedEvent(false)
+ logResetPasswordEvent(false)
}
} catch (e: Exception) {
_uiState.value = RestorePasswordUIState.Initial
- analytics.resetPasswordClickedEvent(false)
+ logResetPasswordEvent(false)
if (e is EdxError.ValidationException) {
_uiMessage.value = UIMessage.SnackBarMessage(e.error)
} else if (e.isInternetError()) {
@@ -84,4 +87,25 @@ class RestorePasswordViewModel(
}
}
-}
\ No newline at end of file
+ private fun logResetPasswordEvent(success: Boolean) {
+ logEvent(
+ event = AuthAnalyticsEvent.RESET_PASSWORD_SUCCESS,
+ params = buildMap {
+ put(AuthAnalyticsKey.SUCCESS.key, success)
+ }
+ )
+ }
+
+ private fun logEvent(
+ event: AuthAnalyticsEvent,
+ params: Map = emptyMap(),
+ ) {
+ analytics.logEvent(
+ event = event.eventName,
+ params = buildMap {
+ put(AuthAnalyticsKey.NAME.key, event.biValue)
+ putAll(params)
+ }
+ )
+ }
+}
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 1adcaa3a1..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
@@ -26,10 +23,11 @@ import org.openedx.core.ui.theme.OpenEdXTheme
class SignInFragment : Fragment() {
private val viewModel: SignInViewModel by viewModel {
- parametersOf(requireArguments().getString(ARG_COURSE_ID, null))
+ parametersOf(
+ requireArguments().getString(ARG_COURSE_ID, ""),
+ requireArguments().getString(ARG_INFO_TYPE, "")
+ )
}
- private val router: AuthRouter by inject()
- private val whatsNewGlobalManager by inject()
override fun onCreateView(
inflater: LayoutInflater,
@@ -58,33 +56,27 @@ class SignInFragment : Fragment() {
)
AuthEvent.ForgotPasswordClick -> {
- viewModel.forgotPasswordClickedEvent()
- router.navigateToRestorePassword(parentFragmentManager)
+ viewModel.navigateToForgotPassword(parentFragmentManager)
}
AuthEvent.RegisterClick -> {
- viewModel.signUpClickedEvent()
- router.navigateToSignUp(parentFragmentManager, 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)
- } else {
- router.navigateToMain(parentFragmentManager, viewModel.courseId)
- }
- }
-
+ viewModel.proceedWhatsNew(parentFragmentManager)
}
} else {
AppUpgradeRequiredScreen(
@@ -99,10 +91,12 @@ class SignInFragment : Fragment() {
companion object {
private const val ARG_COURSE_ID = "courseId"
- fun newInstance(courseId: String?): SignInFragment {
+ private const val ARG_INFO_TYPE = "info_type"
+ fun newInstance(courseId: String?, infoType: String?): SignInFragment {
val fragment = SignInFragment()
fragment.arguments = bundleOf(
- ARG_COURSE_ID to courseId
+ ARG_COURSE_ID to courseId,
+ ARG_INFO_TYPE to infoType
)
return fragment
}
@@ -112,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 e5532429b..7ebc5a569 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,11 @@ 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.AuthAnalyticsEvent
+import org.openedx.auth.presentation.AuthAnalyticsKey
+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 +27,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,8 +45,12 @@ 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?,
) : BaseViewModel() {
private val logger = Logger("SignInViewModel")
@@ -51,6 +62,7 @@ class SignInViewModel(
isMicrosoftAuthEnabled = config.getMicrosoftConfig().isEnabled(),
isSocialAuthEnabled = config.isSocialAuthEnabled(),
isLogistrationEnabled = config.isPreLoginExperienceEnabled(),
+ agreement = agreementProvider.getAgreement(isSignIn = true)?.createHonorCodeField(),
)
)
internal val uiState: StateFlow = _uiState
@@ -68,6 +80,7 @@ class SignInViewModel(
}
fun login(username: String, password: String) {
+ logEvent(AuthAnalyticsEvent.USER_SIGN_IN_CLICKED)
if (!validator.isEmailOrUserNameValid(username)) {
_uiMessage.value =
UIMessage.SnackBarMessage(resourceManager.getString(R.string.auth_invalid_email_username))
@@ -85,7 +98,15 @@ class SignInViewModel(
interactor.login(username, password)
_uiState.update { it.copy(loginSuccess = true) }
setUserId()
- analytics.userLoginEvent(AuthType.PASSWORD.methodName)
+ logEvent(
+ AuthAnalyticsEvent.SIGN_IN_SUCCESS,
+ buildMap {
+ put(
+ AuthAnalyticsKey.METHOD.key,
+ AuthType.PASSWORD.methodName.lowercase()
+ )
+ }
+ )
} catch (e: Exception) {
if (e is EdxError.InvalidGrantException) {
_uiMessage.value =
@@ -123,12 +144,14 @@ class SignInViewModel(
}
}
- fun signUpClickedEvent() {
- analytics.signUpClickedEvent()
+ fun navigateToSignUp(parentFragmentManager: FragmentManager) {
+ router.navigateToSignUp(parentFragmentManager, null, null)
+ logEvent(AuthAnalyticsEvent.REGISTER_CLICKED)
}
- fun forgotPasswordClickedEvent() {
- analytics.forgotPasswordClickedEvent()
+ fun navigateToForgotPassword(parentFragmentManager: FragmentManager) {
+ router.navigateToRestorePassword(parentFragmentManager)
+ logEvent(AuthAnalyticsEvent.FORGOT_PASSWORD_CLICKED)
}
override fun onCleared() {
@@ -146,7 +169,6 @@ class SignInViewModel(
logger.d { "Social login (${authType.methodName}) success" }
_uiState.update { it.copy(loginSuccess = true) }
setUserId()
- analytics.userLoginEvent(authType.methodName)
_uiState.update { it.copy(showProgress = false) }
}
}
@@ -176,4 +198,46 @@ 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
+ )
+ }
+ }
+ }
+
+ private fun logEvent(
+ event: AuthAnalyticsEvent,
+ params: Map = emptyMap(),
+ ) {
+ analytics.logEvent(
+ event = event.eventName,
+ params = buildMap {
+ put(AuthAnalyticsKey.NAME.key, event.biValue)
+ putAll(params)
+ }
+ )
+ }
}
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..77e290994 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))
+ },
+ )
+ }
}
}
}
@@ -257,7 +273,7 @@ private fun AuthForm(
CircularProgressIndicator(color = MaterialTheme.appColors.primary)
} else {
OpenEdXButton(
- width = buttonWidth.testTag("btn_sign_in"),
+ modifier = buttonWidth.testTag("btn_sign_in"),
text = stringResource(id = coreR.string.core_sign_in),
onClick = {
onEvent(AuthEvent.SignIn(login = login, password = password))
@@ -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 97bfe45d9..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
@@ -24,7 +24,10 @@ import org.openedx.core.ui.theme.OpenEdXTheme
class SignUpFragment : Fragment() {
private val viewModel by viewModel {
- parametersOf(requireArguments().getString(ARG_COURSE_ID, ""))
+ parametersOf(
+ requireArguments().getString(ARG_COURSE_ID, ""),
+ requireArguments().getString(ARG_INFO_TYPE, "")
+ )
}
private val router by inject()
@@ -67,13 +70,20 @@ class SignUpFragment : Fragment() {
},
onFieldUpdated = { key, value ->
viewModel.updateField(key, value)
+ },
+ onHyperLinkClick = { links, link ->
+ viewModel.openLink(parentFragmentManager, links, link)
}
)
LaunchedEffect(uiState.successLogin) {
if (uiState.successLogin) {
router.clearBackStack(requireActivity().supportFragmentManager)
- router.navigateToMain(parentFragmentManager, viewModel.courseId)
+ router.navigateToMain(
+ parentFragmentManager,
+ viewModel.courseId,
+ viewModel.infoType
+ )
}
}
} else {
@@ -89,10 +99,12 @@ class SignUpFragment : Fragment() {
companion object {
private const val ARG_COURSE_ID = "courseId"
- fun newInstance(courseId: String?): SignUpFragment {
+ private const val ARG_INFO_TYPE = "info_type"
+ fun newInstance(courseId: String?, infoType: String?): SignUpFragment {
val fragment = SignUpFragment()
fragment.arguments = bundleOf(
- ARG_COURSE_ID to courseId
+ ARG_COURSE_ID to courseId,
+ ARG_INFO_TYPE to infoType
)
return fragment
}
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 af1b8e094..8fafe40ff 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,25 @@ 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.AuthAnalyticsEvent
+import org.openedx.auth.presentation.AuthAnalyticsKey
+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,9 +41,12 @@ 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() {
private val logger = Logger("SignUpViewModel")
@@ -68,35 +77,65 @@ 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("")
+ logEvent(AuthAnalyticsEvent.CREATE_ACCOUNT_CLICKED)
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()) {
@@ -119,7 +158,16 @@ class SignUpViewModel(
resultMap[ApiConstants.CLIENT_ID] = config.getOAuthClientId()
}
interactor.register(resultMap.toMap())
- analytics.registrationSuccessEvent(socialAuth?.authType?.postfix.orEmpty())
+ logEvent(
+ event = AuthAnalyticsEvent.REGISTER_SUCCESS,
+ params = buildMap {
+ put(
+ AuthAnalyticsKey.METHOD.key,
+ (socialAuth?.authType?.methodName
+ ?: AuthType.PASSWORD.methodName).lowercase()
+ )
+ }
+ )
if (socialAuth == null) {
interactor.login(
resultMap.getValue(ApiConstants.EMAIL),
@@ -136,13 +184,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)
)
)
}
@@ -177,21 +225,29 @@ 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)
+ logEvent(
+ AuthAnalyticsEvent.SIGN_IN_SUCCESS,
+ buildMap {
+ put(
+ AuthAnalyticsKey.METHOD.key,
+ socialAuth.authType.methodName.lowercase()
+ )
+ }
+ )
_uiState.update { it.copy(successLogin = true) }
logger.d { "Social login (${socialAuth.authType.methodName}) success" }
}
@@ -207,12 +263,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() {
@@ -230,15 +282,35 @@ 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
}
- it.copy(allFields = updatedFields)
}
+ updateFields(updatedFields)
+ }
+
+ fun openLink(fragmentManager: FragmentManager, links: Map, link: String) {
+ links.forEach { (key, value) ->
+ if (value == link) {
+ router.navigateToWebContent(fragmentManager, key, value)
+ return
+ }
+ }
+ }
+
+ private fun logEvent(
+ event: AuthAnalyticsEvent,
+ params: Map = emptyMap(),
+ ) {
+ analytics.logEvent(
+ event = event.eventName,
+ params = buildMap {
+ put(AuthAnalyticsKey.NAME.key, event.biValue)
+ putAll(params)
+ }
+ )
}
}
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..2e2180d83 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(
@@ -415,7 +435,7 @@ internal fun SignUpView(
}
} else {
OpenEdXButton(
- width = buttonWidth.testTag("btn_create_account"),
+ modifier = buttonWidth.testTag("btn_create_account"),
text = stringResource(id = R.string.auth_create_account),
onClick = {
showErrorMap.clear()
@@ -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 e875a4539..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
@@ -50,6 +50,7 @@ import org.openedx.auth.R
import org.openedx.core.domain.model.RegistrationField
import org.openedx.core.domain.model.RegistrationFieldType
import org.openedx.core.extension.TextConverter
+import org.openedx.core.extension.tagId
import org.openedx.core.ui.HyperlinkText
import org.openedx.core.ui.SheetContent
import org.openedx.core.ui.noRippleClickable
@@ -86,7 +87,7 @@ fun RequiredFields(
val linkedText =
TextConverter.htmlTextToLinkedText(field.label)
HyperlinkText(
- modifier = Modifier.testTag("txt_${field.name}"),
+ modifier = Modifier.testTag("txt_${field.name.tagId()}"),
fullText = linkedText.text,
hyperLinks = linkedText.links,
linkTextColor = MaterialTheme.appColors.primary
@@ -94,7 +95,9 @@ fun RequiredFields(
}
RegistrationFieldType.CHECKBOX -> {
- //Text("checkbox")
+ CheckboxField(text = field.label, defaultValue = field.defaultValue) {
+ onFieldUpdated(field.name, it.toString())
+ }
}
RegistrationFieldType.SELECT -> {
@@ -138,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 ->
@@ -166,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 -> {
@@ -305,7 +314,7 @@ fun InputRegistrationField(
Column {
Text(
modifier = Modifier
- .testTag("txt_${registrationField.name}_label")
+ .testTag("txt_${registrationField.name.tagId()}_label")
.fillMaxWidth(),
text = registrationField.label,
style = MaterialTheme.appTypography.labelLarge,
@@ -329,7 +338,7 @@ fun InputRegistrationField(
shape = MaterialTheme.appShapes.textFieldShape,
placeholder = {
Text(
- modifier = modifier.testTag("txt_${registrationField.name}_placeholder"),
+ modifier = modifier.testTag("txt_${registrationField.name.tagId()}_placeholder"),
text = registrationField.label,
color = MaterialTheme.appColors.textFieldHint,
style = MaterialTheme.appTypography.bodyMedium
@@ -345,11 +354,11 @@ fun InputRegistrationField(
},
textStyle = MaterialTheme.appTypography.bodyMedium,
singleLine = isSingleLine,
- modifier = modifier.testTag("tf_${registrationField.name}")
+ modifier = modifier.testTag("tf_${registrationField.name.tagId()}")
)
Spacer(modifier = Modifier.height(6.dp))
Text(
- modifier = Modifier.testTag("txt_${registrationField.name}_description"),
+ modifier = Modifier.testTag("txt_${registrationField.name.tagId()}_description"),
text = helperText,
style = MaterialTheme.appTypography.bodySmall,
color = helperTextColor
@@ -392,7 +401,7 @@ fun SelectableRegisterField(
) {
Text(
modifier = Modifier
- .testTag("txt_${registrationField.name}_label")
+ .testTag("txt_${registrationField.name.tagId()}_label")
.fillMaxWidth(),
text = registrationField.label,
style = MaterialTheme.appTypography.labelLarge,
@@ -414,14 +423,14 @@ fun SelectableRegisterField(
textStyle = MaterialTheme.appTypography.bodyMedium,
onValueChange = { },
modifier = Modifier
- .testTag("tf_${registrationField.name}")
+ .testTag("tf_${registrationField.name.tagId()}")
.fillMaxWidth()
.noRippleClickable {
onClick(registrationField.name, registrationField.options)
},
placeholder = {
Text(
- modifier = Modifier.testTag("txt_${registrationField.name}_placeholder"),
+ modifier = Modifier.testTag("txt_${registrationField.name.tagId()}_placeholder"),
text = registrationField.label,
color = MaterialTheme.appColors.textFieldHint,
style = MaterialTheme.appTypography.bodyMedium
@@ -437,7 +446,7 @@ fun SelectableRegisterField(
)
Spacer(modifier = Modifier.height(6.dp))
Text(
- modifier = Modifier.testTag("txt_${registrationField.name}_description"),
+ modifier = Modifier.testTag("txt_${registrationField.name.tagId()}_description"),
text = helperText,
style = MaterialTheme.appTypography.bodySmall,
color = helperTextColor
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/java/org/openedx/auth/presentation/ui/SocialAuthView.kt b/auth/src/main/java/org/openedx/auth/presentation/ui/SocialAuthView.kt
index c9d73662b..336c09f8f 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/ui/SocialAuthView.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/ui/SocialAuthView.kt
@@ -74,7 +74,7 @@ internal fun SocialAuthView(
R.string.auth_continue_facebook
}
OpenEdXButton(
- width = Modifier
+ modifier = Modifier
.testTag("btn_facebook_auth")
.padding(top = 12.dp)
.fillMaxWidth(),
@@ -106,7 +106,7 @@ internal fun SocialAuthView(
R.string.auth_continue_microsoft
}
OpenEdXButton(
- width = Modifier
+ modifier = Modifier
.testTag("btn_microsoft_auth")
.padding(top = 12.dp)
.fillMaxWidth(),
diff --git a/auth/src/main/res/drawable/auth_ic_email.xml b/auth/src/main/res/drawable/auth_ic_email.xml
index c76bdb4c0..3fdb37de2 100644
--- a/auth/src/main/res/drawable/auth_ic_email.xml
+++ b/auth/src/main/res/drawable/auth_ic_email.xml
@@ -1,34 +1,10 @@
-
-
-
-
-
-
-
-
-
+ android:width="85dp"
+ android:height="66dp"
+ android:viewportWidth="85"
+ android:viewportHeight="66">
+
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/restore/RestorePasswordViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt
index e80b93db7..4c92b317f 100644
--- a/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt
+++ b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt
@@ -1,12 +1,6 @@
package org.openedx.auth.presentation.restore
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import org.openedx.auth.domain.interactor.AuthInteractor
-import org.openedx.auth.presentation.AuthAnalytics
-import org.openedx.core.R
-import org.openedx.core.UIMessage
-import org.openedx.core.system.EdxError
-import org.openedx.core.system.ResourceManager
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -15,13 +9,23 @@ import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.test.*
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
import org.junit.After
-import org.junit.Assert.*
+import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
+import org.openedx.auth.domain.interactor.AuthInteractor
+import org.openedx.auth.presentation.AuthAnalytics
+import org.openedx.core.R
+import org.openedx.core.UIMessage
+import org.openedx.core.system.EdxError
+import org.openedx.core.system.ResourceManager
import org.openedx.core.system.notifier.AppUpgradeNotifier
import java.net.UnknownHostException
@@ -66,13 +70,14 @@ class RestorePasswordViewModelTest {
@Test
fun `passwordReset empty email validation error`() = runTest {
- val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
+ val viewModel =
+ RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
coEvery { interactor.passwordReset(emptyEmail) } returns true
- every { analytics.resetPasswordClickedEvent(false) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
viewModel.passwordReset(emptyEmail)
advanceUntilIdle()
coVerify(exactly = 0) { interactor.passwordReset(any()) }
- verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) }
+ verify(exactly = 2) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage
@@ -83,13 +88,14 @@ class RestorePasswordViewModelTest {
@Test
fun `passwordReset invalid email validation error`() = runTest {
- val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
+ val viewModel =
+ RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
coEvery { interactor.passwordReset(invalidEmail) } returns true
- every { analytics.resetPasswordClickedEvent(false) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
viewModel.passwordReset(invalidEmail)
advanceUntilIdle()
coVerify(exactly = 0) { interactor.passwordReset(any()) }
- verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) }
+ verify(exactly = 2) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage
@@ -100,13 +106,14 @@ class RestorePasswordViewModelTest {
@Test
fun `passwordReset validation error`() = runTest {
- val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
+ val viewModel =
+ RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
coEvery { interactor.passwordReset(correctEmail) } throws EdxError.ValidationException("error")
- every { analytics.resetPasswordClickedEvent(false) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
viewModel.passwordReset(correctEmail)
advanceUntilIdle()
coVerify(exactly = 1) { interactor.passwordReset(any()) }
- verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) }
+ verify(exactly = 2) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage
@@ -117,13 +124,14 @@ class RestorePasswordViewModelTest {
@Test
fun `passwordReset no internet error`() = runTest {
- val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
+ val viewModel =
+ RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
coEvery { interactor.passwordReset(correctEmail) } throws UnknownHostException()
- every { analytics.resetPasswordClickedEvent(false) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
viewModel.passwordReset(correctEmail)
advanceUntilIdle()
coVerify(exactly = 1) { interactor.passwordReset(any()) }
- verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) }
+ verify(exactly = 2) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage
@@ -134,13 +142,14 @@ class RestorePasswordViewModelTest {
@Test
fun `passwordReset unknown error`() = runTest {
- val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
+ val viewModel =
+ RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
coEvery { interactor.passwordReset(correctEmail) } throws Exception()
- every { analytics.resetPasswordClickedEvent(false) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
viewModel.passwordReset(correctEmail)
advanceUntilIdle()
coVerify(exactly = 1) { interactor.passwordReset(any()) }
- verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) }
+ verify(exactly = 2) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage
@@ -151,13 +160,14 @@ class RestorePasswordViewModelTest {
@Test
fun `unSuccess restore password`() = runTest {
- val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
+ val viewModel =
+ RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
coEvery { interactor.passwordReset(correctEmail) } returns false
- every { analytics.resetPasswordClickedEvent(false) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
viewModel.passwordReset(correctEmail)
advanceUntilIdle()
coVerify(exactly = 1) { interactor.passwordReset(any()) }
- verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) }
+ verify(exactly = 2) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage
@@ -169,13 +179,14 @@ class RestorePasswordViewModelTest {
@Test
fun `success restore password`() = runTest {
- val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
+ val viewModel =
+ RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier)
coEvery { interactor.passwordReset(correctEmail) } returns true
- every { analytics.resetPasswordClickedEvent(true) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
viewModel.passwordReset(correctEmail)
advanceUntilIdle()
coVerify(exactly = 1) { interactor.passwordReset(any()) }
- verify(exactly = 1) { analytics.resetPasswordClickedEvent(true) }
+ verify(exactly = 2) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
val state = viewModel.uiState.value as? RestorePasswordUIState.Success
@@ -185,6 +196,4 @@ class RestorePasswordViewModelTest {
assertEquals(true, viewModel.uiState.value is RestorePasswordUIState.Success)
assertEquals(null, message)
}
-
-
-}
\ No newline at end of file
+}
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 16b5032c4..b36aabb10 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()
@@ -90,6 +97,7 @@ class SignInViewModelTest {
every { validator.isEmailOrUserNameValid(any()) } returns false
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
val viewModel = SignInViewModel(
interactor = interactor,
resourceManager = resourceManager,
@@ -98,12 +106,17 @@ class SignInViewModelTest {
analytics = analytics,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
+ whatsNewGlobalManager = whatsNewGlobalManager,
courseId = "",
+ infoType = "",
)
viewModel.login("", "")
coVerify(exactly = 0) { interactor.login(any(), any()) }
verify(exactly = 0) { analytics.setUserIdForSession(any()) }
+ verify(exactly = 1) { analytics.logEvent(any(), any()) }
val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage
val uiState = viewModel.uiState.value
@@ -117,6 +130,7 @@ class SignInViewModelTest {
every { validator.isEmailOrUserNameValid(any()) } returns false
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
val viewModel = SignInViewModel(
interactor = interactor,
resourceManager = resourceManager,
@@ -125,8 +139,12 @@ class SignInViewModelTest {
analytics = analytics,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
+ whatsNewGlobalManager = whatsNewGlobalManager,
courseId = "",
+ infoType = "",
)
viewModel.login("acc@test.o", "")
coVerify(exactly = 0) { interactor.login(any(), any()) }
@@ -145,6 +163,7 @@ class SignInViewModelTest {
every { validator.isPasswordValid(any()) } returns false
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
coVerify(exactly = 0) { interactor.login(any(), any()) }
val viewModel = SignInViewModel(
interactor = interactor,
@@ -154,8 +173,12 @@ class SignInViewModelTest {
analytics = analytics,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
+ whatsNewGlobalManager = whatsNewGlobalManager,
courseId = "",
+ infoType = "",
)
viewModel.login("acc@test.org", "")
@@ -174,6 +197,7 @@ class SignInViewModelTest {
every { validator.isPasswordValid(any()) } returns false
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
val viewModel = SignInViewModel(
interactor = interactor,
resourceManager = resourceManager,
@@ -182,13 +206,18 @@ class SignInViewModelTest {
analytics = analytics,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
+ whatsNewGlobalManager = whatsNewGlobalManager,
courseId = "",
+ infoType = "",
)
viewModel.login("acc@test.org", "ed")
coVerify(exactly = 0) { interactor.login(any(), any()) }
verify(exactly = 0) { analytics.setUserIdForSession(any()) }
+ verify(exactly = 1) { analytics.logEvent(any(), any()) }
val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage
val uiState = viewModel.uiState.value
@@ -201,9 +230,9 @@ class SignInViewModelTest {
fun `login success`() = runTest {
every { validator.isEmailOrUserNameValid(any()) } returns true
every { validator.isPasswordValid(any()) } returns true
- every { analytics.userLoginEvent(any()) } returns Unit
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
val viewModel = SignInViewModel(
interactor = interactor,
resourceManager = resourceManager,
@@ -212,16 +241,20 @@ class SignInViewModelTest {
analytics = analytics,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
+ whatsNewGlobalManager = whatsNewGlobalManager,
courseId = "",
+ infoType = "",
)
coEvery { interactor.login("acc@test.org", "edx") } returns Unit
viewModel.login("acc@test.org", "edx")
advanceUntilIdle()
coVerify(exactly = 1) { interactor.login(any(), any()) }
- verify(exactly = 1) { analytics.userLoginEvent(any()) }
verify(exactly = 1) { analytics.setUserIdForSession(any()) }
+ verify(exactly = 2) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
val uiState = viewModel.uiState.value
assertFalse(uiState.showProgress)
@@ -235,6 +268,7 @@ class SignInViewModelTest {
every { validator.isPasswordValid(any()) } returns true
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
val viewModel = SignInViewModel(
interactor = interactor,
resourceManager = resourceManager,
@@ -243,8 +277,12 @@ class SignInViewModelTest {
analytics = analytics,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
+ whatsNewGlobalManager = whatsNewGlobalManager,
courseId = "",
+ infoType = "",
)
coEvery { interactor.login("acc@test.org", "edx") } throws UnknownHostException()
viewModel.login("acc@test.org", "edx")
@@ -252,6 +290,7 @@ class SignInViewModelTest {
coVerify(exactly = 1) { interactor.login(any(), any()) }
verify(exactly = 0) { analytics.setUserIdForSession(any()) }
+ verify(exactly = 1) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage
@@ -267,6 +306,7 @@ class SignInViewModelTest {
every { validator.isPasswordValid(any()) } returns true
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
val viewModel = SignInViewModel(
interactor = interactor,
resourceManager = resourceManager,
@@ -275,8 +315,12 @@ class SignInViewModelTest {
analytics = analytics,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
+ whatsNewGlobalManager = whatsNewGlobalManager,
courseId = "",
+ infoType = "",
)
coEvery { interactor.login("acc@test.org", "edx") } throws EdxError.InvalidGrantException()
viewModel.login("acc@test.org", "edx")
@@ -285,6 +329,7 @@ class SignInViewModelTest {
coVerify(exactly = 1) { interactor.login(any(), any()) }
verify(exactly = 0) { analytics.setUserIdForSession(any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
+ verify(exactly = 1) { analytics.logEvent(any(), any()) }
val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage
val uiState = viewModel.uiState.value
@@ -299,6 +344,7 @@ class SignInViewModelTest {
every { validator.isPasswordValid(any()) } returns true
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
val viewModel = SignInViewModel(
interactor = interactor,
resourceManager = resourceManager,
@@ -307,8 +353,12 @@ class SignInViewModelTest {
analytics = analytics,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
+ whatsNewGlobalManager = whatsNewGlobalManager,
courseId = "",
+ infoType = "",
)
coEvery { interactor.login("acc@test.org", "edx") } throws IllegalStateException()
viewModel.login("acc@test.org", "edx")
@@ -317,6 +367,7 @@ class SignInViewModelTest {
coVerify(exactly = 1) { interactor.login(any(), any()) }
verify(exactly = 0) { analytics.setUserIdForSession(any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
+ verify(exactly = 1) { analytics.logEvent(any(), any()) }
val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage
val uiState = viewModel.uiState.value
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 bd048902c..f304f7363 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,14 +135,17 @@ class SignUpViewModelTest {
preferencesManager = preferencesManager,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
courseId = "",
+ infoType = "",
)
coEvery { interactor.validateRegistrationFields(parametersMap) } returns ValidationFields(
parametersMap
)
coEvery { interactor.getRegistrationFields() } returns listOfFields
- every { analytics.createAccountClickedEvent(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
coEvery { interactor.register(parametersMap) } returns Unit
coEvery { interactor.login("", "") } returns Unit
every { preferencesManager.user } returns user
@@ -147,7 +158,7 @@ class SignUpViewModelTest {
viewModel.register()
advanceUntilIdle()
coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) }
- verify(exactly = 1) { analytics.createAccountClickedEvent(any()) }
+ verify(exactly = 1) { analytics.logEvent(any(), any()) }
coVerify(exactly = 0) { interactor.register(any()) }
coVerify(exactly = 0) { interactor.login(any(), any()) }
verify(exactly = 0) { analytics.setUserIdForSession(any()) }
@@ -167,8 +178,11 @@ class SignUpViewModelTest {
preferencesManager = preferencesManager,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
courseId = "",
+ infoType = "",
)
val deferred = async { viewModel.uiMessage.first() }
@@ -181,7 +195,7 @@ class SignUpViewModelTest {
parametersMap.getValue(ApiConstants.PASSWORD)
)
} returns Unit
- every { analytics.createAccountClickedEvent(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
viewModel.getRegistrationFields()
@@ -191,7 +205,7 @@ class SignUpViewModelTest {
}
viewModel.register()
advanceUntilIdle()
- verify(exactly = 1) { analytics.createAccountClickedEvent(any()) }
+ verify(exactly = 1) { analytics.logEvent(any(), any()) }
verify(exactly = 0) { analytics.setUserIdForSession(any()) }
coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) }
coVerify(exactly = 0) { interactor.register(any()) }
@@ -213,21 +227,24 @@ class SignUpViewModelTest {
preferencesManager = preferencesManager,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
courseId = "",
+ infoType = "",
)
val deferred = async { viewModel.uiMessage.first() }
coEvery { interactor.validateRegistrationFields(parametersMap) } throws Exception()
coEvery { interactor.register(parametersMap) } returns Unit
coEvery { interactor.login("", "") } returns Unit
- every { analytics.createAccountClickedEvent(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
every { preferencesManager.user } returns user
every { analytics.setUserIdForSession(any()) } returns Unit
viewModel.register()
advanceUntilIdle()
verify(exactly = 0) { analytics.setUserIdForSession(any()) }
- verify(exactly = 1) { analytics.createAccountClickedEvent(any()) }
+ verify(exactly = 1) { analytics.logEvent(any(), any()) }
coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) }
coVerify(exactly = 0) { interactor.register(any()) }
coVerify(exactly = 0) { interactor.login(any(), any()) }
@@ -239,7 +256,6 @@ class SignUpViewModelTest {
assertEquals(somethingWrong, (deferred.await() as? UIMessage.SnackBarMessage)?.message)
}
-
@Test
fun `success register`() = runTest {
val viewModel = SignUpViewModel(
@@ -249,14 +265,17 @@ class SignUpViewModelTest {
preferencesManager = preferencesManager,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
courseId = "",
+ infoType = "",
)
coEvery { interactor.validateRegistrationFields(parametersMap) } returns ValidationFields(
emptyMap()
)
- every { analytics.createAccountClickedEvent(any()) } returns Unit
- every { analytics.registrationSuccessEvent(any()) } returns Unit
+ every { analytics.logEvent(any(), any()) } returns Unit
+ coEvery { analytics.logEvent(any(), any()) } returns Unit
coEvery { interactor.getRegistrationFields() } returns listOfFields
coEvery { interactor.register(parametersMap) } returns Unit
coEvery {
@@ -278,8 +297,7 @@ class SignUpViewModelTest {
coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) }
coVerify(exactly = 1) { interactor.register(any()) }
coVerify(exactly = 1) { interactor.login(any(), any()) }
- verify(exactly = 1) { analytics.createAccountClickedEvent(any()) }
- verify(exactly = 1) { analytics.registrationSuccessEvent(any()) }
+ verify(exactly = 2) { analytics.logEvent(any(), any()) }
verify(exactly = 1) { appUpgradeNotifier.notifier }
assertFalse(viewModel.uiState.value.validationError)
@@ -296,8 +314,11 @@ class SignUpViewModelTest {
preferencesManager = preferencesManager,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
courseId = "",
+ infoType = "",
)
val deferred = async { viewModel.uiMessage.first() }
@@ -307,7 +328,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)
}
@@ -320,8 +341,11 @@ class SignUpViewModelTest {
preferencesManager = preferencesManager,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
courseId = "",
+ infoType = "",
)
val deferred = async { viewModel.uiMessage.first() }
@@ -331,7 +355,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)
}
@@ -344,8 +368,11 @@ class SignUpViewModelTest {
preferencesManager = preferencesManager,
appUpgradeNotifier = appUpgradeNotifier,
oAuthHelper = oAuthHelper,
+ agreementProvider = agreementProvider,
config = config,
+ router = router,
courseId = "",
+ infoType = "",
)
coEvery { interactor.getRegistrationFields() } returns listOfFields
viewModel.getRegistrationFields()
diff --git a/build.gradle b/build.gradle
index 462369006..6ebb49a56 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,31 +5,31 @@ import java.util.regex.Pattern
buildscript {
ext {
- kotlin_version = '1.8.21'
+ kotlin_version = '1.9.22'
coroutines_version = '1.7.1'
- compose_version = '1.5.0'
- compose_compiler_version = '1.4.7'
+ compose_version = '1.6.2'
+ compose_compiler_version = '1.5.10'
}
}
plugins {
- id 'com.android.application' version '8.1.1' apply false
- id 'com.android.library' version '8.1.1' apply false
+ id 'com.android.application' version '8.3.0' apply false
+ id 'com.android.library' version '8.3.0' apply false
id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
id 'com.google.gms.google-services' version '4.3.15' apply false
id "com.google.firebase.crashlytics" version "2.9.6" apply false
}
-task clean(type: Delete) {
- delete rootProject.buildDir
+tasks.register('clean', Delete) {
+ delete rootProject.layout.buildDirectory
}
ext {
core_version = "1.10.1"
appcompat_version = "1.6.1"
- material_version = "1.9.0"
- lifecycle_version = "2.6.1"
- fragment_version = "1.6.1"
+ material_version = "1.11.0"
+ lifecycle_version = "2.7.0"
+ fragment_version = "1.6.2"
constraintlayout_version = "2.1.4"
viewpager2_version = "1.0.0"
media3_version = "1.1.1"
@@ -46,11 +46,11 @@ ext {
jsoup_version = '1.13.1'
- room_version = '2.5.2'
+ room_version = '2.6.1'
- work_version = '2.8.1'
+ work_version = '2.9.0'
- window_version = '1.1.0'
+ window_version = '1.2.0'
in_app_review = '2.0.1'
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 9cc60ef40..f1d8de5cb 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -1,7 +1,16 @@
+plugins {
+ id 'java-library'
+}
+
repositories {
mavenCentral()
}
+java {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+}
+
dependencies {
implementation localGroovy()
implementation gradleApi()
diff --git a/buildSrc/src/main/groovy/org/edx/builder/ConfigHelper.groovy b/buildSrc/src/main/groovy/org/edx/builder/ConfigHelper.groovy
index ed78319bc..10ca3e8db 100644
--- a/buildSrc/src/main/groovy/org/edx/builder/ConfigHelper.groovy
+++ b/buildSrc/src/main/groovy/org/edx/builder/ConfigHelper.groovy
@@ -95,4 +95,57 @@ class ConfigHelper {
it.write(new JsonBuilder(configJson).toPrettyString())
}
}
+
+ def generateGoogleServicesJson(applicationId) {
+ def config = fetchConfig()
+ def firebase = config.get("FIREBASE")
+ if (!firebase) {
+ return
+ }
+ if (!firebase.getOrDefault("ENABLED", false)) {
+ return
+ }
+
+ def googleServicesJsonPath = projectDir.path + "/app/"
+ new File(googleServicesJsonPath).mkdirs()
+
+ def projectInfo = [
+ project_number: firebase.getOrDefault("PROJECT_NUMBER", ""),
+ project_id : firebase.getOrDefault("PROJECT_ID", ""),
+ storage_bucket: "${firebase.getOrDefault("PROJECT_ID", "")}.appspot.com"
+ ]
+ def clientInfo = [
+ mobilesdk_app_id : firebase.getOrDefault("APPLICATION_ID", ""),
+ android_client_info: [
+ package_name: applicationId
+ ]
+ ]
+ def client = [
+ client_info : clientInfo,
+ oauth_client: [],
+ api_key : [[current_key: firebase.getOrDefault("API_KEY", "")]],
+ services : [
+ appinvite_service: [
+ other_platform_oauth_client: []
+ ]
+ ]
+ ]
+ def configJson = [
+ project_info : projectInfo,
+ client : [client],
+ configuration_version: "1"
+ ]
+
+ new FileWriter(googleServicesJsonPath + "/google-services.json").withWriter {
+ it.write(new JsonBuilder(configJson).toPrettyString())
+ }
+ }
+
+ def removeGoogleServicesJson() {
+ def googleServicesJsonPath = projectDir.path + "/app/google-services.json"
+ def file = new File(googleServicesJsonPath)
+ if (file.exists()) {
+ file.delete()
+ }
+ }
}
diff --git a/catalog-info.yaml b/catalog-info.yaml
new file mode 100644
index 000000000..87c6bc3ad
--- /dev/null
+++ b/catalog-info.yaml
@@ -0,0 +1,16 @@
+# This file records information about this repo. Its use is described in OEP-55:
+# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
+
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: 'openedx-app-android'
+ description: "The mobile app for Android for the Open EdX Platform"
+ links:
+ - url: "https://github.com/openedx/openedx-app-android/tree/main/Documentation"
+ title: "Documentation"
+ icon: "PhoneAndroid"
+spec:
+ owner: group:openedx-mobile-maintainers
+ type: 'mobile'
+ lifecycle: 'production'
diff --git a/core/build.gradle b/core/build.gradle
index 93928bfc8..8c4bdcc6f 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -85,6 +85,7 @@ android {
buildFeatures {
viewBinding true
compose true
+ buildConfig true
}
composeOptions {
kotlinCompilerExtensionVersion = "$compose_compiler_version"
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/AppDataConstants.kt b/core/src/main/java/org/openedx/core/AppDataConstants.kt
index 0bb5a95d0..eb2580e99 100644
--- a/core/src/main/java/org/openedx/core/AppDataConstants.kt
+++ b/core/src/main/java/org/openedx/core/AppDataConstants.kt
@@ -10,4 +10,7 @@ object AppDataConstants {
const val VIDEO_FORMAT_M3U8 = ".m3u8"
const val VIDEO_FORMAT_MP4 = ".mp4"
-}
\ No newline at end of file
+
+ // Equal 1GB
+ const val DOWNLOADS_CONFIRMATION_SIZE = 1024 * 1024 * 1024L
+}
diff --git a/core/src/main/java/org/openedx/core/UIMessage.kt b/core/src/main/java/org/openedx/core/UIMessage.kt
index 6f26d6f65..8a9267f36 100644
--- a/core/src/main/java/org/openedx/core/UIMessage.kt
+++ b/core/src/main/java/org/openedx/core/UIMessage.kt
@@ -2,11 +2,11 @@ package org.openedx.core
import androidx.compose.material.SnackbarDuration
-sealed class UIMessage {
+open class UIMessage {
class SnackBarMessage(
val message: String,
val duration: SnackbarDuration = SnackbarDuration.Long,
) : UIMessage()
class ToastMessage(val message: String) : UIMessage()
-}
\ No newline at end of file
+}
diff --git a/core/src/main/java/org/openedx/core/config/AnalyticsSource.kt b/core/src/main/java/org/openedx/core/config/AnalyticsSource.kt
new file mode 100644
index 000000000..b3ee82211
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/config/AnalyticsSource.kt
@@ -0,0 +1,11 @@
+package org.openedx.core.config
+
+import com.google.gson.annotations.SerializedName
+
+enum class AnalyticsSource {
+ @SerializedName("segment")
+ SEGMENT,
+
+ @SerializedName("none")
+ NONE,
+}
diff --git a/core/src/main/java/org/openedx/core/config/BranchConfig.kt b/core/src/main/java/org/openedx/core/config/BranchConfig.kt
new file mode 100644
index 000000000..363e89fa6
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/config/BranchConfig.kt
@@ -0,0 +1,20 @@
+package org.openedx.core.config
+
+import com.google.gson.annotations.SerializedName
+
+data class BranchConfig(
+ @SerializedName("ENABLED")
+ val enabled: Boolean = false,
+
+ @SerializedName("KEY")
+ val key: String = "",
+
+ @SerializedName("URI_SCHEME")
+ val uriScheme: String = "",
+
+ @SerializedName("HOST")
+ val host: String = "",
+
+ @SerializedName("ALTERNATE_HOST")
+ val alternateHost: String = "",
+)
diff --git a/core/src/main/java/org/openedx/core/config/BrazeConfig.kt b/core/src/main/java/org/openedx/core/config/BrazeConfig.kt
new file mode 100644
index 000000000..62bb2e9be
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/config/BrazeConfig.kt
@@ -0,0 +1,11 @@
+package org.openedx.core.config
+
+import com.google.gson.annotations.SerializedName
+
+data class BrazeConfig(
+ @SerializedName("ENABLED")
+ val isEnabled: Boolean = false,
+
+ @SerializedName("PUSH_NOTIFICATIONS_ENABLED")
+ val isPushNotificationsEnabled: Boolean = false
+)
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 8739c4cfa..9f626cc2e 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()
@@ -57,6 +61,14 @@ class Config(context: Context) {
return getObjectOrNewInstance(FIREBASE, FirebaseConfig::class.java)
}
+ fun getSegmentConfig(): SegmentConfig {
+ return getObjectOrNewInstance(SEGMENT_IO, SegmentConfig::class.java)
+ }
+
+ fun getBrazeConfig(): BrazeConfig {
+ return getObjectOrNewInstance(BRAZE, BrazeConfig::class.java)
+ }
+
fun getFacebookConfig(): FacebookConfig {
return getObjectOrNewInstance(FACEBOOK, FacebookConfig::class.java)
}
@@ -79,6 +91,10 @@ class Config(context: Context) {
return getObjectOrNewInstance(PROGRAM, ProgramConfig::class.java)
}
+ fun getBranchConfig(): BranchConfig {
+ return getObjectOrNewInstance(BRANCH, BranchConfig::class.java)
+ }
+
fun isWhatsNewEnabled(): Boolean {
return getBoolean(WHATS_NEW_ENABLED, false)
}
@@ -148,16 +164,20 @@ class Config(context: Context) {
private const val WHATS_NEW_ENABLED = "WHATS_NEW_ENABLED"
private const val SOCIAL_AUTH_ENABLED = "SOCIAL_AUTH_ENABLED"
private const val FIREBASE = "FIREBASE"
+ private const val SEGMENT_IO = "SEGMENT_IO"
+ private const val BRAZE = "BRAZE"
private const val FACEBOOK = "FACEBOOK"
private const val GOOGLE = "GOOGLE"
private const val MICROSOFT = "MICROSOFT"
private const val PRE_LOGIN_EXPERIENCE_ENABLED = "PRE_LOGIN_EXPERIENCE_ENABLED"
private const val DISCOVERY = "DISCOVERY"
private const val PROGRAM = "PROGRAM"
+ private const val BRANCH = "BRANCH"
private const val COURSE_NESTED_LIST_ENABLED = "COURSE_NESTED_LIST_ENABLED"
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/config/FirebaseConfig.kt b/core/src/main/java/org/openedx/core/config/FirebaseConfig.kt
index b003c3230..f5b2e9136 100644
--- a/core/src/main/java/org/openedx/core/config/FirebaseConfig.kt
+++ b/core/src/main/java/org/openedx/core/config/FirebaseConfig.kt
@@ -6,6 +6,15 @@ data class FirebaseConfig(
@SerializedName("ENABLED")
val enabled: Boolean = false,
+ @SerializedName("ANALYTICS_SOURCE")
+ val analyticsSource: AnalyticsSource = AnalyticsSource.NONE,
+
+ @SerializedName("CLOUD_MESSAGING_ENABLED")
+ val isCloudMessagingEnabled: Boolean = false,
+
+ @SerializedName("PROJECT_NUMBER")
+ val projectNumber: String = "",
+
@SerializedName("PROJECT_ID")
val projectId: String = "",
@@ -14,7 +23,8 @@ data class FirebaseConfig(
@SerializedName("API_KEY")
val apiKey: String = "",
-
- @SerializedName("GCM_SENDER_ID")
- val gcmSenderId: String = "",
-)
+) {
+ fun isSegmentAnalyticsSource(): Boolean {
+ return enabled && analyticsSource == AnalyticsSource.SEGMENT
+ }
+}
diff --git a/core/src/main/java/org/openedx/core/config/SegmentConfig.kt b/core/src/main/java/org/openedx/core/config/SegmentConfig.kt
new file mode 100644
index 000000000..ffa43e8bc
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/config/SegmentConfig.kt
@@ -0,0 +1,11 @@
+package org.openedx.core.config
+
+import com.google.gson.annotations.SerializedName
+
+data class SegmentConfig(
+ @SerializedName("ENABLED")
+ val enabled: Boolean = false,
+
+ @SerializedName("SEGMENT_IO_WRITE_KEY")
+ val segmentWriteKey: String = "",
+)
diff --git a/core/src/main/java/org/openedx/core/data/api/CourseApi.kt b/core/src/main/java/org/openedx/core/data/api/CourseApi.kt
index 64e749a26..4a19c383d 100644
--- a/core/src/main/java/org/openedx/core/data/api/CourseApi.kt
+++ b/core/src/main/java/org/openedx/core/data/api/CourseApi.kt
@@ -1,8 +1,20 @@
package org.openedx.core.data.api
-import okhttp3.ResponseBody
-import org.openedx.core.data.model.*
-import retrofit2.http.*
+import org.openedx.core.data.model.AnnouncementModel
+import org.openedx.core.data.model.BlocksCompletionBody
+import org.openedx.core.data.model.CourseComponentStatus
+import org.openedx.core.data.model.CourseDates
+import org.openedx.core.data.model.CourseDatesBannerInfo
+import org.openedx.core.data.model.CourseEnrollments
+import org.openedx.core.data.model.CourseStructureModel
+import org.openedx.core.data.model.HandoutsModel
+import org.openedx.core.data.model.ResetCourseDates
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.POST
+import retrofit2.http.Path
+import retrofit2.http.Query
interface CourseApi {
@@ -14,27 +26,6 @@ interface CourseApi {
@Query("page") page: Int
): CourseEnrollments
- @GET("/api/courses/v1/courses/")
- suspend fun getCourseList(
- @Query("search_term") searchQuery: String? = null,
- @Query("page") page: Int,
- @Query("mobile") mobile: Boolean,
- @Query("mobile_search") mobileSearch: Boolean,
- @Query("username") username: String? = null,
- @Query("org") org: String? = null,
- @Query("permissions") permission: List = listOf(
- "enroll",
- "see_in_catalog",
- "see_about_page"
- )
- ): CourseList
-
- @GET("/api/courses/v1/courses/{course_id}")
- suspend fun getCourseDetail(
- @Path("course_id") courseId: String?,
- @Query("username") username: String? = null
- ): CourseDetails
-
@GET(
"/api/mobile/{api_version}/course_info/blocks/?" +
"depth=all&" +
@@ -50,9 +41,6 @@ interface CourseApi {
@Query("course_id") courseId: String,
): CourseStructureModel
- @POST("/api/enrollment/v1/enrollment")
- suspend fun enrollInACourse(@Body enrollBody: EnrollBody): ResponseBody
-
@GET("/api/mobile/v1/users/{username}/course_status_info/{course_id}")
suspend fun getCourseStatus(
@Path("username") username: String,
diff --git a/core/src/main/java/org/openedx/core/data/model/AppConfig.kt b/core/src/main/java/org/openedx/core/data/model/AppConfig.kt
new file mode 100644
index 000000000..4fcbe3d89
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/data/model/AppConfig.kt
@@ -0,0 +1,15 @@
+package org.openedx.core.data.model
+
+import com.google.gson.annotations.SerializedName
+import org.openedx.core.domain.model.AppConfig as DomainAppConfig
+
+data class AppConfig(
+ @SerializedName("course_dates_calendar_sync")
+ val calendarSyncConfig: CalendarSyncConfig = CalendarSyncConfig(),
+) {
+ fun mapToDomain(): DomainAppConfig {
+ return DomainAppConfig(
+ courseDatesCalendarSync = calendarSyncConfig.mapToDomain(),
+ )
+ }
+}
diff --git a/core/src/main/java/org/openedx/core/data/model/CalendarSyncConfig.kt b/core/src/main/java/org/openedx/core/data/model/CalendarSyncConfig.kt
new file mode 100644
index 000000000..bfd09b3d3
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/data/model/CalendarSyncConfig.kt
@@ -0,0 +1,29 @@
+package org.openedx.core.data.model
+
+import com.google.gson.annotations.SerializedName
+import org.openedx.core.domain.model.CourseDatesCalendarSync
+
+data class CalendarSyncConfig(
+ @SerializedName("android")
+ val platformConfig: CalendarSyncPlatform = CalendarSyncPlatform(),
+) {
+ fun mapToDomain(): CourseDatesCalendarSync {
+ return CourseDatesCalendarSync(
+ isEnabled = platformConfig.enabled,
+ isSelfPacedEnabled = platformConfig.selfPacedEnabled,
+ isInstructorPacedEnabled = platformConfig.instructorPacedEnabled,
+ isDeepLinkEnabled = platformConfig.deepLinksEnabled,
+ )
+ }
+}
+
+data class CalendarSyncPlatform(
+ @SerializedName("enabled")
+ val enabled: Boolean = false,
+ @SerializedName("self_paced_enabled")
+ val selfPacedEnabled: Boolean = false,
+ @SerializedName("instructor_paced_enabled")
+ val instructorPacedEnabled: Boolean = false,
+ @SerializedName("deep_links_enabled")
+ val deepLinksEnabled: Boolean = false,
+)
diff --git a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt
index 1c10cfa92..89ecdcab4 100644
--- a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt
+++ b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt
@@ -1,8 +1,69 @@
package org.openedx.core.data.model
+import com.google.gson.Gson
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
import com.google.gson.annotations.SerializedName
+import java.lang.reflect.Type
data class CourseEnrollments(
@SerializedName("enrollments")
- val enrollments: DashboardCourseList
-)
+ val enrollments: DashboardCourseList,
+
+ @SerializedName("config")
+ val configs: AppConfig,
+) {
+ class Deserializer : JsonDeserializer {
+ override fun deserialize(
+ json: JsonElement?,
+ typeOfT: Type?,
+ context: JsonDeserializationContext?
+ ): CourseEnrollments {
+ val enrollments = deserializeEnrollments(json)
+ val appConfig = deserializeAppConfig(json)
+
+ return CourseEnrollments(enrollments, appConfig)
+ }
+
+ private fun deserializeEnrollments(json: JsonElement?): DashboardCourseList {
+ return try {
+ Gson().fromJson(
+ (json as JsonObject).get("enrollments"),
+ DashboardCourseList::class.java
+ )
+ } catch (ex: Exception) {
+ DashboardCourseList(
+ next = null,
+ previous = null,
+ count = 0,
+ numPages = 0,
+ currentPage = 0,
+ results = listOf()
+ )
+ }
+ }
+
+ /**
+ * To remove dependency on the backend, all the data related to Remote Config
+ * will be received under the `configs` key. The `config` is the key under
+ * 'configs` which defines the data that is related to the configuration of the
+ * app.
+ */
+ private fun deserializeAppConfig(json: JsonElement?): AppConfig {
+ return try {
+ val config = (json as JsonObject)
+ .getAsJsonObject("configs")
+ .getAsJsonPrimitive("config")
+
+ Gson().fromJson(
+ config.asString,
+ AppConfig::class.java
+ )
+ } catch (ex: Exception) {
+ AppConfig()
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt
index 11f21c661..48999ab4e 100644
--- a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt
+++ b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt
@@ -1,6 +1,7 @@
package org.openedx.core.data.storage
import org.openedx.core.data.model.User
+import org.openedx.core.domain.model.AppConfig
import org.openedx.core.domain.model.VideoSettings
interface CorePreferences {
@@ -9,6 +10,7 @@ interface CorePreferences {
var accessTokenExpiresAt: Long
var user: User?
var videoSettings: VideoSettings
+ var appConfig: AppConfig
fun clear()
}
diff --git a/core/src/main/java/org/openedx/core/domain/model/AppConfig.kt b/core/src/main/java/org/openedx/core/domain/model/AppConfig.kt
new file mode 100644
index 000000000..596fd0619
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/domain/model/AppConfig.kt
@@ -0,0 +1,14 @@
+package org.openedx.core.domain.model
+
+import java.io.Serializable
+
+data class AppConfig(
+ val courseDatesCalendarSync: CourseDatesCalendarSync,
+) : Serializable
+
+data class CourseDatesCalendarSync(
+ val isEnabled: Boolean,
+ val isSelfPacedEnabled: Boolean,
+ val isInstructorPacedEnabled: Boolean,
+ val isDeepLinkEnabled: Boolean,
+) : Serializable
diff --git a/core/src/main/java/org/openedx/core/domain/model/Block.kt b/core/src/main/java/org/openedx/core/domain/model/Block.kt
index 1c69142a8..2f1766ecb 100644
--- a/core/src/main/java/org/openedx/core/domain/model/Block.kt
+++ b/core/src/main/java/org/openedx/core/domain/model/Block.kt
@@ -117,13 +117,19 @@ data class EncodedVideos(
|| fallback?.url != null
val videoUrl: String
- get() = mobileHigh?.url
- ?: mobileLow?.url
- ?: desktopMp4?.url
+ get() = fallback?.url
?: hls?.url
- ?: fallback?.url
+ ?: desktopMp4?.url
+ ?: mobileHigh?.url
+ ?: mobileLow?.url
?: ""
+ val hasVideoUrl: Boolean
+ get() = videoUrl.isNotEmpty()
+
+ val hasYoutubeUrl: Boolean
+ get() = youtube?.url?.isNotEmpty() == true
+
fun getPreferredVideoInfoForDownloading(preferredVideoQuality: VideoQuality): VideoInfo? {
var preferredVideoInfo = when (preferredVideoQuality) {
VideoQuality.OPTION_360P -> mobileLow
diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseList.kt b/core/src/main/java/org/openedx/core/domain/model/CourseList.kt
deleted file mode 100644
index 6c38ef924..000000000
--- a/core/src/main/java/org/openedx/core/domain/model/CourseList.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.openedx.core.domain.model
-
-data class CourseList(
- val pagination: Pagination,
- val results: List,
-)
\ No newline at end of file
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