Skip to content

Commit

Permalink
fix: Parity IAP analytics with legacy app (#55)
Browse files Browse the repository at this point in the history
fix: Parity IAP analytics with legacy app

- Update IAP analytics structure.
- Update flow_type for all the IAP analytics events
- Generalized event triggering in ViewModels
fix: LEARNER-10225
  • Loading branch information
omerhabib26 authored Oct 8, 2024
1 parent 5759db4 commit 578ce3f
Show file tree
Hide file tree
Showing 16 changed files with 200 additions and 223 deletions.
27 changes: 0 additions & 27 deletions app/src/main/java/org/openedx/app/AnalyticsManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import org.openedx.auth.presentation.AuthAnalytics
import org.openedx.core.config.Config
import org.openedx.core.presentation.CoreAnalytics
import org.openedx.core.presentation.IAPAnalytics
import org.openedx.core.presentation.IAPAnalyticsEvent
import org.openedx.core.presentation.IAPAnalyticsKeys
import org.openedx.core.presentation.IAPAnalyticsScreen
import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics
import org.openedx.core.presentation.iap.IAPFlow
import org.openedx.course.presentation.CourseAnalytics
import org.openedx.dashboard.presentation.DashboardAnalytics
import org.openedx.discovery.presentation.DiscoveryAnalytics
Expand Down Expand Up @@ -195,29 +191,6 @@ class AnalyticsManager(
put(Key.TOPIC_NAME.keyName, topicName)
})
}

override fun logIAPEvent(
event: IAPAnalyticsEvent,
params: MutableMap<String, Any?>,
screenName: String
) {
logEvent(
event = event.eventName,
params = params.apply {
put(IAPAnalyticsKeys.NAME.key, event.biValue)
put(IAPAnalyticsKeys.SCREEN_NAME.key, screenName)
put(
IAPAnalyticsKeys.IAP_FLOW_TYPE.key,
if (screenName == IAPAnalyticsScreen.PROFILE.screenName) {
IAPFlow.RESTORE.value
} else {
IAPFlow.SILENT.value
}
)
put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key)
}
)
}
}

