Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/main/java/org/openedx/app/room/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ 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 = 2
const val DATABASE_VERSION = 3
const val DATABASE_NAME = "OpenEdX_db"

@Database(
Expand Down
19 changes: 17 additions & 2 deletions app/src/main/java/org/openedx/app/room/DatabaseMigrations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ val MIGRATION_1_2 = object : Migration(1, 2) {
}
}

/**
* Migration from version 2 to version 3
*
* Changes:
* - Adds new field to course_discovery_table:
* - purchaseLink: URL to purchase paid courses
*/
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
// Add purchaseLink column to course_discovery_table (nullable field)
db.execSQL("ALTER TABLE course_discovery_table ADD COLUMN purchaseLink TEXT DEFAULT NULL")
}
}

/**
* All migrations in chronological order.
* This array is used by the database builder to apply migrations automatically.
Expand All @@ -48,9 +62,10 @@ val MIGRATION_1_2 = object : Migration(1, 2) {
* - Add it to this array: arrayOf(MIGRATION_1_2, MIGRATION_2_3, ...)
*/
val ALL_MIGRATIONS = arrayOf(
MIGRATION_1_2
MIGRATION_1_2,
MIGRATION_2_3
// Add future migrations here, e.g.:
// MIGRATION_2_3,
// MIGRATION_3_4,
// MIGRATION_4_5,
)

Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ data class CourseDetails(
val learningOutcomes: List<String>?,
@SerializedName("instructors")
val instructors: List<Instructor>?,
@SerializedName("purchase_link")
val purchaseLink: String?,
) {

fun mapToDomain(): Course {
Expand Down Expand Up @@ -87,7 +89,8 @@ data class CourseDetails(
courseRequirement = courseRequirement.orEmpty(),
description = description.orEmpty(),
learningOutcomes = learningOutcomes?.joinToString(", ") ?: "",
instructorsList = instructors ?: emptyList()
instructorsList = instructors ?: emptyList(),
purchaseLink = purchaseLink
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ data class CourseEntity(
val learningOutcomes: String,
@ColumnInfo("instructors")
val instructors: String,
@ColumnInfo("purchaseLink")
val purchaseLink: String?,
) {

fun mapToDomain(): Course {
Expand Down Expand Up @@ -93,7 +95,8 @@ data class CourseEntity(
courseRequirement = courseRequirement,
description = description,
learningOutcomes = learningOutcomes,
instructorsList = emptyList()
instructorsList = emptyList(),
purchaseLink = purchaseLink
)
}

Expand Down Expand Up @@ -126,6 +129,7 @@ data class CourseEntity(
description = model.description.orEmpty(),
learningOutcomes = model.learningOutcomes?.joinToString(", ") ?: "",
instructors = model.instructors?.mapNotNull { it.name }?.joinToString(", ") ?: "",
purchaseLink = model.purchaseLink
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ data class Course(
val courseRequirement: String,
val description: String,
val learningOutcomes: String,
val instructorsList: List<org.openedx.discovery.data.model.Instructor> = emptyList()
val instructorsList: List<org.openedx.discovery.data.model.Instructor> = emptyList(),
val purchaseLink: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Assignment
import androidx.compose.material.icons.automirrored.filled.Rule
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Domain
import androidx.compose.material.icons.filled.Group
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.material.icons.filled.TrackChanges
import androidx.compose.material.icons.outlined.Report
import androidx.compose.material.rememberScaffoldState
Expand Down Expand Up @@ -82,14 +84,17 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.zIndex
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import coil.compose.AsyncImage
Expand All @@ -108,6 +113,7 @@ import org.openedx.core.ui.isPreview
import org.openedx.core.ui.statusBarsInset
import org.openedx.core.ui.theme.OpenEdXTheme
import org.openedx.core.ui.theme.appColors
import org.openedx.core.ui.theme.appShapes
import org.openedx.core.ui.theme.appTypography
import org.openedx.core.utils.EmailUtil
import org.openedx.discovery.R
Expand All @@ -133,6 +139,13 @@ class CourseDetailsFragment : Fragment() {
}
private val router by inject<DiscoveryRouter>()

override fun onResume() {
super.onResume()
// Refresh course details when returning from browser purchase
// This ensures enrollment status is updated if user completed purchase
viewModel.getCourseDetail()
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand Down Expand Up @@ -510,10 +523,15 @@ private fun CourseDetailNativeContent(

val buttonText = if (course.isEnrolled) {
stringResource(id = R.string.discovery_view_course)
} else if (!course.purchaseLink.isNullOrBlank()) {
stringResource(id = R.string.discovery_buy_course)
} else {
stringResource(id = R.string.discovery_enroll_now)
}

var showPurchaseDialog by remember { mutableStateOf(false) }
val context = LocalContext.current

val durationText = if (course.duration.isBlank()) {
stringResource(id = R.string.course_duration_unspecified)
} else {
Expand Down Expand Up @@ -576,10 +594,61 @@ private fun CourseDetailNativeContent(
)
if (!(enrollmentEnd != null && Date() > enrollmentEnd)) {
Spacer(Modifier.height(32.dp))
OpenEdXButton(
modifier = buttonWidth,
text = buttonText,
onClick = onButtonClick
if (course.isEnrolled) {
// User is enrolled - show regular View Course button
OpenEdXButton(
modifier = buttonWidth,
text = buttonText,
onClick = onButtonClick
)
} else if (!course.purchaseLink.isNullOrBlank()) {
// User not enrolled and purchase link exists - show Buy Course button with cart icon
OpenEdXButton(
modifier = buttonWidth,
text = buttonText,
onClick = {
showPurchaseDialog = true
},
content = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.ShoppingCart,
contentDescription = null,
tint = MaterialTheme.appColors.primaryButtonText,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = buttonText,
color = MaterialTheme.appColors.primaryButtonText,
style = MaterialTheme.appTypography.labelLarge
)
}
}
)
} else {
// User not enrolled and no purchase link - show regular Enroll Now button
OpenEdXButton(
modifier = buttonWidth,
text = buttonText,
onClick = onButtonClick
)
}
}

// Show purchase confirmation dialog
if (showPurchaseDialog) {
PurchaseConfirmationDialog(
onDismiss = { showPurchaseDialog = false },
onContinue = {
val intent = Intent(Intent.ACTION_VIEW, course.purchaseLink?.toUri())
context.startActivity(intent)
showPurchaseDialog = false
}
)
}
Text(
Expand Down Expand Up @@ -958,6 +1027,111 @@ private fun CourseDescription(
})
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PurchaseConfirmationDialog(
onDismiss: () -> Unit,
onContinue: () -> Unit
) {
Dialog(
onDismissRequest = onDismiss,
content = {
Column(
Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.background(
MaterialTheme.appColors.background,
MaterialTheme.appShapes.cardShape
)
.clip(MaterialTheme.appShapes.cardShape)
.border(
1.dp,
MaterialTheme.appColors.cardViewBorder,
MaterialTheme.appShapes.cardShape
)
.padding(horizontal = 40.dp, vertical = 36.dp)
.semantics { testTagsAsResourceId = true },
horizontalAlignment = Alignment.CenterHorizontally
) {
// Close button at top right
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd
) {
IconButton(
modifier = Modifier
.testTag("ib_close_purchase_dialog")
.size(24.dp),
onClick = onDismiss
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(id = CoreR.string.core_cancel),
tint = MaterialTheme.appColors.primary
)
}
}

// Shopping cart icon
Icon(
modifier = Modifier
.width(88.dp)
.height(85.dp),
imageVector = Icons.Filled.ShoppingCart,
contentDescription = null,
tint = MaterialTheme.appColors.onBackground
)

Spacer(Modifier.size(36.dp))

// Title
Text(
modifier = Modifier.testTag("txt_purchase_dialog_title"),
text = stringResource(id = R.string.discovery_purchase_confirmation_title),
color = MaterialTheme.appColors.textPrimary,
style = MaterialTheme.appTypography.titleLarge,
textAlign = TextAlign.Center
)

Spacer(Modifier.size(16.dp))

// Message
Text(
modifier = Modifier.testTag("txt_purchase_dialog_message"),
text = stringResource(id = R.string.discovery_purchase_confirmation_message),
color = MaterialTheme.appColors.textPrimary,
style = MaterialTheme.appTypography.bodyMedium,
textAlign = TextAlign.Center
)

Spacer(Modifier.size(36.dp))

// Continue button with primary color
OpenEdXButton(
modifier = Modifier
.testTag("btn_continue_purchase")
.fillMaxWidth(),
text = stringResource(id = R.string.discovery_continue),
onClick = onContinue,
backgroundColor = MaterialTheme.appColors.primary,
content = {
Text(
modifier = Modifier
.testTag("txt_continue_purchase")
.fillMaxWidth(),
text = stringResource(id = R.string.discovery_continue),
color = MaterialTheme.appColors.primaryButtonText,
style = MaterialTheme.appTypography.labelLarge,
textAlign = TextAlign.Center
)
}
)
}
}
)
}

@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
Expand Down
4 changes: 4 additions & 0 deletions discovery/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<string name="discovery_course_details">Course details</string>
<string name="discovery_enroll_now">Enroll now</string>
<string name="discovery_view_course">View course</string>
<string name="discovery_buy_course">Buy Course</string>
<string name="discovery_purchase_confirmation_title">Complete Purchase</string>
<string name="discovery_purchase_confirmation_message">You\'ll be redirected to your browser to complete the purchase.</string>
<string name="discovery_continue">Continue</string>
<string name="discovery_you_cant_enroll">You cannot enroll in this course because the enrollment date is over.</string>
<string name="discovery_no_internet_label">To enroll in this course, please make sure you are connected to the internet.</string>
<string name="discovery_enrolled_successfully">You have been successfully enrolled in this course.</string>
Expand Down
Loading