Skip to content

Commit

Permalink
feat: added ability to handle course errors
Browse files Browse the repository at this point in the history
- Integrate and parse CourseEnrollmentDetails API
- Handle CourseAccess Errors on course Dashboard
- Update UI based on CourseAccess Errors.

fix:LEARNER-10019
  • Loading branch information
omerhabib26 committed Jul 25, 2024
1 parent 12056d2 commit f723215
Show file tree
Hide file tree
Showing 50 changed files with 924 additions and 277 deletions.
11 changes: 7 additions & 4 deletions app/src/main/java/org/openedx/app/AppRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,10 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
fm: FragmentManager,
courseId: String,
courseTitle: String,
enrollmentMode: String,
) {
replaceFragmentWithBackStack(
fm,
CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode)
CourseContainerFragment.newInstance(courseId, courseTitle)
)
}
//endregion
Expand All @@ -164,7 +163,6 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
fm: FragmentManager,
courseId: String,
courseTitle: String,
enrollmentMode: String,
openTab: String,
resumeBlockId: String,
) {
Expand All @@ -173,7 +171,6 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
CourseContainerFragment.newInstance(
courseId,
courseTitle,
enrollmentMode,
openTab,
resumeBlockId
)
Expand Down Expand Up @@ -397,6 +394,12 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
replaceFragmentWithBackStack(fm, VideoQualityFragment.newInstance(videoQualityType.name))
}

override fun navigateToDiscover(fm: FragmentManager) {
fm.beginTransaction()
.replace(R.id.container, MainFragment.newInstance("", "", "DISCOVER"))
.commit()
}