enum class Event(val eventName: String) {
Expand Down
4 changes: 1 addition & 3 deletions app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import org.openedx.core.data.repository.iap.IAPRepository
import org.openedx.core.domain.interactor.IAPInteractor
import org.openedx.core.domain.model.iap.PurchaseFlowData
import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectDialogViewModel
import org.openedx.core.presentation.iap.IAPFlow
import org.openedx.core.presentation.iap.IAPViewModel
import org.openedx.core.presentation.settings.video.VideoQualityViewModel
import org.openedx.core.ui.WindowSize
Expand Down Expand Up @@ -451,9 +450,8 @@ val screenModule = module {

single { IAPRepository(get()) }
factory { IAPInteractor(get(), get(), get(), get(), get()) }
viewModel { (iapFlow: IAPFlow, purchaseFlowData: PurchaseFlowData) ->
viewModel { (purchaseFlowData: PurchaseFlowData) ->
IAPViewModel(
iapFlow = iapFlow,
purchaseFlowData = purchaseFlowData,
get(),
get(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import kotlinx.parcelize.Parcelize

@Parcelize
data class PurchaseFlowData(
var iapFlow: IAPFlow? = null,
var screenName: String? = null,
var courseId: String? = null,
var courseName: String? = null,
Expand All @@ -22,6 +23,7 @@ data class PurchaseFlowData(
var flowStartTime: Long = 0

fun reset() {
iapFlow = null
screenName = null
courseId = null
courseName = null
Expand All @@ -36,3 +38,19 @@ data class PurchaseFlowData(
flowStartTime = 0
}
}

enum class IAPFlow(val value: String) {
RESTORE("restore"),
SILENT("silent"),
USER_INITIATED("user_initiated");

fun value(): String {
return this.name.lowercase()
}
}

enum class IAPFlowSource(val screen: String) {
COURSE_ENROLLMENT("course_enrollment"),
COURSE_DASHBOARD("course_dashboard"),
PROFILE("profile"),
}
12 changes: 1 addition & 11 deletions core/src/main/java/org/openedx/core/presentation/IAPAnalytics.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package org.openedx.core.presentation

interface IAPAnalytics {
fun logIAPEvent(
event: IAPAnalyticsEvent,
params: MutableMap<String, Any?> = mutableMapOf(),
screenName: String,
)
fun logEvent(event: String, params: Map<String, Any?>)

fun logScreenEvent(screenName: String, params: Map<String, Any?>)
}
Expand Down Expand Up @@ -74,9 +70,3 @@ enum class IAPAnalyticsKeys(val key: String) {
SCREEN_NAME("screen_name"),
ERROR_ALERT_TYPE("error_alert_type"),
}

enum class IAPAnalyticsScreen(val screenName: String) {
COURSE_ENROLLMENT("course_enrollment"),
COURSE_DASHBOARD("course_dashboard"),
PROFILE("profile"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ import androidx.fragment.app.DialogFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.openedx.core.R
import org.openedx.core.domain.model.iap.IAPFlow
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.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
Expand All @@ -54,7 +53,6 @@ class IAPDialogFragment : DialogFragment() {

private val iapViewModel by viewModel<IAPViewModel> {
parametersOf(
requireArguments().serializable<IAPFlow>(ARG_IAP_FLOW),
requireArguments().parcelable<PurchaseFlowData>(ARG_PURCHASE_FLOW_DATA)
)
}
Expand Down Expand Up @@ -236,7 +234,6 @@ class IAPDialogFragment : DialogFragment() {
companion object {
const val TAG = "IAPDialogFragment"

private const val ARG_IAP_FLOW = "iap_flow"
private const val ARG_PURCHASE_FLOW_DATA = "purchase_flow_data"

fun newInstance(
Expand All @@ -246,10 +243,11 @@ class IAPDialogFragment : DialogFragment() {
courseName: String = "",
isSelfPaced: Boolean = false,
componentId: String? = null,
productInfo: ProductInfo? = null
productInfo: ProductInfo? = null,
): IAPDialogFragment {
val fragment = IAPDialogFragment()
val purchaseFlowData = PurchaseFlowData().apply {
this.iapFlow = iapFlow
this.screenName = screenName
this.courseId = courseId
this.courseName = courseName
Expand All @@ -259,7 +257,6 @@ class IAPDialogFragment : DialogFragment() {
}

fragment.arguments = bundleOf(
ARG_IAP_FLOW to iapFlow,
ARG_PURCHASE_FLOW_DATA to purchaseFlowData
)
return fragment
Expand Down
129 changes: 95 additions & 34 deletions core/src/main/java/org/openedx/core/presentation/iap/IAPEventLogger.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.openedx.core.presentation.iap

import com.android.billingclient.api.BillingClient
import org.openedx.core.domain.model.iap.IAPFlow
import org.openedx.core.domain.model.iap.PurchaseFlowData
import org.openedx.core.exception.iap.IAPException
import org.openedx.core.extension.isNull
import org.openedx.core.extension.isTrue
import org.openedx.core.extension.nonZero
import org.openedx.core.extension.takeIfNotEmpty
import org.openedx.core.presentation.IAPAnalytics
Expand All @@ -12,23 +15,24 @@ import org.openedx.core.utils.TimeUtils

class IAPEventLogger(
private val analytics: IAPAnalytics,
private val purchaseFlowData: PurchaseFlowData,
private val purchaseFlowData: PurchaseFlowData? = null,
private val isSilentIAPFlow: Boolean? = null,
) {
fun upgradeNowClickedEvent() {
logIAPEvent(IAPAnalyticsEvent.IAP_UPGRADE_NOW_CLICKED)
}

fun upgradeSuccessEvent() {
val elapsedTime = TimeUtils.getCurrentTime() - purchaseFlowData.flowStartTime
val elapsedTime = TimeUtils.getCurrentTime() - (purchaseFlowData?.flowStartTime ?: 0L)
logIAPEvent(IAPAnalyticsEvent.IAP_COURSE_UPGRADE_SUCCESS, buildMap {
put(IAPAnalyticsKeys.ELAPSED_TIME.key, elapsedTime)
}.toMutableMap())
})
}

private fun purchaseErrorEvent(error: String) {
logIAPEvent(IAPAnalyticsEvent.IAP_PAYMENT_ERROR, buildMap {
put(IAPAnalyticsKeys.ERROR.key, error)
}.toMutableMap())
})
}

private fun canceledByUserEvent() {
Expand All @@ -38,13 +42,13 @@ class IAPEventLogger(
private fun courseUpgradeErrorEvent(error: String) {
logIAPEvent(IAPAnalyticsEvent.IAP_COURSE_UPGRADE_ERROR, buildMap {
put(IAPAnalyticsKeys.ERROR.key, error)
}.toMutableMap())
})
}

private fun priceLoadErrorEvent(error: String) {
logIAPEvent(IAPAnalyticsEvent.IAP_PRICE_LOAD_ERROR, buildMap {
put(IAPAnalyticsKeys.ERROR.key, error)
}.toMutableMap())
})
}

fun logExceptionEvent(iapException: IAPException) {
Expand Down Expand Up @@ -74,58 +78,115 @@ class IAPEventLogger(
logIAPEvent(IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION, buildMap {
put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, alertType)
put(IAPAnalyticsKeys.ERROR_ACTION.key, action)
}.toMutableMap())
})
}

fun logRestorePurchasesClickedEvent() {
logIAPEvent(IAPAnalyticsEvent.IAP_RESTORE_PURCHASE_CLICKED)
}

fun logUnfulfilledPurchaseInitiatedEvent() {
logIAPEvent(IAPAnalyticsEvent.IAP_UNFULFILLED_PURCHASE_INITIATED)
}

fun logGetHelpEvent() {
logIAPEvent(
event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION,
params = buildMap {
put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action)
put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_GET_HELP.action)
})
}

fun logIAPCancelEvent() {
logIAPEvent(
event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION,
params = buildMap {
put(IAPAnalyticsKeys.ERROR_ALERT_TYPE.key, IAPAction.ACTION_UNFULFILLED.action)
put(IAPAnalyticsKeys.ERROR_ACTION.key, IAPAction.ACTION_CLOSE.action)
})
}

fun onRestorePurchaseCancel() {
logIAPEvent(
event = IAPAnalyticsEvent.IAP_ERROR_ALERT_ACTION,
params = buildMap {
put(IAPAnalyticsKeys.ACTION.key, IAPAction.ACTION_CLOSE.action)
})
}

fun loadIAPScreenEvent() {
val event = IAPAnalyticsEvent.IAP_VALUE_PROP_VIEWED
val params = buildMap {
put(IAPAnalyticsKeys.NAME.key, event.biValue)
purchaseFlowData.screenName?.takeIfNotEmpty()?.let { screenName ->
purchaseFlowData?.screenName?.takeIfNotEmpty()?.let { screenName ->
put(IAPAnalyticsKeys.SCREEN_NAME.key, screenName)
}
putAll(getIAPEventParams())
}
analytics.logScreenEvent(screenName = event.eventName, params = params)
}

private fun getIAPEventParams(): MutableMap<String, Any?> {
private fun getIAPEventParams(): Map<String, Any?> {
if (purchaseFlowData.isNull() || purchaseFlowData?.courseId.isNullOrEmpty()) return emptyMap()

return buildMap {
purchaseFlowData.takeIf { it.courseId.isNullOrBlank().not() }?.let {
put(IAPAnalyticsKeys.COURSE_ID.key, purchaseFlowData.courseId)
purchaseFlowData?.apply {
put(IAPAnalyticsKeys.COURSE_ID.key, courseId)
put(
IAPAnalyticsKeys.PACING.key,
if (purchaseFlowData.isSelfPaced == true) IAPAnalyticsKeys.SELF.key else IAPAnalyticsKeys.INSTRUCTOR.key
if (isSelfPaced.isTrue()) IAPAnalyticsKeys.SELF.key else IAPAnalyticsKeys.INSTRUCTOR.key
)
productInfo?.lmsUSDPrice?.nonZero()?.let { lmsUSDPrice ->
put(IAPAnalyticsKeys.LMS_USD_PRICE.key, lmsUSDPrice)
}
price.nonZero()?.let { localizedPrice ->
put(IAPAnalyticsKeys.LOCALIZED_PRICE.key, localizedPrice)
}
currencyCode.takeIfNotEmpty()?.let { currencyCode ->
put(IAPAnalyticsKeys.CURRENCY_CODE.key, currencyCode)
}
componentId?.takeIfNotEmpty()?.let { componentId ->
put(IAPAnalyticsKeys.COMPONENT_ID.key, componentId)
}
iapFlow?.let { iapFlow ->
put(IAPAnalyticsKeys.IAP_FLOW_TYPE.key, iapFlow.value)
}
put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key)
screenName?.takeIfNotEmpty()?.let { screenName ->
put(IAPAnalyticsKeys.SCREEN_NAME.key, screenName)
}
}
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)
}
}
}

private fun getUnfulfilledIAPEventParams(): Map<String, Any?> {
if (isSilentIAPFlow.isNull()) return emptyMap()

return buildMap {
put(IAPAnalyticsKeys.CATEGORY.key, IAPAnalyticsKeys.IN_APP_PURCHASES.key)
}.toMutableMap()
purchaseFlowData?.screenName?.takeIfNotEmpty()?.let { screenName ->
put(IAPAnalyticsKeys.SCREEN_NAME.key, screenName)
}
put(
IAPAnalyticsKeys.IAP_FLOW_TYPE.key,
if (isSilentIAPFlow.isTrue()) IAPFlow.SILENT.value else IAPFlow.RESTORE.value
)
}
}

private fun logIAPEvent(
event: IAPAnalyticsEvent,
params: MutableMap<String, Any?> = mutableMapOf(),
params: Map<String, Any?> = mutableMapOf(),
) {
params.apply {
put(IAPAnalyticsKeys.NAME.key, event.biValue)
putAll(getIAPEventParams())
}
analytics.logIAPEvent(
event = event,
params = params,
screenName = purchaseFlowData.screenName.orEmpty()
analytics.logEvent(
event = event.eventName,
params = buildMap {
put(IAPAnalyticsKeys.NAME.key, event.biValue)
putAll(params)
putAll(getIAPEventParams())
putAll(getUnfulfilledIAPEventParams())
},
)
}
}
10 changes: 0 additions & 10 deletions core/src/main/java/org/openedx/core/presentation/iap/IAPUIState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ enum class IAPLoaderType {
PRICE, PURCHASE_FLOW, FULL_SCREEN, RESTORE_PURCHASES
}

enum class IAPFlow(val value: String) {
RESTORE("restore"),
SILENT("silent"),
USER_INITIATED("user_initiated");

fun value(): String {
return this.name.lowercase()
}
}

enum class IAPAction(val action: String) {
ACTION_USER_INITIATED("user_initiated"),
ACTION_GET_HELP("get_help"),
Expand Down
Loading

0 comments on commit 578ce3f

Please sign in to comment.