Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore: add missing course properties in IAP restore & unfulfilled events #82

Merged
merged 5 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ val screenModule = module {
get(),
get(),
get(),
get(),
windowSize,
get(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import org.openedx.core.R
import org.openedx.core.config.Config
import org.openedx.core.data.repository.iap.IAPRepository
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.EnrolledCourse
import org.openedx.core.domain.model.iap.ProductInfo
import org.openedx.core.domain.model.iap.PurchaseFlowData
import org.openedx.core.exception.iap.IAPException
import org.openedx.core.extension.decodeToLong
import org.openedx.core.extension.decodeToString
import org.openedx.core.module.billing.BillingProcessor
import org.openedx.core.module.billing.getCourseSku
import org.openedx.core.module.billing.getPriceAmount
Expand Down Expand Up @@ -109,12 +112,42 @@ class IAPInteractor(
}
}

suspend fun processUnfulfilledPurchase(userId: Long): Boolean {
suspend fun processUnfulfilledPurchase(
userId: Long,
enrolledCourses: List<EnrolledCourse>,
purchaseVerified: (PurchaseFlowData) -> Unit = {},
): Boolean {
val purchases = billingProcessor.queryPurchases()
val userPurchases =
purchases.filter { it.accountIdentifiers?.obfuscatedAccountId?.decodeToLong() == userId }
val userPurchases = purchases.filter { purchase ->
val userAccountId = purchase.accountIdentifiers?.obfuscatedAccountId?.decodeToLong()
val storeSku = purchase.accountIdentifiers?.obfuscatedProfileId?.decodeToString()
HamzaIsrar12 marked this conversation as resolved.
Show resolved Hide resolved

userAccountId == userId && enrolledCourses.any { enrolledCourse ->
storeSku == enrolledCourse.productInfo?.courseSku
}
}
if (userPurchases.isNotEmpty()) {
startUnfulfilledVerification(userPurchases)
userPurchases.forEach { purchase ->
val courseVerified = enrolledCourses.find { enrolledCourse ->
enrolledCourse.productInfo?.courseSku == purchase.getCourseSku()
}
HamzaIsrar12 marked this conversation as resolved.
Show resolved Hide resolved
courseVerified?.let {
val productDetails =
billingProcessor.querySyncDetails(purchase.products[0]).productDetailsList?.firstOrNull()
val purchaseProductFlow = PurchaseFlowData(
courseId = courseVerified.course.id,
isSelfPaced = courseVerified.course.isSelfPaced,
productInfo = courseVerified.productInfo
).apply {
productDetails?.oneTimePurchaseOfferDetails?.let {
this.price = it.getPriceAmount()
this.currencyCode = it.priceCurrencyCode
}
}
startUnfulfilledVerification(purchase)
purchaseVerified(purchaseProductFlow)
}
}
return true
} else {
purchases.forEach {
Expand All @@ -124,34 +157,34 @@ class IAPInteractor(
return false
}

private suspend fun startUnfulfilledVerification(userPurchases: List<Purchase>) {
userPurchases.forEach { purchase ->
val productDetail =
billingProcessor.querySyncDetails(purchase.products.first()).productDetailsList?.firstOrNull()
productDetail?.oneTimePurchaseOfferDetails?.takeIf {
purchase.getCourseSku().isNullOrEmpty().not()
}?.let { oneTimeProductDetails ->
val courseSku = purchase.getCourseSku() ?: return@let
val basketId = addToBasket(courseSku)
executeOrder(
basketId = basketId,
purchaseToken = purchase.purchaseToken,
price = oneTimeProductDetails.getPriceAmount(),
currencyCode = oneTimeProductDetails.priceCurrencyCode,
)
consumePurchase(purchase.purchaseToken)
}
private suspend fun startUnfulfilledVerification(userPurchase: Purchase) {
val productDetail =
billingProcessor.querySyncDetails(userPurchase.products.first()).productDetailsList?.firstOrNull()
productDetail?.oneTimePurchaseOfferDetails?.takeIf {
userPurchase.getCourseSku().isNullOrEmpty().not()
}?.let { oneTimeProductDetails ->
val courseSku = userPurchase.getCourseSku() ?: return@let
val basketId = addToBasket(courseSku)
executeOrder(
basketId = basketId,
purchaseToken = userPurchase.purchaseToken,
price = oneTimeProductDetails.getPriceAmount(),
currencyCode = oneTimeProductDetails.priceCurrencyCode,
)
consumePurchase(userPurchase.purchaseToken)
}
}

suspend fun detectUnfulfilledPurchase(
enrolledCourses: List<EnrolledCourse>,
purchaseVerified: (PurchaseFlowData) -> Unit,
onSuccess: () -> Unit,
onFailure: (IAPException) -> Unit,
) {
if (isIAPEnabled) {
preferencesManager.user?.id?.let { userId ->
runCatching {
processUnfulfilledPurchase(userId)
processUnfulfilledPurchase(userId, enrolledCourses, purchaseVerified)
}.onSuccess {
if (it) {
onSuccess()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import org.openedx.core.utils.TimeUtils

class IAPEventLogger(
private val analytics: IAPAnalytics,
private val purchaseFlowData: PurchaseFlowData? = null,
private val isSilentIAPFlow: Boolean? = null,
var purchaseFlowData: PurchaseFlowData? = null,
) {
fun upgradeNowClickedEvent() {
logIAPEvent(IAPAnalyticsEvent.IAP_UPGRADE_NOW_CLICKED)
Expand Down Expand Up @@ -64,7 +64,7 @@ class IAPEventLogger(

IAPRequestType.PRICE_CODE,
IAPRequestType.NO_SKU_CODE,
-> {
-> {
priceLoadErrorEvent(feedbackErrorMessage)
}

Expand Down Expand Up @@ -186,7 +186,7 @@ class IAPEventLogger(
putAll(params)
putAll(getIAPEventParams())
putAll(getUnfulfilledIAPEventParams())
},
}
)
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package org.openedx.core.system.notifier.app

import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow

class AppNotifier {

private val channel = MutableSharedFlow<AppEvent>(replay = 0, extraBufferCapacity = 0)
private val channel = MutableSharedFlow<AppEvent>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)

val notifier: Flow<AppEvent> = channel.asSharedFlow()

Expand All @@ -16,4 +21,7 @@ class AppNotifier {

suspend fun send(event: AppUpgradeEvent) = channel.emit(event)

suspend fun send(event: EnrolledCourseEvent) = channel.emit(event)

suspend fun send(event: RequestEnrolledCourseEvent) = channel.emit(event)
HamzaIsrar12 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.openedx.core.system.notifier.app

import org.openedx.core.domain.model.EnrolledCourse

class EnrolledCourseEvent(val enrolledCourses: List<EnrolledCourse>,) : AppEvent
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.openedx.core.system.notifier.app

object RequestEnrolledCourseEvent : AppEvent
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ import org.openedx.core.system.notifier.NavigationToDiscovery
import org.openedx.core.system.notifier.PushEvent
import org.openedx.core.system.notifier.PushNotifier
import org.openedx.core.system.notifier.UpdateCourseData
import org.openedx.core.system.notifier.app.AppNotifier
import org.openedx.core.system.notifier.app.EnrolledCourseEvent
import org.openedx.core.system.notifier.app.RequestEnrolledCourseEvent
import org.openedx.core.ui.WindowSize
import org.openedx.core.utils.FileUtil
import org.openedx.dashboard.domain.CourseStatusFilter
import org.openedx.dashboard.domain.interactor.DashboardInteractor
import org.openedx.dashboard.presentation.DashboardRouter

Expand All @@ -60,6 +64,7 @@ class DashboardGalleryViewModel(
private val dashboardRouter: DashboardRouter,
private val iapNotifier: IAPNotifier,
private val pushNotifier: PushNotifier,
private val appNotifier: AppNotifier,
private val iapInteractor: IAPInteractor,
private val windowSize: WindowSize,
iapAnalytics: IAPAnalytics,
Expand All @@ -76,7 +81,7 @@ class DashboardGalleryViewModel(
val uiMessage: SharedFlow<UIMessage?>
get() = _uiMessage.asSharedFlow()

private val _updating = MutableStateFlow<Boolean>(false)
private val _updating = MutableStateFlow(false)
val updating: StateFlow<Boolean>
get() = _updating.asStateFlow()

Expand All @@ -96,11 +101,25 @@ class DashboardGalleryViewModel(
private var isLoading = false

init {
collectAppEvent()
collectDiscoveryNotifier()
collectIapNotifier()
getCourses()
}

private fun collectAppEvent() {
appNotifier.notifier
.onEach {
if (it is RequestEnrolledCourseEvent) {
val enrolledCourses =
interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses
appNotifier.send(EnrolledCourseEvent(enrolledCourses))
}
}
.distinctUntilChanged()
.launchIn(viewModelScope)
}

fun getCourses(isIAPFlow: Boolean = false) {
viewModelScope.launch {
try {
Expand Down Expand Up @@ -258,9 +277,17 @@ class DashboardGalleryViewModel(

private fun detectUnfulfilledPurchase() {
viewModelScope.launch(Dispatchers.IO) {
val enrolledCourses =
interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses
iapInteractor.detectUnfulfilledPurchase(
enrolledCourses = enrolledCourses,
purchaseVerified = { purchaseFlowData ->
eventLogger.apply {
this.purchaseFlowData = purchaseFlowData
this.logUnfulfilledPurchaseInitiatedEvent()
}
},
onSuccess = {
eventLogger.logUnfulfilledPurchaseInitiatedEvent()
_iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted)
},
onFailure = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ import org.openedx.core.system.notifier.PushNotifier
import org.openedx.core.system.notifier.UpdateCourseData
import org.openedx.core.system.notifier.app.AppNotifier
import org.openedx.core.system.notifier.app.AppUpgradeEvent
import org.openedx.core.system.notifier.app.EnrolledCourseEvent
import org.openedx.core.system.notifier.app.RequestEnrolledCourseEvent
import org.openedx.dashboard.domain.CourseStatusFilter
import org.openedx.dashboard.domain.interactor.DashboardInteractor

@SuppressLint("StaticFieldLeak")
Expand Down Expand Up @@ -124,7 +127,7 @@ class DashboardListViewModel(

init {
getCourses()
collectAppUpgradeEvent()
collectAppEvent()
}

fun getCourses() {
Expand Down Expand Up @@ -292,14 +295,20 @@ class DashboardListViewModel(
}
}

private fun collectAppUpgradeEvent() {
viewModelScope.launch {
appNotifier.notifier.collect { event ->
if (event is AppUpgradeEvent) {
_appUpgradeEvent.value = event
private fun collectAppEvent() {
appNotifier.notifier
.onEach {
if (it is AppUpgradeEvent) {
_appUpgradeEvent.value = it
}
if (it is RequestEnrolledCourseEvent) {
val enrolledCourses =
interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses
appNotifier.send(EnrolledCourseEvent(enrolledCourses))
}
}
}
.distinctUntilChanged()
.launchIn(viewModelScope)
}

fun dashboardCourseClickedEvent(courseId: String, courseName: String) {
Expand All @@ -308,9 +317,17 @@ class DashboardListViewModel(

private fun detectUnfulfilledPurchase() {
viewModelScope.launch(Dispatchers.IO) {
val enrolledCourses =
interactor.getAllUserCourses(status = CourseStatusFilter.ALL).courses
iapInteractor.detectUnfulfilledPurchase(
enrolledCourses = enrolledCourses,
purchaseVerified = { purchaseFlowData ->
eventLogger.apply {
this.purchaseFlowData = purchaseFlowData
this.logUnfulfilledPurchaseInitiatedEvent()
}
},
onSuccess = {
eventLogger.logUnfulfilledPurchaseInitiatedEvent()
_iapUiState.tryEmit(IAPUIState.PurchasesFulfillmentCompleted)
},
onFailure = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class SettingsFragment : Fragment() {
}

SettingsScreenAction.RestorePurchaseClick -> {
viewModel.restorePurchase()
viewModel.restorePurchasesClicked()
}

SettingsScreenAction.FeedbackFormClick -> {
Expand Down
Loading
Loading