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

feat: Calendar Sync Feature Analytics Implementation #386

Merged
merged 3 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
50 changes: 49 additions & 1 deletion Core/Core/Analytics/CoreAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public enum AnalyticsEvent: String {
case courseOutlineDiscussionTabClicked = "Course:Discussion Tab"
case courseOutlineHandoutsTabClicked = "Course:Handouts Tab"
case datesComponentClicked = "Dates:Course Component Clicked"
case datesCalendarSyncToggle = "Dates:CalendarSync Toggle"
case datesCalendarSyncDialogAction = "Dates:CalendarSync Dialog Action"
case datesCalendarSyncSnackbar = "Dates:CalendarSync Snackbar"
case plsBannerViewed = "PLS:Banner Viewed"
case plsShiftDatesClicked = "PLS:Shift Button Clicked"
case plsShiftDatesSuccess = "PLS:Shift Dates Success"
Expand Down Expand Up @@ -157,7 +160,10 @@ public enum EventBIValue: String {
case cookiePolicyClicked = "edx.bi.app.profile.cookie_policy.clicked"
case profileDeleteAccountClicked = "edx.bi.app.profile.delete_account.clicked"
case userLogout = "edx.bi.app.user.logout"
case datesComponentClicked = "edx.bi.app.coursedates.component.clicked"
case datesComponentClicked = "edx.bi.app.dates.component.clicked"
case datesCalendarSyncToggle = "edx.bi.app.dates.calendar_sync.toggle"
case datesCalendarSyncDialogAction = "edx.bi.app.dates.calendar_sync.dialog_action"
case datesCalendarSyncSnackbar = "edx.bi.app.dates.calendar_sync.snackbar"
case plsBannerViewed = "edx.bi.app.dates.pls_banner.viewed"
case plsShiftDatesClicked = "edx.bi.app.dates.pls_banner.shift_dates.clicked"
case plsShiftDatesSuccess = "edx.bi.app.dates.pls_banner.shift_dates.success"
Expand Down Expand Up @@ -233,6 +239,10 @@ public struct EventParamKey {
public static let noOfVideos = "number_of_videos"
public static let supported = "supported"
public static let conversion = "conversion"
public static let userType = "user_type"
public static let pacing = "pacing"
public static let dialog = "dialog"
public static let snackbar = "snackbar"
}

public struct EventCategory {
Expand All @@ -244,3 +254,41 @@ public struct EventCategory {
public static let video = "video"
public static let course = "course"
}

public enum EnrollmentMode: String {
case audit
case verified
case none
}

public enum Pacing: String {
case `self` = "self"
case instructor = "instructor"
}

public enum Action: String {
case on = "on"
case off = "off"
case allow = "Allow"
case doNotAllow = "donot_allow"
case ok = "OK"
case cancel = "Cancel"
case update = "Update"
case remove = "Remove"
case done = "Done"
case viewEvent = "view_event"
}

public enum Dialog: String {
case permission = "Permission"
case add = "Add"
case remove = "Remove"
case update = "Update"
case confirmed = "Confirmed"
}

public enum Snackbar: String {
case add = "Add"
case remove = "Remove"
case update = "Update"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if you could move this to Course Analytics and also keep all the actions, and stuff in snake case instead of camel case.

}
5 changes: 4 additions & 1 deletion Core/Core/Domain/Model/CourseBlockModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public struct CourseStructure: Equatable {
public var childs: [CourseChapter]
public let media: DataLayer.CourseMedia //FIXME Domain model
public let certificate: Certificate?
public let isSelfPaced: Bool

public init(
id: String,
Expand All @@ -33,7 +34,8 @@ public struct CourseStructure: Equatable {
topicID: String? = nil,
childs: [CourseChapter],
media: DataLayer.CourseMedia,
certificate: Certificate?
certificate: Certificate?,
isSelfPaced: Bool
) {
self.id = id
self.graded = graded
Expand All @@ -45,6 +47,7 @@ public struct CourseStructure: Equatable {
self.childs = childs
self.media = media
self.certificate = certificate
self.isSelfPaced = isSelfPaced
}

public func totalVideosSizeInBytes(downloadQuality: DownloadQuality) -> Int {
Expand Down
6 changes: 4 additions & 2 deletions Course/Course/Data/CourseRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ public class CourseRepository: CourseRepositoryProtocol {
topicID: courseBlock.userViewData?.topicID,
childs: childs,
media: course.media,
certificate: course.certificate?.domain
certificate: course.certificate?.domain,
isSelfPaced: course.isSelfPaced
)
}

Expand Down Expand Up @@ -346,7 +347,8 @@ And there are various ways of describing it-- call it oral poetry or
topicID: courseBlock.userViewData?.topicID,
childs: childs,
media: course.media,
certificate: course.certificate?.domain
certificate: course.certificate?.domain,
isSelfPaced: course.isSelfPaced
)
}

Expand Down
13 changes: 12 additions & 1 deletion Course/Course/Data/Model/Data_CourseOutlineResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,31 @@ public extension DataLayer {
public let id: String
public let media: DataLayer.CourseMedia
public let certificate: Certificate?
public let isSelfPaced: Bool

enum CodingKeys: String, CodingKey {
case blocks
case rootItem = "root"
case id
case media
case certificate
case isSelfPaced = "is_self_paced"
}

public init(rootItem: String, dict: Blocks, id: String, media: DataLayer.CourseMedia, certificate: Certificate?) {
public init(
rootItem: String,
dict: Blocks,
id: String,
media: DataLayer.CourseMedia,
certificate: Certificate?,
isSelfPaced: Bool
) {
self.rootItem = rootItem
self.dict = dict
self.id = id
self.media = media
self.certificate = certificate
self.isSelfPaced = isSelfPaced
}

public init(from decoder: Decoder) throws {
Expand All @@ -44,6 +54,7 @@ public extension DataLayer {
id = try values.decode(String.self, forKey: .id)
media = try values.decode(DataLayer.CourseMedia.self, forKey: .media)
certificate = try values.decode(Certificate.self, forKey: .certificate)
isSelfPaced = try values.decode(Bool.self, forKey: .isSelfPaced)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23D60" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23E214" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="CDCourseBlock" representedClassName="CDCourseBlock" syncable="YES" codeGenerationType="class">
<attribute name="allSources" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
<attribute name="blockId" optional="YES" attributeType="String"/>
Expand Down Expand Up @@ -79,6 +79,7 @@
<entity name="CDCourseStructure" representedClassName="CDCourseStructure" syncable="YES" codeGenerationType="class">
<attribute name="certificate" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="isSelfPaced" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mediaLarge" optional="YES" attributeType="String"/>
<attribute name="mediaRaw" optional="YES" attributeType="String"/>
<attribute name="mediaSmall" optional="YES" attributeType="String"/>
Expand Down
3 changes: 2 additions & 1 deletion Course/Course/Domain/CourseInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ public class CourseInteractor: CourseInteractorProtocol {
topicID: course.topicID,
childs: newChilds,
media: course.media,
certificate: course.certificate
certificate: course.certificate,
isSelfPaced: course.isSelfPaced
)
}

Expand Down
38 changes: 38 additions & 0 deletions Course/Course/Presentation/CourseAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ public protocol CourseAnalytics {
link: String,
supported: Bool
)
func calendarSyncToggle(
userType: EnrollmentMode,
pacing: Pacing,
courseId: String,
action: Action
)
func calendarSyncDialogAction(
userType: EnrollmentMode,
pacing: Pacing,
courseId: String,
dialog: Dialog,
action: Action
)
func calendarSyncSnackbar(
userType: EnrollmentMode,
pacing: Pacing,
courseId: String,
snackbar: Snackbar
)
func trackCourseEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String)
func plsEvent(
_ event: AnalyticsEvent,
Expand Down Expand Up @@ -87,6 +106,25 @@ class CourseAnalyticsMock: CourseAnalytics {
link: String,
supported: Bool
) {}
func calendarSyncToggle(
userType: EnrollmentMode,
pacing: Pacing,
courseId: String,
action: Action
) {}
func calendarSyncDialogAction(
userType: EnrollmentMode,
pacing: Pacing,
courseId: String,
dialog: Dialog,
action: Action
) {}
func calendarSyncSnackbar(
userType: EnrollmentMode,
pacing: Pacing,
courseId: String,
snackbar: Snackbar
) {}
public func trackCourseEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) {}
public func plsEvent(
_ event: AnalyticsEvent,
Expand Down
50 changes: 50 additions & 0 deletions Course/Course/Presentation/Dates/CourseDatesViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ public class CourseDatesViewModel: ObservableObject {
}
set {
if newValue {
trackCalendarSyncToggle(action: .on)
handleCalendar()
} else {
trackCalendarSyncToggle(action: .off)
showRemoveCalendarAlert()
}
}
Expand Down Expand Up @@ -217,8 +219,14 @@ extension CourseDatesViewModel {
guard let self else { return }
switch status {
case .authorized:
if previousStatus == .notDetermined {
trackCalendarSyncDialogAction(dialog: .permission, action: .allow)
}
showAddCalendarAlert()
default:
if previousStatus == .notDetermined {
trackCalendarSyncDialogAction(dialog: .permission, action: .doNotAllow)
}
isOn = false
if previousStatus == status {
self.showCalendarSettingsAlert()
Expand Down Expand Up @@ -260,11 +268,13 @@ extension CourseDatesViewModel {
),
positiveAction: CoreLocalization.Alert.accept,
onCloseTapped: { [weak self] in
self?.trackCalendarSyncDialogAction(dialog: .add, action: .cancel)
self?.router.dismiss(animated: true)
self?.isOn = false
self?.calendar.syncOn = false
},
okTapped: { [weak self] in
self?.trackCalendarSyncDialogAction(dialog: .add, action: .ok)
self?.router.dismiss(animated: true)
Task { [weak self] in
await self?.addCourseEvents()
Expand All @@ -283,11 +293,14 @@ extension CourseDatesViewModel {
),
positiveAction: CoreLocalization.Alert.accept,
onCloseTapped: { [weak self] in
self?.trackCalendarSyncDialogAction(dialog: .remove, action: .cancel)
self?.router.dismiss(animated: true)
},
okTapped: { [weak self] in
self?.trackCalendarSyncDialogAction(dialog: .remove, action: .ok)
self?.router.dismiss(animated: true)
self?.removeCourseCalendar { [weak self] _ in
self?.trackCalendarSyncSnackbar(snackbar: .remove)
self?.eventState = .removedCalendar
}

Expand All @@ -298,6 +311,7 @@ extension CourseDatesViewModel {

private func showEventsAddedSuccessAlert() {
if calendar.isModalPresented {
trackCalendarSyncSnackbar(snackbar: .add)
eventState = .addedCalendar
return
}
Expand All @@ -309,11 +323,13 @@ extension CourseDatesViewModel {
),
positiveAction: CourseLocalization.CourseDates.calendarViewEvents,
onCloseTapped: { [weak self] in
self?.trackCalendarSyncDialogAction(dialog: .confirmed, action: .done)
self?.router.dismiss(animated: true)
self?.isOn = true
self?.calendar.syncOn = true
},
okTapped: { [weak self] in
self?.trackCalendarSyncDialogAction(dialog: .confirmed, action: .viewEvent)
self?.router.dismiss(animated: true)
if let url = URL(string: "calshow://"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
Expand Down Expand Up @@ -350,20 +366,24 @@ extension CourseDatesViewModel {
positiveAction: CourseLocalization.CourseDates.calendarShiftPromptUpdateNow,
onCloseTapped: { [weak self] in
// Remove course calendar
self?.trackCalendarSyncDialogAction(dialog: .update, action: .remove)
self?.router.dismiss(animated: true)
self?.removeCourseCalendar { [weak self] _ in
self?.trackCalendarSyncSnackbar(snackbar: .remove)
self?.eventState = .removedCalendar
}
},
okTapped: { [weak self] in
// Update Calendar Now
self?.trackCalendarSyncDialogAction(dialog: .update, action: .update)
self?.router.dismiss(animated: true)
self?.removeCourseCalendar(trackAnalytics: false) { success in
self?.isOn = !success
self?.calendar.syncOn = false
self?.addCourseEvents(trackAnalytics: false) { [weak self] calendarEventsAdded in
self?.isOn = calendarEventsAdded
if calendarEventsAdded {
self?.trackCalendarSyncSnackbar(snackbar: .update)
self?.calendar.syncOn = calendarEventsAdded
self?.eventState = .updatedCalendar
}
Expand Down Expand Up @@ -444,3 +464,33 @@ extension CourseDatesViewModel {
)
}
}

extension CourseDatesViewModel {
private func trackCalendarSyncToggle(action: Action) {
analytics.calendarSyncToggle(
userType: .none,
pacing: courseStructure?.isSelfPaced ?? true ? .`self` : .instructor,
courseId: courseID,
action: action
)
}

private func trackCalendarSyncDialogAction(dialog: Dialog, action: Action) {
analytics.calendarSyncDialogAction(
userType: .none,
pacing: courseStructure?.isSelfPaced ?? true ? .`self` : .instructor,
courseId: courseID,
dialog: dialog,
action: action
)
}

private func trackCalendarSyncSnackbar(snackbar: Snackbar) {
analytics.calendarSyncSnackbar(
userType: .none,
pacing: courseStructure?.isSelfPaced ?? true ? .`self` : .instructor,
courseId: courseID,
snackbar: snackbar
)
}
}
Loading
Loading