override fun navigateToWebContent(fm: FragmentManager, title: String, url: String) {
replaceFragmentWithBackStack(
fm,
Expand Down
5 changes: 0 additions & 5 deletions app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,6 @@ class DeepLinkRouter(
fm = fm,
courseId = courseId,
courseTitle = courseTitle,
enrollmentMode = ""
)
}
}
Expand All @@ -311,7 +310,6 @@ class DeepLinkRouter(
fm = fm,
courseId = courseId,
courseTitle = "",
enrollmentMode = "",
openTab = "VIDEOS"
)
}
Expand All @@ -323,7 +321,6 @@ class DeepLinkRouter(
fm = fm,
courseId = courseId,
courseTitle = "",
enrollmentMode = "",
openTab = "DATES"
)
}
Expand All @@ -335,7 +332,6 @@ class DeepLinkRouter(
fm = fm,
courseId = courseId,
courseTitle = "",
enrollmentMode = "",
openTab = "DISCUSSIONS"
)
}
Expand All @@ -347,7 +343,6 @@ class DeepLinkRouter(
fm = fm,
courseId = courseId,
courseTitle = "",
enrollmentMode = "",
openTab = "MORE"
)
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/org/openedx/app/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.openedx.auth.presentation.sso.MicrosoftAuthHelper
import org.openedx.auth.presentation.sso.OAuthHelper
import org.openedx.core.ImageProcessor
import org.openedx.core.config.Config
import org.openedx.core.data.model.CourseEnrollmentDetails
import org.openedx.core.data.model.CourseEnrollments
import org.openedx.core.data.model.CourseStructureModel
import org.openedx.core.data.storage.CorePreferences
Expand Down Expand Up @@ -97,6 +98,10 @@ val appModule = module {
CourseStructureModel::class.java,
CourseStructureModel.Deserializer(get())
)
.registerTypeAdapter(
CourseEnrollmentDetails::class.java,
CourseEnrollmentDetails.Deserializer(get())
)
.create()
}

Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,11 @@ val screenModule = module {
get()
)
}
viewModel { (courseId: String, courseTitle: String, enrollmentMode: String, resumeBlockId: String) ->
viewModel { (courseId: String, courseTitle: String, resumeBlockId: String) ->
CourseContainerViewModel(
courseId,
courseTitle,
resumeBlockId,
enrollmentMode,
get(),
get(),
get(),
Expand Down
2 changes: 1 addition & 1 deletion core/src/edx/org/openedx/core/ui/theme/Colors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ val light_course_home_header_shade = Color(0xFFBABABA)
val light_course_home_back_btn_background = light_surface
val light_settings_title_content = light_surface
val light_progress_bar_color = light_primary_button_background
val light_progress_bar_background_color = light_secondary_variant
val light_progress_bar_background_color = Color(0xFFCCD4E0)


// Dark theme colors scheme
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/java/org/openedx/core/data/api/CourseApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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.CourseEnrollmentDetails
import org.openedx.core.data.model.CourseEnrollments
import org.openedx.core.data.model.CourseStructureModel
import org.openedx.core.data.model.HandoutsModel
Expand Down Expand Up @@ -76,4 +77,9 @@ interface CourseApi {
@Query("status") status: String? = null,
@Query("requested_fields") fields: List<String> = emptyList()
): CourseEnrollments

@GET("/api/mobile/v1/course_info/{course_id}/enrollment_details")
suspend fun getEnrollmentDetails(
@Path("course_id") courseId: String,
): CourseEnrollmentDetails
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,31 @@ import org.openedx.core.utils.TimeUtils
import org.openedx.core.domain.model.CourseAccessDetails as DomainCourseAccessDetails

data class CourseAccessDetails(
@SerializedName("has_unmet_prerequisites")
val hasUnmetPrerequisites: Boolean,
@SerializedName("is_too_early")
val isTooEarly: Boolean,
@SerializedName("is_staff")
val isStaff: Boolean,
@SerializedName("audit_access_expires")
val auditAccessExpires: String?,
@SerializedName("courseware_access")
var coursewareAccess: CoursewareAccess?,
) {
fun mapToDomain(): DomainCourseAccessDetails =
DomainCourseAccessDetails(
TimeUtils.iso8601ToDate(auditAccessExpires ?: ""),
coursewareAccess?.mapToDomain()
)
fun mapToDomain() = DomainCourseAccessDetails(
hasUnmetPrerequisites = hasUnmetPrerequisites,
isTooEarly = isTooEarly,
isStaff = isStaff,
auditAccessExpires = TimeUtils.iso8601ToDate(auditAccessExpires ?: ""),
coursewareAccess = coursewareAccess?.mapToDomain(),
)

fun mapToRoomEntity(): CourseAccessDetailsDb =
CourseAccessDetailsDb(auditAccessExpires, coursewareAccess?.mapToRoomEntity())
CourseAccessDetailsDb(
hasUnmetPrerequisites = hasUnmetPrerequisites,
isTooEarly = isTooEarly,
isStaff = isStaff,
auditAccessExpires = auditAccessExpires,
coursewareAccess = coursewareAccess?.mapToRoomEntity()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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.annotations.SerializedName
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.extension.takeIfNotEmpty
import java.lang.reflect.Type
import org.openedx.core.domain.model.CourseEnrollmentDetails as DomainCourseEnrollmentDetails

data class CourseEnrollmentDetails(
@SerializedName("id")
val id: String,
@SerializedName("course_updates")
val courseUpdates: String,
@SerializedName("course_handouts")
val courseHandouts: String,
@SerializedName("discussion_url")
val discussionUrl: String,
@SerializedName("course_access_details")
val courseAccessDetails: CourseAccessDetails,
@SerializedName("certificate")
val certificate: Certificate?,
@SerializedName("enrollment_details")
val enrollmentDetails: EnrollmentDetails,
@SerializedName("course_info_overview")
val courseInfoOverview: CourseInfoOverview,
) {
fun mapToDomain(): DomainCourseEnrollmentDetails {
return DomainCourseEnrollmentDetails(
id = id,
courseUpdates = courseUpdates,
courseHandouts = courseHandouts,
discussionUrl = discussionUrl,
courseAccessDetails = courseAccessDetails.mapToDomain(),
certificate = certificate?.mapToDomain(),
enrollmentDetails = enrollmentDetails.mapToDomain(),
courseInfoOverview = courseInfoOverview.mapToDomain(),
)
}

class Deserializer(val corePreferences: CorePreferences) :
JsonDeserializer<CourseEnrollmentDetails> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?,
): CourseEnrollmentDetails {
val courseDetails = Gson().fromJson(json, CourseEnrollmentDetails::class.java)
corePreferences.appConfig.iapConfig.productPrefix?.takeIfNotEmpty()?.let {
courseDetails.courseInfoOverview.courseModes?.forEach { courseModes ->
courseModes.setStoreProductSku(it)
}
}
return courseDetails
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.openedx.core.data.model

import com.google.gson.annotations.SerializedName
import org.openedx.core.domain.model.iap.ProductInfo
import org.openedx.core.extension.isNotNullOrEmpty
import org.openedx.core.utils.TimeUtils
import org.openedx.core.domain.model.CourseInfoOverview as DomainCourseInfoOverview

data class CourseInfoOverview(
@SerializedName("name")
val name: String,
@SerializedName("number")
val number: String,
@SerializedName("org")
val org: String,
@SerializedName("start")
val start: String?,
@SerializedName("start_display")
val startDisplay: String,
@SerializedName("start_type")
val startType: String,
@SerializedName("end")
val end: String?,
@SerializedName("is_self_paced")
val isSelfPaced: Boolean,
@SerializedName("media")
var media: Media?,
@SerializedName("course_sharing_utm_parameters")
val courseSharingUtmParameters: CourseSharingUtmParameters,
@SerializedName("course_about")
val courseAbout: String,
@SerializedName("course_modes")
val courseModes: List<CourseMode>?,
) {
fun mapToDomain() = DomainCourseInfoOverview(
name = name,
number = number,
org = org,
start = TimeUtils.iso8601ToDate(start ?: ""),
startDisplay = startDisplay,
startType = startType,
end = TimeUtils.iso8601ToDate(end ?: ""),
isSelfPaced = isSelfPaced,
media = media?.mapToDomain(),
courseSharingUtmParameters = courseSharingUtmParameters.mapToDomain(),
courseAbout = courseAbout,
courseModes = courseModes?.map { it.mapToDomain() },
productInfo = courseModes?.find {
it.isVerifiedMode()
}?.takeIf {
it.androidSku.isNotNullOrEmpty() && it.storeSku.isNotNullOrEmpty()
}?.run {
ProductInfo(
courseSku = androidSku!!,
storeSku = storeSku!!,
lmsUSDPrice = minPrice ?: 0.0
)
}
)
}
25 changes: 19 additions & 6 deletions core/src/main/java/org/openedx/core/data/model/CourseMode.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.openedx.core.data.model

import com.google.gson.annotations.SerializedName
import org.openedx.core.domain.model.EnrollmentMode
import kotlin.math.ceil
import org.openedx.core.domain.model.CourseMode as DomainCourseMode

/**
* Data class representing the mode of a course ("audit, verified etc"), with various attributes
Expand All @@ -10,20 +12,31 @@ import kotlin.math.ceil
data class CourseMode(
@SerializedName("slug")
val slug: String?,

@SerializedName("sku")
val sku: String?,

@SerializedName("android_sku")
val androidSku: String?,

@SerializedName("ios_sku")
val iosSku: String?,
@SerializedName("min_price")
val price: Double?,

val minPrice: Double?,
var storeSku: String?,
) {
fun mapToDomain() = DomainCourseMode(
slug = slug,
sku = sku,
androidSku = androidSku,
iosSku = iosSku,
minPrice = minPrice,
storeSku = storeSku
)

fun isVerifiedMode(): Boolean {
return EnrollmentMode.VERIFIED.toString().equals(slug, ignoreCase = true)
}

fun setStoreProductSku(storeProductPrefix: String) {
val ceilPrice = price
val ceilPrice = minPrice
?.let { ceil(it).toInt() }
?.takeIf { it > 0 }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import org.openedx.core.data.model.room.MediaDb
import org.openedx.core.data.model.room.discovery.ProgressDb
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.CourseStructure
import org.openedx.core.domain.model.EnrollmentMode
import org.openedx.core.domain.model.iap.ProductInfo
import org.openedx.core.extension.isNotNullOrEmpty
import org.openedx.core.utils.TimeUtils
import java.lang.reflect.Type

Expand Down Expand Up @@ -73,14 +73,14 @@ data class CourseStructureModel(
progress = progress?.mapToDomain(),
enrollmentDetails = enrollmentDetails.mapToDomain(),
productInfo = courseModes?.find {
EnrollmentMode.VERIFIED.toString().equals(it.slug, ignoreCase = true)
it.isVerifiedMode()
}?.takeIf {
it.androidSku.isNullOrEmpty().not() && it.storeSku.isNullOrEmpty().not()
it.androidSku.isNotNullOrEmpty() && it.storeSku.isNotNullOrEmpty()
}?.run {
ProductInfo(
courseSku = androidSku!!,
storeSku = storeSku!!,
lmsUSDPrice = price ?: 0.0
lmsUSDPrice = minPrice ?: 0.0
)
}
)
Expand Down Expand Up @@ -112,7 +112,7 @@ data class CourseStructureModel(
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
context: JsonDeserializationContext?,
): CourseStructureModel {
val courseStructure = Gson().fromJson(json, CourseStructureModel::class.java)
if (corePreferences.appConfig.iapConfig.productPrefix.isNullOrEmpty().not()) {
Expand Down
Loading

0 comments on commit f723215

Please sign in to comment.