From cfd50994c33c1066e8f4da02e4ddf9e84138155d Mon Sep 17 00:00:00 2001 From: Farhan Arshad <43750646+farhan-arshad-dev@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:36:55 +0500 Subject: [PATCH] fix: In App Purchases(IAP) Improvements (#17) * chore: add value prop screen event * fix: remove value_prop_enabled check for IAP * fix: put restore button behind the iap enable config * fix: Make IAP error dialog Non-Cancellable * fix: Improve IAP full screen dialog UI * fix: hide upgrade button for collapsed toolbar in course dashboard --- app/build.gradle | 6 +- core/build.gradle | 6 +- .../org/openedx/core/data/model/AppConfig.kt | 4 - .../openedx/core/domain/model/AppConfig.kt | 1 - .../org/openedx/core/extension/ViewExt.kt | 9 - .../openedx/core/presentation/IAPAnalytics.kt | 6 + .../presentation/dialog/IAPDialogFragment.kt | 11 +- .../core/presentation/iap/IAPViewModel.kt | 66 ++-- .../main/java/org/openedx/core/ui/IAPUI.kt | 19 +- core/src/main/res/values-night/themes.xml | 7 + core/src/main/res/values/themes.xml | 28 +- .../res/values-night/colors.xml | 0 .../{main => openedx}/res/values/colors.xml | 0 .../container/CollapsingLayout.kt | 88 +++--- .../container/CourseContainerViewModel.kt | 9 +- .../presentation/DashboardGalleryView.kt | 287 ++++++++++++------ .../presentation/DashboardListFragment.kt | 6 +- .../presentation/DashboardListViewModel.kt | 4 +- .../presentation/DashboardUIState.kt | 2 +- .../presentation/DashboardViewModelTest.kt | 3 - .../profile/domain/model/Configuration.kt | 2 + .../presentation/settings/SettingsScreenUI.kt | 11 +- .../settings/SettingsViewModel.kt | 2 +- 23 files changed, 351 insertions(+), 226 deletions(-) create mode 100644 core/src/main/res/values-night/themes.xml rename core/src/{main => openedx}/res/values-night/colors.xml (100%) rename core/src/{main => openedx}/res/values/colors.xml (100%) diff --git a/app/build.gradle b/app/build.gradle index 5bf5182e7..b54c9db1e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,13 +82,13 @@ android { sourceSets { prod { - res.srcDirs = ["src/$themeDirectory/res"] + res.srcDirs += ["src/$themeDirectory/res"] } develop { - res.srcDirs = ["src/$themeDirectory/res"] + res.srcDirs += ["src/$themeDirectory/res"] } stage { - res.srcDirs = ["src/$themeDirectory/res"] + res.srcDirs += ["src/$themeDirectory/res"] } } diff --git a/core/build.gradle b/core/build.gradle index 1aed7f0a2..89f99e1cd 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -51,15 +51,15 @@ android { sourceSets { prod { java.srcDirs = ["src/$themeDirectory"] - res.srcDirs = ["src/$themeDirectory/res"] + res.srcDirs += ["src/$themeDirectory/res"] } develop { java.srcDirs = ["src/$themeDirectory"] - res.srcDirs = ["src/$themeDirectory/res"] + res.srcDirs += ["src/$themeDirectory/res"] } stage { java.srcDirs = ["src/$themeDirectory"] - res.srcDirs = ["src/$themeDirectory/res"] + res.srcDirs += ["src/$themeDirectory/res"] } main { assets { 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 index 218a35a4e..988173232 100644 --- a/core/src/main/java/org/openedx/core/data/model/AppConfig.kt +++ b/core/src/main/java/org/openedx/core/data/model/AppConfig.kt @@ -7,16 +7,12 @@ data class AppConfig( @SerializedName("course_dates_calendar_sync") val calendarSyncConfig: CalendarSyncConfig = CalendarSyncConfig(), - @SerializedName("value_prop_enabled") - val isValuePropEnabled: Boolean = false, - @SerializedName("iap_config") val iapConfig: IAPConfig = IAPConfig(), ) { fun mapToDomain(): DomainAppConfig { return DomainAppConfig( courseDatesCalendarSync = calendarSyncConfig.mapToDomain(), - isValuePropEnabled = isValuePropEnabled, iapConfig = iapConfig.mapToDomain(), ) } 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 index 17ef4a5c5..a57f8efd9 100644 --- a/core/src/main/java/org/openedx/core/domain/model/AppConfig.kt +++ b/core/src/main/java/org/openedx/core/domain/model/AppConfig.kt @@ -4,7 +4,6 @@ import java.io.Serializable data class AppConfig( val courseDatesCalendarSync: CourseDatesCalendarSync, - val isValuePropEnabled: Boolean = false, val iapConfig: IAPConfig = IAPConfig(), ) : Serializable diff --git a/core/src/main/java/org/openedx/core/extension/ViewExt.kt b/core/src/main/java/org/openedx/core/extension/ViewExt.kt index 12155a2b7..ebd007d3d 100644 --- a/core/src/main/java/org/openedx/core/extension/ViewExt.kt +++ b/core/src/main/java/org/openedx/core/extension/ViewExt.kt @@ -50,15 +50,6 @@ fun DialogFragment.setWidthPercent(percentage: Int) { dialog?.window?.setLayout(percentWidth.toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) } -fun DialogFragment.setFullScreen(percentage: Int) { - val percent = percentage.toFloat() / 100 - val dm = Resources.getSystem().displayMetrics - val rect = dm.run { Rect(0, 0, widthPixels, heightPixels) } - val percentWidth = rect.width() * percent - val percentHeight = rect.height() * percent - dialog?.window?.setLayout(percentWidth.toInt(), percentHeight.toInt()) -} - fun Context.toastMessage(message: String) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } diff --git a/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt b/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt index 87d2e8ad2..78587e978 100644 --- a/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt +++ b/core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt @@ -6,6 +6,8 @@ interface IAPAnalytics { params: MutableMap = mutableMapOf(), screenName: String, ) + + fun logScreenEvent(screenName: String, params: Map) } enum class IAPAnalyticsEvent(val eventName: String, val biValue: String) { @@ -45,6 +47,10 @@ enum class IAPAnalyticsEvent(val eventName: String, val biValue: String) { IAP_RESTORE_PURCHASE_CLICKED( "Payments: Restore Purchases Clicked", "edx.bi.app.payments.restore_purchases.clicked" + ), + IAP_VALUE_PROP_VIEWED( + "Payments: Value Prop Viewed", + "edx.bi.app.payments.value_prop.viewed" ) } diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/IAPDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/IAPDialogFragment.kt index c18ce07b2..5ecb27b0e 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/IAPDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/IAPDialogFragment.kt @@ -1,7 +1,5 @@ package org.openedx.core.presentation.dialog -import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.text.TextUtils import android.view.LayoutInflater @@ -38,7 +36,6 @@ import org.openedx.core.domain.model.iap.ProductInfo import org.openedx.core.domain.model.iap.PurchaseFlowData import org.openedx.core.extension.parcelable import org.openedx.core.extension.serializable -import org.openedx.core.extension.setFullScreen import org.openedx.core.presentation.iap.IAPAction import org.openedx.core.presentation.iap.IAPFlow import org.openedx.core.presentation.iap.IAPLoaderType @@ -67,7 +64,6 @@ class IAPDialogFragment : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ) = ComposeView(requireContext()).apply { - dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { @@ -77,7 +73,7 @@ class IAPDialogFragment : DialogFragment() { val isFullScreenLoader = (iapState as? IAPUIState.Loading)?.loaderType == IAPLoaderType.FULL_SCREEN - + isCancelable = !isFullScreenLoader Scaffold( modifier = Modifier.fillMaxSize(), backgroundColor = MaterialTheme.appColors.background, @@ -228,9 +224,8 @@ class IAPDialogFragment : DialogFragment() { } } - override fun onStart() { - super.onStart() - setFullScreen(100) + override fun getTheme(): Int { + return R.style.Theme_OpenEdX_IAPDialog } private fun onDismiss() { diff --git a/core/src/main/java/org/openedx/core/presentation/iap/IAPViewModel.kt b/core/src/main/java/org/openedx/core/presentation/iap/IAPViewModel.kt index 1f0592de3..618bb75e9 100644 --- a/core/src/main/java/org/openedx/core/presentation/iap/IAPViewModel.kt +++ b/core/src/main/java/org/openedx/core/presentation/iap/IAPViewModel.kt @@ -99,6 +99,7 @@ class IAPViewModel( when (iapFlow) { IAPFlow.USER_INITIATED -> { + loadIAPScreenEvent() loadPrice() } @@ -337,39 +338,58 @@ class IAPViewModel( }.toMutableMap()) } + private fun getIAPEventParams(): MutableMap { + return buildMap { + purchaseFlowData.takeIf { it.courseId.isNullOrBlank().not() }?.let { + put(IAPAnalyticsKeys.COURSE_ID.key, purchaseFlowData.courseId) + put( + IAPAnalyticsKeys.PACING.key, + if (purchaseFlowData.isSelfPaced == true) IAPAnalyticsKeys.SELF.key else IAPAnalyticsKeys.INSTRUCTOR.key + ) + } + purchaseFlowData.productInfo?.lmsUSDPrice?.nonZero()?.let { lmsUSDPrice -> + put(IAPAnalyticsKeys.LMS_USD_PRICE.key, lmsUSDPrice) + } + purchaseFlowData.price.nonZero()?.let { localizedPrice -> + put(IAPAnalyticsKeys.LOCALIZED_PRICE.key, localizedPrice) + } + purchaseFlowData.currencyCode.takeIfNotEmpty()?.let { currencyCode -> + put(IAPAnalyticsKeys.CURRENCY_CODE.key, currencyCode) + } + purchaseFlowData.componentId?.takeIf { it.isNotBlank() }?.let { componentId -> + put(IAPAnalyticsKeys.COMPONENT_ID.key, componentId) + } + put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key) + }.toMutableMap() + } + private fun logIAPEvent( event: IAPAnalyticsEvent, params: MutableMap = mutableMapOf(), ) { + params.apply { + put(IAPAnalyticsKeys.NAME.key, event.biValue) + putAll(getIAPEventParams()) + } analytics.logIAPEvent( event = event, - params = params.apply { - put(IAPAnalyticsKeys.NAME.key, event.biValue) - purchaseFlowData.takeIf { it.courseId.isNullOrBlank().not() }?.let { - put(IAPAnalyticsKeys.COURSE_ID.key, purchaseFlowData.courseId) - put( - IAPAnalyticsKeys.PACING.key, - if (purchaseFlowData.isSelfPaced == true) IAPAnalyticsKeys.SELF.key else IAPAnalyticsKeys.INSTRUCTOR.key - ) - } - purchaseFlowData.productInfo?.lmsUSDPrice?.nonZero()?.let { lmsUSDPrice -> - put(IAPAnalyticsKeys.LMS_USD_PRICE.key, lmsUSDPrice) - } - purchaseFlowData.price.nonZero()?.let { localizedPrice -> - put(IAPAnalyticsKeys.LOCALIZED_PRICE.key, localizedPrice) - } - purchaseFlowData.currencyCode.takeIfNotEmpty()?.let { currencyCode -> - put(IAPAnalyticsKeys.CURRENCY_CODE.key, currencyCode) - } - purchaseFlowData.componentId?.takeIf { it.isNotBlank() }?.let { componentId -> - put(IAPAnalyticsKeys.COMPONENT_ID.key, componentId) - } - put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key) - }, + params = params, screenName = purchaseFlowData.screenName.orEmpty() ) } + private fun loadIAPScreenEvent() { + val event = IAPAnalyticsEvent.IAP_VALUE_PROP_VIEWED + val params = buildMap { + put(IAPAnalyticsKeys.NAME.key, event.biValue) + purchaseFlowData.screenName?.takeIfNotEmpty()?.let { screenName -> + put(IAPAnalyticsKeys.SCREEN_NAME.key, screenName) + } + putAll(getIAPEventParams()) + } + analytics.logScreenEvent(screenName = event.eventName, params = params) + } + fun clearIAPFLow() { _uiState.value = IAPUIState.Clear purchaseFlowData.reset() diff --git a/core/src/main/java/org/openedx/core/ui/IAPUI.kt b/core/src/main/java/org/openedx/core/ui/IAPUI.kt index 1eb56d503..e6a0b4ac1 100644 --- a/core/src/main/java/org/openedx/core/ui/IAPUI.kt +++ b/core/src/main/java/org/openedx/core/ui/IAPUI.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import org.openedx.core.R import org.openedx.core.exception.iap.IAPException import org.openedx.core.presentation.iap.IAPAction @@ -183,7 +184,8 @@ fun NoSkuErrorDialog( onClick = onConfirm ) }, - onDismissRequest = onConfirm + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), + onDismissRequest = {} ) } @@ -252,6 +254,7 @@ fun CourseAlreadyPurchasedExecuteErrorDialog( ) } }, + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), onDismissRequest = {} ) } @@ -302,13 +305,17 @@ fun UpgradeErrorDialog( onClick = onDismiss ) }, - onDismissRequest = onConfirm + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), + onDismissRequest = {} ) } @Composable fun CheckingPurchasesDialog() { - Dialog(onDismissRequest = { }) { + Dialog( + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), + onDismissRequest = {} + ) { Column( Modifier .padding(16.dp) @@ -372,7 +379,8 @@ fun FakePurchasesFulfillmentCompleted(onCancel: () -> Unit, onGetHelp: () -> Uni onClick = onGetHelp ) }, - onDismissRequest = onCancel + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), + onDismissRequest = {} ) } @@ -415,7 +423,8 @@ fun PurchasesFulfillmentCompletedDialog(onConfirm: () -> Unit, onDismiss: () -> onClick = onDismiss ) }, - onDismissRequest = onDismiss + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), + onDismissRequest = {} ) } diff --git a/core/src/main/res/values-night/themes.xml b/core/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..762b2bd0e --- /dev/null +++ b/core/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/core/src/main/res/values/themes.xml b/core/src/main/res/values/themes.xml index a092e8d47..b3e5b5e35 100644 --- a/core/src/main/res/values/themes.xml +++ b/core/src/main/res/values/themes.xml @@ -1,14 +1,14 @@ - + + + + + diff --git a/core/src/main/res/values-night/colors.xml b/core/src/openedx/res/values-night/colors.xml similarity index 100% rename from core/src/main/res/values-night/colors.xml rename to core/src/openedx/res/values-night/colors.xml diff --git a/core/src/main/res/values/colors.xml b/core/src/openedx/res/values/colors.xml similarity index 100% rename from core/src/main/res/values/colors.xml rename to core/src/openedx/res/values/colors.xml diff --git a/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt b/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt index 52c87456f..b18eefa5c 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt @@ -377,14 +377,16 @@ private fun CollapsingLayoutTablet( ) } - Box( + Column( modifier = Modifier .onSizeChanged { size -> expandedTopHeight.value = size.height.toFloat() } .offset { IntOffset(x = 0, y = backgroundImageHeight.value.roundToInt()) }, - content = expandedTop, - ) + ) { + Box(content = expandedTop) + Box(content = upgradeButton) + } Icon( modifier = Modifier @@ -400,19 +402,19 @@ private fun CollapsingLayoutTablet( contentDescription = null ) - Column(modifier = Modifier - .offset { - IntOffset( - x = 0, - y = (backgroundImageHeight.value + expandedTopHeight.value).roundToInt() - ) - } - .onSizeChanged { size -> - navigationHeight.value = size.height.toFloat() - }) { - Box(content = upgradeButton) - Box(content = navigation) - } + Box( + modifier = Modifier + .offset { + IntOffset( + x = 0, + y = (backgroundImageHeight.value + expandedTopHeight.value).roundToInt() + ) + } + .onSizeChanged { size -> + navigationHeight.value = size.height.toFloat() + }, + content = navigation, + ) Box( modifier = Modifier @@ -540,15 +542,15 @@ private fun CollapsingLayoutMobile( } - Column(modifier = Modifier - .displayCutoutForLandscape() - .offset { IntOffset(x = 0, y = (collapsedTopHeight.value).roundToInt()) } - .onSizeChanged { size -> - navigationHeight.value = size.height.toFloat() - }) { - Box(content = upgradeButton) - Box(content = navigation) - } + Box( + modifier = Modifier + .displayCutoutForLandscape() + .offset { IntOffset(x = 0, y = (collapsedTopHeight.value).roundToInt()) } + .onSizeChanged { size -> + navigationHeight.value = size.height.toFloat() + }, + content = navigation, + ) Box( modifier = Modifier @@ -659,7 +661,7 @@ private fun CollapsingLayoutMobile( ) } - Box( + Column( modifier = Modifier .onSizeChanged { size -> expandedTopHeight.value = size.height.toFloat() @@ -670,9 +672,11 @@ private fun CollapsingLayoutMobile( y = (offset.value + backgroundImageHeight.value - blurImagePaddingPx).roundToInt() ) } - .alpha(factor), - content = expandedTop, - ) + .alpha(factor) + ) { + Box(content = expandedTop) + Box(content = upgradeButton) + } Row( modifier = Modifier @@ -705,19 +709,19 @@ private fun CollapsingLayoutMobile( } val adaptiveImagePadding = blurImagePaddingPx * factor - Column(modifier = Modifier - .offset { - IntOffset( - x = 0, - y = (offset.value + backgroundImageHeight.value + expandedTopHeight.value - adaptiveImagePadding).roundToInt() - ) - } - .onSizeChanged { size -> - navigationHeight.value = size.height.toFloat() - }) { - Box(content = upgradeButton) - Box(content = navigation) - } + Box( + modifier = Modifier + .offset { + IntOffset( + x = 0, + y = (offset.value + backgroundImageHeight.value + expandedTopHeight.value - adaptiveImagePadding).roundToInt() + ) + } + .onSizeChanged { size -> + navigationHeight.value = size.height.toFloat() + }, + content = navigation, + ) Box( modifier = Modifier diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index b4a846b09..ded741be7 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -106,9 +106,6 @@ class CourseContainerViewModel( val uiMessage: SharedFlow get() = _uiMessage.asSharedFlow() - private val isValuePropEnabled: Boolean - get() = corePreferences.appConfig.isValuePropEnabled - private val iapConfig get() = corePreferences.appConfig.iapConfig @@ -214,9 +211,8 @@ class CourseContainerViewModel( } isReady } - _canShowUpgradeButton.value = isIAPEnabled && - isValuePropEnabled && - courseStructure?.isUpgradeable == true + _canShowUpgradeButton.value = + isIAPEnabled && courseStructure?.isUpgradeable == true } if (_dataReady.value == true && resumeBlockId.isNotEmpty()) { delay(500L) @@ -285,7 +281,6 @@ class CourseContainerViewModel( try { _courseStructure = interactor.getCourseStructure(courseId, isNeedRefresh = true) _canShowUpgradeButton.value = isIAPEnabled && - isValuePropEnabled && courseStructure?.productInfo != null && courseStructure?.isUpgradeable == true } catch (e: Exception) { diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt index 10c28fc9e..d208e7a3f 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -53,6 +54,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -62,13 +64,13 @@ 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.fragment.app.FragmentManager import coil.compose.AsyncImage import coil.request.ImageRequest import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf -import org.openedx.Lock import org.openedx.core.UIMessage import org.openedx.core.domain.model.AppConfig import org.openedx.core.domain.model.Certificate @@ -95,6 +97,7 @@ import org.openedx.core.ui.TextIcon import org.openedx.core.ui.UpgradeErrorDialog import org.openedx.core.ui.UpgradeToAccessView import org.openedx.core.ui.UpgradeToAccessViewType +import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors @@ -205,6 +208,7 @@ private fun DashboardGalleryView( Surface( modifier = Modifier .fillMaxSize() + .displayCutoutForLandscape() .padding(paddingValues), color = MaterialTheme.appColors.background ) { @@ -348,7 +352,7 @@ private fun UserCourses( val primaryCourse = userCourses.primary if (primaryCourse != null) { PrimaryCourseCard( - isValuePropEnabled = userCourses.configs.isValuePropEnabled, + isIAPEnabled = userCourses.configs.iapConfig.isEnabled, primaryCourse = primaryCourse, apiHostUrl = apiHostUrl, navigateToDates = navigateToDates, @@ -569,7 +573,7 @@ private fun AssignmentItem( @Composable private fun PrimaryCourseCard( - isValuePropEnabled: Boolean, + isIAPEnabled: Boolean, primaryCourse: EnrolledCourse, apiHostUrl: String, navigateToDates: (EnrolledCourse) -> Unit, @@ -577,7 +581,8 @@ private fun PrimaryCourseCard( openCourse: (EnrolledCourse) -> Unit, onIAPAction: (IAPAction, EnrolledCourse?, IAPException?) -> Unit = { _, _, _ -> }, ) { - val context = LocalContext.current + val orientation = LocalConfiguration.current.orientation + Card( modifier = Modifier .padding(horizontal = 16.dp) @@ -587,110 +592,190 @@ private fun PrimaryCourseCard( shape = MaterialTheme.appShapes.courseImageShape, elevation = 2.dp ) { - Column( - modifier = Modifier - .clickable { - openCourse(primaryCourse) - } - ) { - AsyncImage( - model = ImageRequest.Builder(context) - .data(apiHostUrl + primaryCourse.course.courseImage) - .error(CoreR.drawable.core_no_image_course) - .placeholder(CoreR.drawable.core_no_image_course) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height(140.dp) - ) - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .height(8.dp), - progress = primaryCourse.progress.value, - color = MaterialTheme.appColors.progressBarColor, - backgroundColor = MaterialTheme.appColors.progressBarBackgroundColor - ) - PrimaryCourseTitle( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp) - .padding(top = 8.dp, bottom = 16.dp), - primaryCourse = primaryCourse - ) - val pastAssignments = primaryCourse.courseAssignments?.pastAssignments - if (!pastAssignments.isNullOrEmpty()) { - val nearestAssignment = pastAssignments.maxBy { it.date } - val title = if (pastAssignments.size == 1) nearestAssignment.title else null - Divider() - AssignmentItem( - modifier = Modifier.clickable { - if (pastAssignments.size == 1) { - resumeBlockId(primaryCourse, nearestAssignment.blockId) - } else { - navigateToDates(primaryCourse) + when (orientation) { + Configuration.ORIENTATION_LANDSCAPE -> { + Row( + modifier = Modifier + .clickable { + openCourse(primaryCourse) } - }, - painter = rememberVectorPainter(Icons.Default.Warning), - title = title, - info = pluralStringResource( - R.plurals.dashboard_past_due_assignment, - pastAssignments.size, - pastAssignments.size + .height(IntrinsicSize.Min) + ) { + PrimaryCourseCaption( + modifier = Modifier.weight(1f), + primaryCourse = primaryCourse, + apiHostUrl = apiHostUrl, + imageHeight = null, ) - ) - } - val futureAssignments = primaryCourse.courseAssignments?.futureAssignments - if (!futureAssignments.isNullOrEmpty()) { - val nearestAssignment = futureAssignments.minBy { it.date } - val title = if (futureAssignments.size == 1) nearestAssignment.title else null - Divider() - AssignmentItem( - modifier = Modifier.clickable { - if (futureAssignments.size == 1) { - resumeBlockId(primaryCourse, nearestAssignment.blockId) - } else { - navigateToDates(primaryCourse) - } - }, - painter = painterResource(id = CoreR.drawable.ic_core_chapter_icon), - title = title, - info = stringResource( - R.string.dashboard_assignment_due, - nearestAssignment.assignmentType ?: "", - TimeUtils.getAssignmentFormattedDate(context, nearestAssignment.date) + PrimaryCourseButtons( + modifier = Modifier.weight(1f), + primaryCourse = primaryCourse, + navigateToDates = navigateToDates, + resumeBlockId = resumeBlockId, + openCourse = openCourse, + adjustHeight = true, + isIAPEnabled = isIAPEnabled, + onIAPAction = onIAPAction, ) - ) + } } - if (primaryCourse.isUpgradeable && isValuePropEnabled) { - UpgradeToAccessView( - type = UpgradeToAccessViewType.GALLERY, - iconPadding = PaddingValues(end = 12.dp), - padding = PaddingValues(vertical = 16.dp, horizontal = 14.dp) + + else -> { + Column( + modifier = Modifier.clickable { + openCourse(primaryCourse) + } ) { - onIAPAction( - IAPAction.ACTION_USER_INITIATED, - primaryCourse, - null + PrimaryCourseCaption( + primaryCourse = primaryCourse, + apiHostUrl = apiHostUrl, + ) + PrimaryCourseButtons( + primaryCourse = primaryCourse, + navigateToDates = navigateToDates, + resumeBlockId = resumeBlockId, + openCourse = openCourse, + isIAPEnabled = isIAPEnabled, + onIAPAction = onIAPAction, ) } } - ResumeButton( - primaryCourse = primaryCourse, - onClick = { - if (primaryCourse.courseStatus == null) { - openCourse(primaryCourse) + } + } +} + +@Composable +private fun PrimaryCourseButtons( + modifier: Modifier = Modifier, + primaryCourse: EnrolledCourse, + adjustHeight: Boolean = false, + navigateToDates: (EnrolledCourse) -> Unit, + resumeBlockId: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, + openCourse: (EnrolledCourse) -> Unit, + isIAPEnabled: Boolean, + onIAPAction: (IAPAction, EnrolledCourse?, IAPException?) -> Unit = { _, _, _ -> }, +) { + val context = LocalContext.current + val pastAssignments = primaryCourse.courseAssignments?.pastAssignments + Column(modifier = modifier) { + var titleModifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(top = 8.dp, bottom = 16.dp) + if (adjustHeight) { + titleModifier = titleModifier.weight(1f) + } + PrimaryCourseTitle( + modifier = titleModifier, + primaryCourse = primaryCourse, + ) + Divider() + if (!pastAssignments.isNullOrEmpty()) { + val nearestAssignment = pastAssignments.maxBy { it.date } + val title = if (pastAssignments.size == 1) nearestAssignment.title else null + AssignmentItem( + modifier = Modifier.clickable { + if (pastAssignments.size == 1) { + resumeBlockId(primaryCourse, nearestAssignment.blockId) } else { - resumeBlockId( - primaryCourse, - primaryCourse.courseStatus?.lastVisitedBlockId ?: "" - ) + navigateToDates(primaryCourse) } - } + }, + painter = rememberVectorPainter(Icons.Default.Warning), + title = title, + info = pluralStringResource( + R.plurals.dashboard_past_due_assignment, + pastAssignments.size, + pastAssignments.size + ) ) } + val futureAssignments = primaryCourse.courseAssignments?.futureAssignments + if (!futureAssignments.isNullOrEmpty()) { + val nearestAssignment = futureAssignments.minBy { it.date } + val title = if (futureAssignments.size == 1) nearestAssignment.title else null + Divider() + AssignmentItem( + modifier = Modifier.clickable { + if (futureAssignments.size == 1) { + resumeBlockId(primaryCourse, nearestAssignment.blockId) + } else { + navigateToDates(primaryCourse) + } + }, + painter = painterResource(id = CoreR.drawable.ic_core_chapter_icon), + title = title, + info = stringResource( + R.string.dashboard_assignment_due, + nearestAssignment.assignmentType ?: "", + TimeUtils.getAssignmentFormattedDate(context, nearestAssignment.date) + ) + ) + } + if (primaryCourse.isUpgradeable && isIAPEnabled) { + UpgradeToAccessView( + type = UpgradeToAccessViewType.GALLERY, + iconPadding = PaddingValues(end = 12.dp), + padding = PaddingValues(vertical = 16.dp, horizontal = 14.dp) + ) { + onIAPAction( + IAPAction.ACTION_USER_INITIATED, + primaryCourse, + null + ) + } + } + ResumeButton( + primaryCourse = primaryCourse, + onClick = { + if (primaryCourse.courseStatus == null) { + openCourse(primaryCourse) + } else { + resumeBlockId( + primaryCourse, + primaryCourse.courseStatus?.lastVisitedBlockId ?: "" + ) + } + } + ) + } +} + +@Composable +private fun PrimaryCourseCaption( + modifier: Modifier = Modifier, + primaryCourse: EnrolledCourse, + imageHeight: Dp? = 140.dp, + apiHostUrl: String, +) { + val context = LocalContext.current + Column(modifier = modifier) { + val imageModifier = imageHeight?.let { + Modifier + .height(it) + .fillMaxWidth() + } ?: Modifier + .height(IntrinsicSize.Max) + .fillMaxWidth() + .weight(1f) + AsyncImage( + model = ImageRequest.Builder(context) + .data(apiHostUrl + primaryCourse.course.courseImage) + .error(CoreR.drawable.core_no_image_course) + .placeholder(CoreR.drawable.core_no_image_course) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = imageModifier + ) + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(8.dp), + progress = primaryCourse.progress.value, + color = MaterialTheme.appColors.progressBarColor, + backgroundColor = MaterialTheme.appColors.progressBarBackgroundColor + ) } } @@ -760,7 +845,7 @@ private fun PrimaryCourseTitle( ) { Column( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.Center ) { Text( modifier = Modifier.fillMaxWidth(), @@ -769,7 +854,9 @@ private fun PrimaryCourseTitle( color = MaterialTheme.appColors.textFieldHint ) Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), text = primaryCourse.course.name, style = MaterialTheme.appTypography.titleLarge, color = MaterialTheme.appColors.textDark, @@ -777,7 +864,9 @@ private fun PrimaryCourseTitle( maxLines = 3 ) Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), style = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.textFieldHint, text = TimeUtils.getCourseFormattedDate( diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt index 1a38fad14..f6759fd9e 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -298,7 +298,7 @@ internal fun DashboardListView( course, windowSize, onClick = { onItemClick(it) }) - if (course.isUpgradeable && state.isValuePropEnabled) { + if (course.isUpgradeable && state.isIAPEnabled) { UpgradeToAccessView( modifier = Modifier.padding( bottom = 16.dp @@ -598,7 +598,7 @@ private fun DashboardListViewPreview() { mockCourseEnrolled, mockCourseEnrolled, mockCourseEnrolled - ), isValuePropEnabled = false + ), isIAPEnabled = false ), uiMessage = null, iapUiState = null, @@ -631,7 +631,7 @@ private fun DashboardListViewTabletPreview() { mockCourseEnrolled, mockCourseEnrolled, mockCourseEnrolled - ), isValuePropEnabled = false + ), isIAPEnabled = false ), uiMessage = null, iapUiState = null, diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt index 5359eee8e..0f5f9284a 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -153,7 +153,7 @@ class DashboardListViewModel( } else { _uiState.value = DashboardUIState.Courses( courses = ArrayList(coursesList), - isValuePropEnabled = preferencesManager.appConfig.isValuePropEnabled + isIAPEnabled = preferencesManager.appConfig.iapConfig.isEnabled ) } if (isIAPFlow) { @@ -261,7 +261,7 @@ class DashboardListViewModel( } else { _uiState.value = DashboardUIState.Courses( courses = ArrayList(coursesList), - isValuePropEnabled = preferencesManager.appConfig.isValuePropEnabled + isIAPEnabled = preferencesManager.appConfig.iapConfig.isEnabled ) } } catch (e: Exception) { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardUIState.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardUIState.kt index aa3ba1a31..263cebcae 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardUIState.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardUIState.kt @@ -3,7 +3,7 @@ package org.openedx.dashboard.presentation import org.openedx.core.domain.model.EnrolledCourse sealed class DashboardUIState { - data class Courses(val courses: List, val isValuePropEnabled: Boolean) : + data class Courses(val courses: List, val isIAPEnabled: Boolean) : DashboardUIState() object Empty : DashboardUIState() diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt index 507afcfee..c0455c763 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt @@ -237,7 +237,6 @@ class DashboardViewModelTest { @Test fun `getCourses from cache`() = runTest { every { networkConnection.isOnline() } returns false - every { corePreferences.appConfig.isValuePropEnabled } returns false coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) every { corePreferences.appConfig.iapConfig } returns appConfig.iapConfig @@ -337,7 +336,6 @@ class DashboardViewModelTest { @Test fun `updateCourses success`() = runTest { every { networkConnection.isOnline() } returns true - every { corePreferences.appConfig.isValuePropEnabled } returns false every { corePreferences.appConfig.iapConfig } returns appConfig.iapConfig coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList coEvery { iapNotifier.notifier } returns flow { emit(CourseDataUpdated()) } @@ -375,7 +373,6 @@ class DashboardViewModelTest { fun `updateCourses success with next page`() = runTest { every { networkConnection.isOnline() } returns true every { corePreferences.appConfig.iapConfig } returns appConfig.iapConfig - every { corePreferences.appConfig.isValuePropEnabled } returns false coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy( Pagination( 10, diff --git a/profile/src/main/java/org/openedx/profile/domain/model/Configuration.kt b/profile/src/main/java/org/openedx/profile/domain/model/Configuration.kt index f35d18ac4..df4d47b39 100644 --- a/profile/src/main/java/org/openedx/profile/domain/model/Configuration.kt +++ b/profile/src/main/java/org/openedx/profile/domain/model/Configuration.kt @@ -3,12 +3,14 @@ package org.openedx.profile.domain.model import org.openedx.core.domain.model.AgreementUrls /** + * @param isIAPEnabled In App Purchase is enabled or not * @param agreementUrls User agreement urls * @param faqUrl FAQ url * @param supportEmail Email address of support * @param versionName Version of the application (1.0.0) */ data class Configuration( + val isIAPEnabled: Boolean, val agreementUrls: AgreementUrls, val faqUrl: String, val supportEmail: String, diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt index a51044bda..48f8e5c5f 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt @@ -197,11 +197,13 @@ internal fun SettingsScreen( Spacer(modifier = Modifier.height(24.dp)) - PurchaseSection(onRestorePurchaseClick = { - onAction(SettingsScreenAction.RestorePurchaseClick) - }) + if (uiState.configuration.isIAPEnabled) { + PurchaseSection(onRestorePurchaseClick = { + onAction(SettingsScreenAction.RestorePurchaseClick) + }) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) + } SupportInfoSection( uiState = uiState, @@ -716,6 +718,7 @@ private val mockConfiguration = Configuration( faqUrl = "https://example.com/faq", supportEmail = "test@example.com", versionName = mockAppData.versionName, + isIAPEnabled = true, ) private val mockUiState = SettingsUIState.Data( diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt index 5be21b10c..d6b1c36e9 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt @@ -30,7 +30,6 @@ import org.openedx.core.presentation.IAPAnalyticsKeys import org.openedx.core.presentation.IAPAnalyticsScreen import org.openedx.core.presentation.global.AppData import org.openedx.core.presentation.iap.IAPAction -import org.openedx.core.presentation.iap.IAPFlow import org.openedx.core.presentation.iap.IAPLoaderType import org.openedx.core.presentation.iap.IAPRequestType import org.openedx.core.presentation.iap.IAPUIState @@ -89,6 +88,7 @@ class SettingsViewModel( private val configuration get() = Configuration( + isIAPEnabled = corePreferences.appConfig.iapConfig.isEnabled, agreementUrls = config.getAgreement(Locale.current.language), faqUrl = config.getFaqUrl(), supportEmail = config.getFeedbackEmailAddress(),