From 5498a1c96744c12024cc760766cc74f1c725ec2c Mon Sep 17 00:00:00 2001 From: Vadim Kuznetsov Date: Thu, 20 Jun 2024 12:55:35 +0300 Subject: [PATCH 1/8] [iOS] Prompt | Confirmation Alert Missing Before Deleting Section-Level Downloaded Videos #456 (#463) * fix: confirmation alert * chore: changed string to localization * chore: review required changes --- .../CourseVideoDownloadBarViewModel.swift | 4 ++-- .../Subviews/CustomDisclosureGroup.swift | 23 ++++++++++++++++++- Course/Course/SwiftGen/Strings.swift | 2 ++ Course/Course/en.lproj/Localizable.strings | 1 + Course/Course/uk.lproj/Localizable.strings | 1 + 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift index 5caa8ece4..841c49c1f 100644 --- a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift +++ b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift @@ -124,7 +124,7 @@ final class CourseVideoDownloadBarViewModel: ObservableObject { func onToggle() async { if allVideosDownloaded { courseViewModel.router.presentAlert( - alertTitle: "Warning", + alertTitle: CourseLocalization.Alert.warning, alertMessage: "\(CourseLocalization.Alert.deleteAllVideos) \"\(courseStructure.displayName)\"?", positiveAction: CoreLocalization.Alert.delete, onCloseTapped: { [weak self] in @@ -145,7 +145,7 @@ final class CourseVideoDownloadBarViewModel: ObservableObject { if isOn { courseViewModel.router.presentAlert( - alertTitle: "Warning", + alertTitle: CourseLocalization.Alert.warning, alertMessage: "\(CourseLocalization.Alert.stopDownloading) \"\(courseStructure.displayName)\"", positiveAction: CoreLocalization.Alert.accept, onCloseTapped: { [weak self] in diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index 75186dd91..10ab50625 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -50,7 +50,24 @@ struct CustomDisclosureGroup: View { let state = downloadAllButtonState(for: chapter) { Button( action: { - downloadAllSubsections(in: chapter, state: state) + switch state { + case .finished: + viewModel.router.presentAlert( + alertTitle: CourseLocalization.Alert.warning, + alertMessage: deleteMessage(for: chapter), + positiveAction: CoreLocalization.Alert.delete, + onCloseTapped: { + viewModel.router.dismiss(animated: true) + }, + okTapped: { + downloadAllSubsections(in: chapter, state: state) + viewModel.router.dismiss(animated: true) + }, + type: .deleteVideo + ) + default: + downloadAllSubsections(in: chapter, state: state) + } }, label: { switch state { case .available: @@ -175,6 +192,10 @@ struct CustomDisclosureGroup: View { } } + private func deleteMessage(for chapter: CourseChapter) -> String { + "\(CourseLocalization.Alert.deleteVideos) \"\(chapter.displayName)\"?" + } + func getAssignmentStatus(for date: Date) -> String { let calendar = Calendar.current let today = Date() diff --git a/Course/Course/SwiftGen/Strings.swift b/Course/Course/SwiftGen/Strings.swift index 1cada7b12..91e479f03 100644 --- a/Course/Course/SwiftGen/Strings.swift +++ b/Course/Course/SwiftGen/Strings.swift @@ -37,6 +37,8 @@ public enum CourseLocalization { public static let rotateDevice = CourseLocalization.tr("Localizable", "ALERT.ROTATE_DEVICE", fallback: "Rotate your device to view this video in full screen.") /// Turning off the switch will stop downloading and delete all downloaded videos for public static let stopDownloading = CourseLocalization.tr("Localizable", "ALERT.STOP_DOWNLOADING", fallback: "Turning off the switch will stop downloading and delete all downloaded videos for") + /// Warning + public static let warning = CourseLocalization.tr("Localizable", "ALERT.WARNING", fallback: "Warning") } public enum Course { /// Due Today diff --git a/Course/Course/en.lproj/Localizable.strings b/Course/Course/en.lproj/Localizable.strings index 40a4d8157..f4a37b8a4 100644 --- a/Course/Course/en.lproj/Localizable.strings +++ b/Course/Course/en.lproj/Localizable.strings @@ -34,6 +34,7 @@ "ALERT.DELETE_ALL_VIDEOS" = "Are you sure you want to delete all video(s) for"; "ALERT.DELETE_VIDEOS" = "Are you sure you want to delete video(s) for"; "ALERT.STOP_DOWNLOADING" = "Turning off the switch will stop downloading and delete all downloaded videos for"; +"ALERT.WARNING" = "Warning"; "COURSE_CONTAINER.HOME" = "Home"; "COURSE_CONTAINER.VIDEOS" = "Videos"; diff --git a/Course/Course/uk.lproj/Localizable.strings b/Course/Course/uk.lproj/Localizable.strings index abb9dc970..b5a3746f6 100644 --- a/Course/Course/uk.lproj/Localizable.strings +++ b/Course/Course/uk.lproj/Localizable.strings @@ -33,6 +33,7 @@ "ALERT.DELETE_ALL_VIDEOS" = "Are you sure you want to delete all video(s) for"; "ALERT.DELETE_VIDEOS" = "Are you sure you want to delete video(s) for"; "ALERT.STOP_DOWNLOADING" = "Turning off the switch will stop downloading and delete all downloaded videos for"; +"ALERT.WARNING" = "Warning"; "COURSE_CONTAINER.COURSE" = "Курс"; "COURSE_CONTAINER.VIDEOS" = "Всі відео"; From b2539a6f0ee00a06879f3820d438430a4346363e Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:45:34 +0300 Subject: [PATCH 2/8] feat: [FC-0047] FCM (#461) * feat: firebase cloud messaging * fix: address feedback * fix: address feedback --- .../Presentation/Login/SignInViewModel.swift | 2 + .../Registration/SignUpViewModel.swift | 3 +- Core/Core/Data/CoreStorage.swift | 2 + Core/Core/Extensions/Notification.swift | 11 ++- Core/Core/Network/RequestInterceptor.swift | 6 +- .../Presentation/ListDashboardViewModel.swift | 9 ++ .../PrimaryCourseDashboardViewModel.swift | 10 ++ .../Data/Model/Data_CommentsResponse.swift | 8 +- .../Data/Network/DiscussionEndpoint.swift | 5 +- .../Data/Network/DiscussionRepository.swift | 70 +++++--------- OpenEdX.xcodeproj/project.pbxproj | 37 +++++++ OpenEdX/AppDelegate.swift | 68 +++++++++---- OpenEdX/DI/AppAssembly.swift | 9 +- OpenEdX/Data/AppStorage.swift | 65 ++++++++----- .../Data/Network/NotificationsEndpoints.swift | 44 +++++++++ .../DeepLinkManager/DeepLinkManager.swift | 79 +++++++++++---- .../DeepLinkRouter/DeepLinkRouter.swift | 6 +- .../DeepLinkManager/Link/DeepLink.swift | 16 +++- .../FirebaseAnalyticsService.swift | 8 +- .../Listeners/BrazeListener.swift | 14 +++ .../Listeners/FCMListener.swift | 23 ++++- .../Providers/BrazeProvider.swift | 6 ++ .../Providers/FCMProvider.swift | 49 +++++++++- .../PushNotificationsManager.swift | 96 ++++++++++++++----- OpenEdX/Router.swift | 4 + .../Settings/SettingsViewModel.swift | 5 + 26 files changed, 500 insertions(+), 155 deletions(-) create mode 100644 OpenEdX/Data/Network/NotificationsEndpoints.swift diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index 041c98ca7..ef573e047 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -82,6 +82,7 @@ public class SignInViewModel: ObservableObject { analytics.identify(id: "\(user.id)", username: user.username, email: user.email) analytics.userLogin(method: .password) router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + NotificationCenter.default.post(name: .userAuthorized, object: nil) } catch let error { failure(error) } @@ -113,6 +114,7 @@ public class SignInViewModel: ObservableObject { analytics.identify(id: "\(user.id)", username: user.username, email: user.email) analytics.userLogin(method: authMethod) router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + NotificationCenter.default.post(name: .userAuthorized, object: nil) } catch let error { failure(error, authMethod: authMethod) } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift index 1f57b8c02..5c41badef 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift @@ -136,7 +136,7 @@ public class SignUpViewModel: ObservableObject { analytics.registrationSuccess(method: authMetod.analyticsValue) isShowProgress = false router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) - + NotificationCenter.default.post(name: .userAuthorized, object: nil) } catch let error { isShowProgress = false if case APIError.invalidGrant = error { @@ -193,6 +193,7 @@ public class SignUpViewModel: ObservableObject { analytics.userLogin(method: authMethod) isShowProgress = false router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + NotificationCenter.default.post(name: .userAuthorized, object: nil) } catch { update(fullName: response.name, email: response.email) self.externalToken = response.token diff --git a/Core/Core/Data/CoreStorage.swift b/Core/Core/Data/CoreStorage.swift index 5acf16b66..60837da41 100644 --- a/Core/Core/Data/CoreStorage.swift +++ b/Core/Core/Data/CoreStorage.swift @@ -10,6 +10,7 @@ import Foundation public protocol CoreStorage { var accessToken: String? {get set} var refreshToken: String? {get set} + var pushToken: String? {get set} var appleSignFullName: String? {get set} var appleSignEmail: String? {get set} var cookiesDate: String? {get set} @@ -25,6 +26,7 @@ public protocol CoreStorage { public class CoreStorageMock: CoreStorage { public var accessToken: String? public var refreshToken: String? + public var pushToken: String? public var appleSignFullName: String? public var appleSignEmail: String? public var cookiesDate: String? diff --git a/Core/Core/Extensions/Notification.swift b/Core/Core/Extensions/Notification.swift index d8e99731b..1a4aeb1db 100644 --- a/Core/Core/Extensions/Notification.swift +++ b/Core/Core/Extensions/Notification.swift @@ -8,6 +8,8 @@ import Foundation public extension Notification.Name { + static let userAuthorized = Notification.Name("userAuthorized") + static let userLoggedOut = Notification.Name("userLoggedOut") static let onCourseEnrolled = Notification.Name("onCourseEnrolled") static let onblockCompletionRequested = Notification.Name("onblockCompletionRequested") static let onTokenRefreshFailed = Notification.Name("onTokenRefreshFailed") @@ -15,8 +17,15 @@ public extension Notification.Name { static let onAppUpgradeAccountSettingsTapped = Notification.Name("onAppUpgradeAccountSettingsTapped") static let onNewVersionAvaliable = Notification.Name("onNewVersionAvaliable") static let webviewReloadNotification = Notification.Name("webviewReloadNotification") - static let onBlockCompletion = Notification.Name.init("onBlockCompletion") + static let onBlockCompletion = Notification.Name("onBlockCompletion") static let shiftCourseDates = Notification.Name("shiftCourseDates") static let profileUpdated = Notification.Name("profileUpdated") static let getCourseDates = Notification.Name("getCourseDates") + static let refreshEnrollments = Notification.Name("refreshEnrollments") +} + +public extension Notification { + enum UserInfoKey: String { + case isForced + } } diff --git a/Core/Core/Network/RequestInterceptor.swift b/Core/Core/Network/RequestInterceptor.swift index f27b6f310..860fa070d 100644 --- a/Core/Core/Network/RequestInterceptor.swift +++ b/Core/Core/Network/RequestInterceptor.swift @@ -93,7 +93,11 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor { } self.requestsToRetry.removeAll() } else { - NotificationCenter.default.post(name: .onTokenRefreshFailed, object: nil) + NotificationCenter.default.post( + name: .userLoggedOut, + object: nil, + userInfo: [Notification.UserInfoKey.isForced: true] + ) } } } diff --git a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift index 4e79877c2..bef00add1 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift @@ -30,6 +30,7 @@ public class ListDashboardViewModel: ObservableObject { private let interactor: DashboardInteractorProtocol private let analytics: DashboardAnalytics private var onCourseEnrolledCancellable: AnyCancellable? + private var refreshEnrollmentsCancellable: AnyCancellable? public init(interactor: DashboardInteractorProtocol, connectivity: ConnectivityProtocol, @@ -46,6 +47,14 @@ public class ListDashboardViewModel: ObservableObject { await self.getMyCourses(page: 1, refresh: true) } } + refreshEnrollmentsCancellable = NotificationCenter.default + .publisher(for: .refreshEnrollments) + .sink { [weak self] _ in + guard let self = self else { return } + Task { + await self.getMyCourses(page: 1, refresh: true) + } + } } @MainActor diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index 7b3a51e37..60ffe9c0b 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -48,6 +48,7 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { let enrollmentPublisher = NotificationCenter.default.publisher(for: .onCourseEnrolled) let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) + let refreshEnrollmentsPublisher = NotificationCenter.default.publisher(for: .refreshEnrollments) enrollmentPublisher .sink { [weak self] _ in @@ -64,6 +65,15 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { updateEnrollmentsIfNeeded() } .store(in: &cancellables) + + refreshEnrollmentsPublisher + .sink { [weak self] _ in + guard let self = self else { return } + Task { + await self.getEnrollments() + } + } + .store(in: &cancellables) } private func updateEnrollmentsIfNeeded() { diff --git a/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift b/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift index 0665d1cac..995917921 100644 --- a/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift +++ b/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift @@ -48,6 +48,7 @@ public extension DataLayer { public let childCount: Int public let children: [String] public let users: Users? + public let profileImage: ProfileImage? enum CodingKeys: String, CodingKey { case id = "id" @@ -71,6 +72,7 @@ public extension DataLayer { case childCount = "child_count" case children = "children" case users + case profileImage = "profile_image" } public init( @@ -94,7 +96,8 @@ public extension DataLayer { endorsedAt: String?, childCount: Int, children: [String], - users: Users? + users: Users?, + profileImage: ProfileImage? ) { self.id = id self.author = author @@ -117,6 +120,7 @@ public extension DataLayer { self.childCount = childCount self.children = children self.users = users + self.profileImage = profileImage } } } @@ -125,7 +129,7 @@ public extension DataLayer.Comments { var domain: UserComment { UserComment( authorName: author ?? DiscussionLocalization.anonymous, - authorAvatar: users?.userName?.profile?.image?.imageURLLarge ?? "", + authorAvatar: users?.userName?.profile?.image?.imageURLLarge ?? profileImage?.imageURLFull ?? "", postDate: Date(iso8601: createdAt), postTitle: "", postBody: rawBody, diff --git a/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift b/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift index 4409f22dc..1d433aa9b 100644 --- a/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift +++ b/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift @@ -186,7 +186,10 @@ enum DiscussionEndpoint: EndPointType { } return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) case .getThread: - return .requestParameters(parameters: [:], encoding: URLEncoding.queryString) + return .requestParameters( + parameters: ["requested_fields": "profile_image"], + encoding: URLEncoding.queryString + ) case .getTopics: return .requestParameters(encoding: URLEncoding.queryString) case let .getTopic(_, topicID): diff --git a/Discussion/Discussion/Data/Network/DiscussionRepository.swift b/Discussion/Discussion/Data/Network/DiscussionRepository.swift index 9ece98508..661ae603f 100644 --- a/Discussion/Discussion/Data/Network/DiscussionRepository.swift +++ b/Discussion/Discussion/Data/Network/DiscussionRepository.swift @@ -32,9 +32,6 @@ public protocol DiscussionRepositoryProtocol { func followThread(following: Bool, threadID: String) async throws func createNewThread(newThread: DiscussionNewThread) async throws func readBody(threadID: String) async throws - func renameThreadUser(data: Data) async throws -> DataLayer.ThreadListsResponse - func renameUsers(data: Data) async throws -> DataLayer.CommentsResponse - func renameUsersInJSON(stringJSON: String) -> String } public class DiscussionRepository: DiscussionRepositoryProtocol { @@ -66,21 +63,20 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { let threads = try await api.requestData(DiscussionEndpoint .getThreads(courseID: courseID, type: type, sort: sort, filter: filter, page: page)) - return try await renameThreadUser(data: threads).domain + return try await renameThreadListUser(data: threads).domain } public func getThread(threadID: String) async throws -> UserThread { let thread = try await api.requestData(DiscussionEndpoint .getThread(threadID: threadID)) - .mapResponse(DataLayer.ThreadList.self) - return thread.userThread + return try await renameThreadUser(data: thread).userThread } public func searchThreads(courseID: String, searchText: String, pageNumber: Int) async throws -> ThreadLists { let posts = try await api.requestData(DiscussionEndpoint.searchThreads(courseID: courseID, searchText: searchText, pageNumber: pageNumber)) - return try await renameThreadUser(data: posts).domain + return try await renameThreadListUser(data: posts).domain } public func getTopics(courseID: String) async throws -> Topics { @@ -158,7 +154,7 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { _ = try await api.request(DiscussionEndpoint.readBody(threadID: threadID)) } - public func renameThreadUser(data: Data) async throws -> DataLayer.ThreadListsResponse { + private func renameThreadListUser(data: Data) async throws -> DataLayer.ThreadListsResponse { var modifiedJSON = "" let parsed = try data.mapResponse(DataLayer.ThreadListsResponse.self) @@ -176,7 +172,25 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { } } - public func renameUsers(data: Data) async throws -> DataLayer.CommentsResponse { + private func renameThreadUser(data: Data) async throws -> DataLayer.ThreadList { + var modifiedJSON = "" + let parsed = try data.mapResponse(DataLayer.ThreadList.self) + + if let stringJSON = String(data: data, encoding: .utf8) { + modifiedJSON = renameUsersInJSON(stringJSON: stringJSON) + if let modifiedParsed = try modifiedJSON.data(using: .utf8)?.mapResponse( + DataLayer.ThreadList.self + ) { + return modifiedParsed + } else { + return parsed + } + } else { + return parsed + } + } + + private func renameUsers(data: Data) async throws -> DataLayer.CommentsResponse { var modifiedJSON = "" let parsed = try data.mapResponse(DataLayer.CommentsResponse.self) @@ -192,7 +206,7 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { } } - public func renameUsersInJSON(stringJSON: String) -> String { + private func renameUsersInJSON(stringJSON: String) -> String { var modifiedJSON = stringJSON let userNames = stringJSON.find(from: "\"users\":{\"", to: "\":{\"profile\":") if userNames.count >= 1 { @@ -478,42 +492,6 @@ public class DiscussionRepositoryMock: DiscussionRepositoryProtocol { public func readBody(threadID: String) async throws { } - - public func renameThreadUser(data: Data) async throws -> DataLayer.ThreadListsResponse { - DataLayer.ThreadListsResponse(threads: [], - textSearchRewrite: "", - pagination: DataLayer.Pagination(next: "", previous: "", count: 0, numPages: 0) ) - } - - public func renameUsers(data: Data) async throws -> DataLayer.CommentsResponse { - DataLayer.CommentsResponse( - comments: [ - DataLayer.Comments(id: "", author: "Bill", - authorLabel: nil, - createdAt: "25.11.2043", - updatedAt: "25.11.2043", - rawBody: "Raw Body", - renderedBody: "Rendered body", - abuseFlagged: false, - voted: true, - voteCount: 2, - editableFields: [], - canDelete: true, - threadID: "", - parentID: nil, - endorsed: false, - endorsedBy: nil, - endorsedByLabel: nil, - endorsedAt: nil, - childCount: 0, - children: [], - users: nil) - ], pagination: DataLayer.Pagination(next: nil, previous: nil, count: 0, numPages: 0)) - } - - public func renameUsersInJSON(stringJSON: String) -> String { - return stringJSON - } } #endif // swiftlint:enable all diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 8341e2233..5ab7602d4 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -40,6 +40,9 @@ 0770DE4C28D0A462006D8A5D /* Authorization.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0770DE4A28D0A462006D8A5D /* Authorization.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0770DE5028D0A707006D8A5D /* NetworkAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE4F28D0A707006D8A5D /* NetworkAssembly.swift */; }; 0770DE6428D0BCC7006D8A5D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE6628D0BCC7006D8A5D /* Localizable.strings */; }; + 0780ABE32BFBA2E40093A4A6 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 0780ABE22BFBA2E40093A4A6 /* FirebaseAnalytics */; }; + 0780ABE52BFBA2E40093A4A6 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 0780ABE42BFBA2E40093A4A6 /* FirebaseMessaging */; }; + 0780ABE82BFCA1530093A4A6 /* NotificationsEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0780ABE72BFCA1530093A4A6 /* NotificationsEndpoints.swift */; }; 07A7D78F28F5C9060000BE81 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07A7D78E28F5C9060000BE81 /* Core.framework */; }; 07A7D79028F5C9060000BE81 /* Core.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 07A7D78E28F5C9060000BE81 /* Core.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 07D5DA3528D075AA00752FD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */; }; @@ -124,6 +127,7 @@ 0770DE4A28D0A462006D8A5D /* Authorization.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Authorization.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0770DE4F28D0A707006D8A5D /* NetworkAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAssembly.swift; sourceTree = ""; }; 0770DE6528D0BCC7006D8A5D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 0780ABE72BFCA1530093A4A6 /* NotificationsEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsEndpoints.swift; sourceTree = ""; }; 07A7D78E28F5C9060000BE81 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 07D5DA3128D075AA00752FD9 /* OpenEdX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenEdX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -165,7 +169,9 @@ A5462D9F2B865713003B96A5 /* Segment in Frameworks */, 072787B128D34D83002E9142 /* Discovery.framework in Frameworks */, 0218196428F734FA00202564 /* Discussion.framework in Frameworks */, + 0780ABE52BFBA2E40093A4A6 /* FirebaseMessaging in Frameworks */, 0219C67728F4347600D64452 /* Course.framework in Frameworks */, + 0780ABE32BFBA2E40093A4A6 /* FirebaseAnalytics in Frameworks */, 027DB33028D8A063002B6862 /* Dashboard.framework in Frameworks */, A5BD3E302B83B0F7006A8983 /* SegmentFirebase in Frameworks */, A51CDBED2B6D2BEE009B6D4E /* SegmentBraze in Frameworks */, @@ -180,6 +186,7 @@ 0293A2012A6FC9E30090A336 /* Data */ = { isa = PBXGroup; children = ( + 0780ABE62BFC9C9D0093A4A6 /* Network */, 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */, 0293A2022A6FCA590090A336 /* CorePersistence.swift */, 0293A2042A6FCD430090A336 /* CoursePersistence.swift */, @@ -209,6 +216,14 @@ path = DI; sourceTree = ""; }; + 0780ABE62BFC9C9D0093A4A6 /* Network */ = { + isa = PBXGroup; + children = ( + 0780ABE72BFCA1530093A4A6 /* NotificationsEndpoints.swift */, + ); + path = Network; + sourceTree = ""; + }; 07D5DA2828D075AA00752FD9 = { isa = PBXGroup; children = ( @@ -410,6 +425,8 @@ A51CDBEE2B6D2BEE009B6D4E /* SegmentBrazeUI */, A5BD3E2F2B83B0F7006A8983 /* SegmentFirebase */, A5462D9E2B865713003B96A5 /* Segment */, + 0780ABE22BFBA2E40093A4A6 /* FirebaseAnalytics */, + 0780ABE42BFBA2E40093A4A6 /* FirebaseMessaging */, ); productName = OpenEdX; productReference = 07D5DA3128D075AA00752FD9 /* OpenEdX.app */; @@ -445,6 +462,7 @@ A51CDBEB2B6D2BEE009B6D4E /* XCRemoteSwiftPackageReference "braze-segment-swift" */, A5BD3E2E2B83B0F7006A8983 /* XCRemoteSwiftPackageReference "analytics-swift-firebase" */, A5462D9D2B865713003B96A5 /* XCRemoteSwiftPackageReference "analytics-swift" */, + 0780ABE12BFBA2E40093A4A6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, ); productRefGroup = 07D5DA3228D075AA00752FD9 /* Products */; projectDirPath = ""; @@ -560,6 +578,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0780ABE82BFCA1530093A4A6 /* NotificationsEndpoints.swift in Sources */, A500668B2B613ED10024680B /* PushNotificationsManager.swift in Sources */, 0293A2052A6FCD430090A336 /* CoursePersistence.swift in Sources */, 020CA5D92AA0A25300970AAF /* AppStorage.swift in Sources */, @@ -1211,6 +1230,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 0780ABE12BFBA2E40093A4A6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 10.26.0; + }; + }; A51CDBEB2B6D2BEE009B6D4E /* XCRemoteSwiftPackageReference "braze-segment-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/braze-inc/braze-segment-swift"; @@ -1246,6 +1273,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 0780ABE22BFBA2E40093A4A6 /* FirebaseAnalytics */ = { + isa = XCSwiftPackageProductDependency; + package = 0780ABE12BFBA2E40093A4A6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAnalytics; + }; + 0780ABE42BFBA2E40093A4A6 /* FirebaseMessaging */ = { + isa = XCSwiftPackageProductDependency; + package = 0780ABE12BFBA2E40093A4A6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseMessaging; + }; A51CDBEC2B6D2BEE009B6D4E /* SegmentBraze */ = { isa = XCSwiftPackageProductDependency; package = A51CDBEB2B6D2BEE009B6D4E /* XCRemoteSwiftPackageReference "braze-segment-swift" */; diff --git a/OpenEdX/AppDelegate.swift b/OpenEdX/AppDelegate.swift index 59138b48c..36cea0ba7 100644 --- a/OpenEdX/AppDelegate.swift +++ b/OpenEdX/AppDelegate.swift @@ -12,6 +12,9 @@ import Profile import GoogleSignIn import FacebookCore import MSAL +import UserNotifications +import FirebaseCore +import FirebaseMessaging import Theme @UIApplicationMain @@ -32,6 +35,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { initDI() + if let config = Container.shared.resolve(ConfigProtocol.self) { Theme.Shapes.isRoundedCorners = config.theme.isRoundedCorners @@ -42,6 +46,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) } configureDeepLinkServices(launchOptions: launchOptions) + + let pushManager = Container.shared.resolve(PushNotificationsManager.self) + + if config.firebase.enabled { + FirebaseApp.configure() + if config.firebase.cloudMessagingEnabled { + Messaging.messaging().delegate = pushManager + UNUserNotificationCenter.current().delegate = pushManager + } + } + + if pushManager?.hasProviders == true { + UIApplication.shared.registerForRemoteNotifications() + } } Theme.Fonts.registerFonts() @@ -49,17 +67,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window?.rootViewController = RouteController() window?.makeKeyAndVisible() window?.tintColor = Theme.UIColors.accentColor - + NotificationCenter.default.addObserver( self, - selector: #selector(forceLogoutUser), - name: .onTokenRefreshFailed, + selector: #selector(didUserAuthorize), + name: .userAuthorized, object: nil ) - if let pushManager = Container.shared.resolve(PushNotificationsManager.self) { - pushManager.performRegistration() - } + NotificationCenter.default.addObserver( + self, + selector: #selector(didUserLogout), + name: .userLoggedOut, + object: nil + ) return true } @@ -120,21 +141,31 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) } - @objc private func forceLogoutUser() { + @objc private func didUserAuthorize() { + Container.shared.resolve(PushNotificationsManager.self)?.synchronizeToken() + } + + @objc func didUserLogout(_ notification: Notification) { guard Date().timeIntervalSince1970 - lastForceLogoutTime > 5 else { return } - let analyticsManager = Container.shared.resolve(AnalyticsManager.self) - analyticsManager?.userLogout(force: true) - - lastForceLogoutTime = Date().timeIntervalSince1970 - - Container.shared.resolve(CoreStorage.self)?.clear() - Task { - await Container.shared.resolve(DownloadManagerProtocol.self)?.deleteAllFiles() + if let userInfo = notification.userInfo, + userInfo[Notification.UserInfoKey.isForced] as? Bool == true { + let analyticsManager = Container.shared.resolve(AnalyticsManager.self) + analyticsManager?.userLogout(force: true) + + lastForceLogoutTime = Date().timeIntervalSince1970 + + Container.shared.resolve(CoreStorage.self)?.clear() + Task { + await Container.shared.resolve(DownloadManagerProtocol.self)?.deleteAllFiles() + } + Container.shared.resolve(CoreDataHandlerProtocol.self)?.clear() + window?.rootViewController = RouteController() } - Container.shared.resolve(CoreDataHandlerProtocol.self)?.clear() - window?.rootViewController = RouteController() + + UNUserNotificationCenter.current().removeAllDeliveredNotifications() + Container.shared.resolve(PushNotificationsManager.self)?.refreshToken() } // Push Notifications @@ -151,8 +182,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { - guard let pushManager = Container.shared.resolve(PushNotificationsManager.self) - else { + guard let pushManager = Container.shared.resolve(PushNotificationsManager.self) else { completionHandler(.newData) return } diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 6b722f8c2..cabc57d33 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -174,6 +174,9 @@ class AppAssembly: Assembly { container.register(PushNotificationsManager.self) { r in PushNotificationsManager( + deepLinkManager: r.resolve(DeepLinkManager.self)!, + storage: r.resolve(CoreStorage.self)!, + api: r.resolve(API.self)!, config: r.resolve(ConfigProtocol.self)! ) }.inObjectScope(.container) @@ -196,10 +199,8 @@ class AppAssembly: Assembly { ) }.inObjectScope(.container) - container.register(FirebaseAnalyticsService.self) { r in - FirebaseAnalyticsService( - config: r.resolve(ConfigProtocol.self)! - ) + container.register(FirebaseAnalyticsService.self) { _ in + FirebaseAnalyticsService() }.inObjectScope(.container) container.register(PipManagerProtocol.self) { r in diff --git a/OpenEdX/Data/AppStorage.swift b/OpenEdX/Data/AppStorage.swift index e21a94513..0d75f2851 100644 --- a/OpenEdX/Data/AppStorage.swift +++ b/OpenEdX/Data/AppStorage.swift @@ -34,45 +34,29 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } } } - - public var reviewLastShownVersion: String? { + + public var refreshToken: String? { get { - return userDefaults.string(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + return keychain.get(KEY_REFRESH_TOKEN) } set(newValue) { if let newValue { - userDefaults.set(newValue, forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + keychain.set(newValue, forKey: KEY_REFRESH_TOKEN) } else { - userDefaults.removeObject(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + keychain.delete(KEY_REFRESH_TOKEN) } } } - public var lastReviewDate: Date? { - get { - guard let dateString = userDefaults.string(forKey: KEY_REVIEW_LAST_REVIEW_DATE) else { - return nil - } - return Date(iso8601: dateString) - } - set(newValue) { - if let newValue { - userDefaults.set(newValue.dateToString(style: .iso8601), forKey: KEY_REVIEW_LAST_REVIEW_DATE) - } else { - userDefaults.removeObject(forKey: KEY_REVIEW_LAST_REVIEW_DATE) - } - } - } - - public var refreshToken: String? { + public var pushToken: String? { get { - return keychain.get(KEY_REFRESH_TOKEN) + return keychain.get(KEY_PUSH_TOKEN) } set(newValue) { if let newValue { - keychain.set(newValue, forKey: KEY_REFRESH_TOKEN) + keychain.set(newValue, forKey: KEY_PUSH_TOKEN) } else { - keychain.delete(KEY_REFRESH_TOKEN) + keychain.delete(KEY_PUSH_TOKEN) } } } @@ -116,6 +100,35 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } } + public var reviewLastShownVersion: String? { + get { + return userDefaults.string(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + } else { + userDefaults.removeObject(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + } + } + } + + public var lastReviewDate: Date? { + get { + guard let dateString = userDefaults.string(forKey: KEY_REVIEW_LAST_REVIEW_DATE) else { + return nil + } + return Date(iso8601: dateString) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue.dateToString(style: .iso8601), forKey: KEY_REVIEW_LAST_REVIEW_DATE) + } else { + userDefaults.removeObject(forKey: KEY_REVIEW_LAST_REVIEW_DATE) + } + } + } + public var whatsNewVersion: String? { get { return userDefaults.string(forKey: KEY_WHATSNEW_VERSION) @@ -222,10 +235,12 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto refreshToken = nil cookiesDate = nil user = nil + userProfile = nil } private let KEY_ACCESS_TOKEN = "accessToken" private let KEY_REFRESH_TOKEN = "refreshToken" + private let KEY_PUSH_TOKEN = "pushToken" private let KEY_COOKIES_DATE = "cookiesDate" private let KEY_USER_PROFILE = "userProfile" private let KEY_USER = "refreshToken" diff --git a/OpenEdX/Data/Network/NotificationsEndpoints.swift b/OpenEdX/Data/Network/NotificationsEndpoints.swift new file mode 100644 index 000000000..8b5639f6c --- /dev/null +++ b/OpenEdX/Data/Network/NotificationsEndpoints.swift @@ -0,0 +1,44 @@ +// +// NotificationsEndpoints.swift +// OpenEdX +// +// Created by Volodymyr Chekyrta on 21.05.24. +// + +import Foundation +import Core +import Alamofire + +enum NotificationsEndpoints: EndPointType { + + case syncFirebaseToken(token: String) + + var path: String { + switch self { + case .syncFirebaseToken: + return "/api/mobile/v4/notifications/create-token/" + } + } + + var httpMethod: HTTPMethod { + switch self { + case .syncFirebaseToken: + return .post + } + } + + var headers: HTTPHeaders? { + nil + } + + var task: HTTPTask { + switch self { + case let .syncFirebaseToken(token): + let params: [String: Encodable] = [ + "registration_id": token, + "active": true + ] + return .requestParameters(parameters: params, encoding: URLEncoding.httpBody) + } + } +} diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift index be43a71c6..20262a0ef 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift @@ -38,11 +38,11 @@ public class DeepLinkManager { private let discussionInteractor: DiscussionInteractorProtocol private let courseInteractor: CourseInteractorProtocol private let profileInteractor: ProfileInteractorProtocol - + var userloggedIn: Bool { - return !(storage.user?.username?.isEmpty ?? true) - } - + return !(storage.user?.username?.isEmpty ?? true) + } + public init( config: ConfigProtocol, router: DeepLinkRouter, @@ -59,7 +59,7 @@ public class DeepLinkManager { self.discussionInteractor = discussionInteractor self.courseInteractor = courseInteractor self.profileInteractor = profileInteractor - + services = servicesFor(config: config) } @@ -102,9 +102,9 @@ public class DeepLinkManager { guard link.type != .none else { return } - + let isAppActive = UIApplication.shared.applicationState == .active - + Task { if isAppActive { await showNotificationAlert(link) @@ -124,11 +124,11 @@ public class DeepLinkManager { } } } - + @MainActor private func showNotificationAlert(_ link: PushLink) { router.dismissPresentedViewController() - + router.presentAlert( alertTitle: link.title ?? "", alertMessage: link.body ?? "", @@ -148,17 +148,19 @@ public class DeepLinkManager { type: .deepLink ) } - + private func isDiscovery(type: DeepLinkType) -> Bool { type == .discovery || type == .discoveryCourseDetail || type == .discoveryProgramDetail } - + private func isDiscussionThreads(type: DeepLinkType) -> Bool { type == .discussionPost || type == .discussionTopic || - type == .discussionComment + type == .discussionComment || + type == .forumResponse || + type == .forumComment } private func isHandout(type: DeepLinkType) -> Bool { @@ -195,9 +197,14 @@ public class DeepLinkManager { .courseAnnouncement, .discussionTopic, .discussionPost, + .forumResponse, + .forumComment, .discussionComment, .courseComponent: await showCourseScreen(with: type, link: link) + case .enroll, .addBetaTester: + await showCourseScreen(with: type, link: link) + NotificationCenter.default.post(name: .refreshEnrollments, object: nil) case .program, .programDetail: guard config.program.enabled else { return } if let pathID = link.pathID, !pathID.isEmpty { @@ -209,6 +216,9 @@ public class DeepLinkManager { router.showTabScreen(tab: .profile) case .userProfile: await showEditProfile() + case .unenroll, .removeBetaTester: + router.showTabScreen(tab: .dashboard) + NotificationCenter.default.post(name: .refreshEnrollments, object: nil) default: break } @@ -248,9 +258,8 @@ public class DeepLinkManager { link: link, courseDetails: courseDetails ) { [weak self] in - guard let self else { - return - } + guard let self else { return } + guard courseDetails.isEnrolled else { return } if self.isHandout(type: type) { self.router.showProgress() @@ -344,7 +353,6 @@ public class DeepLinkManager { isBlackedOut: isBlackedOut ) case .discussionPost: - if let topicID = link.topicID, !topicID.isEmpty, let topics = try? await discussionInteractor.getTopic( @@ -367,8 +375,7 @@ public class DeepLinkManager { isBlackedOut: isBlackedOut ) } - - case .discussionComment: + case .discussionComment, .forumResponse: if let topicID = link.topicID, !topicID.isEmpty, let topics = try? await discussionInteractor.getTopic( @@ -404,6 +411,42 @@ public class DeepLinkManager { isBlackedOut: isBlackedOut ) } + case .forumComment: + if let topicID = link.topicID, + !topicID.isEmpty, + let topics = try? await discussionInteractor.getTopic( + courseID: courseDetails.courseID, + topicID: topicID + ) { + router.showThreads( + topicID: topicID, + courseDetails: courseDetails, + topics: topics, + isBlackedOut: isBlackedOut + ) + } + + if let threadID = link.threadID, + !threadID.isEmpty, + let userThread = try? await discussionInteractor.getThread(threadID: threadID) { + router.showThread( + userThread: userThread, + isBlackedOut: isBlackedOut + ) + } + + if let parentID = link.parentID, + !parentID.isEmpty, + let comment = try? await self.discussionInteractor.getResponse(responseID: parentID), + let commentParentID = comment.parentID, + !commentParentID.isEmpty, + let parentComment = try? await self.discussionInteractor.getResponse(responseID: commentParentID) { + router.showComment( + comment: comment, + parentComment: parentComment.post, + isBlackedOut: isBlackedOut + ) + } default: break } diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift index 0ae41e30a..15f026235 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift @@ -112,7 +112,7 @@ extension Router: DeepLinkRouter { courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle, + title: courseDetails.courseTitle, showDates: false, lastVisitedBlockID: nil ) @@ -131,7 +131,9 @@ extension Router: DeepLinkRouter { .courseHandout, .courseAnnouncement, .courseDashboard, - .courseComponent: + .courseComponent, + .enroll, + .addBetaTester: popToCourseContainerView(animated: false) default: break diff --git a/OpenEdX/Managers/DeepLinkManager/Link/DeepLink.swift b/OpenEdX/Managers/DeepLinkManager/Link/DeepLink.swift index 1da67d716..90ca14792 100644 --- a/OpenEdX/Managers/DeepLinkManager/Link/DeepLink.swift +++ b/OpenEdX/Managers/DeepLinkManager/Link/DeepLink.swift @@ -26,6 +26,12 @@ enum DeepLinkType: String { case programDetail = "program_detail" case userProfile = "user_profile" case profile = "profile" + case forumResponse = "forum_response" + case forumComment = "forum_comment" + case enroll = "enroll" + case unenroll = "unenroll" + case addBetaTester = "add_beta_tester" + case removeBetaTester = "remove_beta_tester" case none } @@ -33,30 +39,38 @@ private enum DeepLinkKeys: String, RawStringExtractable { case courseID = "course_id" case pathID = "path_id" case screenName = "screen_name" + case notificationType = "notification_type" case topicID = "topic_id" case threadID = "thread_id" case commentID = "comment_id" + case parentID = "parent_id" case componentID = "component_id" } public class DeepLink { let courseID: String? let screenName: String? + let notificationType: String? let pathID: String? let topicID: String? let threadID: String? let commentID: String? + let parentID: String? let componentID: String? var type: DeepLinkType init(dictionary: [AnyHashable: Any]) { courseID = dictionary[DeepLinkKeys.courseID.rawValue] as? String screenName = dictionary[DeepLinkKeys.screenName.rawValue] as? String + notificationType = dictionary[DeepLinkKeys.notificationType.rawValue] as? String pathID = dictionary[DeepLinkKeys.pathID.rawValue] as? String topicID = dictionary[DeepLinkKeys.topicID.rawValue] as? String threadID = dictionary[DeepLinkKeys.threadID.rawValue] as? String commentID = dictionary[DeepLinkKeys.commentID.rawValue] as? String componentID = dictionary[DeepLinkKeys.componentID.rawValue] as? String - type = DeepLinkType(rawValue: screenName ?? DeepLinkType.none.rawValue) ?? .none + parentID = dictionary[DeepLinkKeys.parentID.rawValue] as? String + type = DeepLinkType( + rawValue: screenName ?? notificationType ?? DeepLinkType.none.rawValue + ) ?? .none } } diff --git a/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift b/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift index 12bd225e8..fe49bc007 100644 --- a/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift +++ b/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift @@ -6,19 +6,13 @@ // import Foundation -import Firebase import Core +import FirebaseAnalytics private let MaxParameterValueCharacters = 100 private let MaxNameValueCharacters = 40 class FirebaseAnalyticsService: AnalyticsService { - // Init manager - public init(config: ConfigProtocol) { - guard config.firebase.enabled && config.firebase.isAnalyticsSourceFirebase else { return } - - FirebaseApp.configure() - } func identify(id: String, username: String?, email: String?) { Analytics.setUserID(id) diff --git a/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift b/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift index 5d049e02e..b117843fc 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift @@ -8,9 +8,23 @@ import Foundation class BrazeListener: PushNotificationsListener { + + private let deepLinkManager: DeepLinkManager + + init(deepLinkManager: DeepLinkManager) { + self.deepLinkManager = deepLinkManager + } + func shouldListenNotification(userinfo: [AnyHashable: Any]) -> Bool { //A push notification sent from the braze has a key ab in it like ab = {c = "c_value";}; let data = userinfo["ab"] as? [String: Any] return userinfo.count > 0 && data != nil } + + func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { + guard let dictionary = userInfo as? [String: AnyHashable], + shouldListenNotification(userinfo: userInfo) else { return } + let link = PushLink(dictionary: dictionary) + deepLinkManager.processLinkFromNotification(link) + } } diff --git a/OpenEdX/Managers/PushNotificationsManager/Listeners/FCMListener.swift b/OpenEdX/Managers/PushNotificationsManager/Listeners/FCMListener.swift index b0ed7f5f8..5d951e1a8 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Listeners/FCMListener.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Listeners/FCMListener.swift @@ -6,8 +6,29 @@ // import Foundation +import FirebaseMessaging class FCMListener: PushNotificationsListener { + + private let deepLinkManager: DeepLinkManager + + init(deepLinkManager: DeepLinkManager) { + self.deepLinkManager = deepLinkManager + } + // check if userinfo contains data for this Listener - func shouldListenNotification(userinfo: [AnyHashable: Any]) -> Bool { false } + func shouldListenNotification(userinfo: [AnyHashable: Any]) -> Bool { + let data = userinfo["gcm.message_id"] + return userinfo.count > 0 && data != nil + } + + func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { + guard let dictionary = userInfo as? [String: AnyHashable], + shouldListenNotification(userinfo: userInfo) else { return } + // With swizzling disabled you must let Messaging know about the message, for Analytics + Messaging.messaging().appDidReceiveMessage(userInfo) + + let link = PushLink(dictionary: dictionary) + deepLinkManager.processLinkFromNotification(link) + } } diff --git a/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift b/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift index 9e45059e3..43ca0c780 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift @@ -25,4 +25,10 @@ class BrazeProvider: PushNotificationsProvider { func didFailToRegisterForRemoteNotificationsWithError(error: Error) { } + + func synchronizeToken() { + } + + func refreshToken() { + } } diff --git a/OpenEdX/Managers/PushNotificationsManager/Providers/FCMProvider.swift b/OpenEdX/Managers/PushNotificationsManager/Providers/FCMProvider.swift index 4e66a2a30..6a418ce5f 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Providers/FCMProvider.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Providers/FCMProvider.swift @@ -6,13 +6,58 @@ // import Foundation +import Core +import FirebaseCore +import FirebaseMessaging -class FCMProvider: PushNotificationsProvider { +class FCMProvider: NSObject, PushNotificationsProvider, MessagingDelegate { + + private var storage: CoreStorage + private let api: API + + init(storage: CoreStorage, api: API) { + self.storage = storage + self.api = api + } + func didRegisterWithDeviceToken(deviceToken: Data) { - + Messaging.messaging().apnsToken = deviceToken } func didFailToRegisterForRemoteNotificationsWithError(error: Error) { + } + + func synchronizeToken() { + guard let fcmToken = storage.pushToken, storage.user != nil else { return } + sendFCMToken(fcmToken) + } + + func refreshToken() { + Messaging.messaging().deleteToken { error in + if let error = error { + debugLog("Error deleting FCM token: \(error.localizedDescription)") + } else { + Messaging.messaging().token { token, error in + if let error = error { + debugLog("Error fetching FCM token: \(error.localizedDescription)") + } else if let token = token { + debugLog("FCM token fetched: \(token)") + } + } + } + } + } + + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + storage.pushToken = fcmToken + guard let fcmToken, storage.user != nil else { return } + sendFCMToken(fcmToken) + } + + private func sendFCMToken(_ token: String) { + Task { + try? await api.request(NotificationsEndpoints.syncFirebaseToken(token: token)) + } } } diff --git a/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift b/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift index be832bcbb..1f6ab02b4 100644 --- a/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift +++ b/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift @@ -8,11 +8,15 @@ import Foundation import Core import UIKit -import Swinject +import UserNotifications +import FirebaseCore +import FirebaseMessaging public protocol PushNotificationsProvider { func didRegisterWithDeviceToken(deviceToken: Data) func didFailToRegisterForRemoteNotificationsWithError(error: Error) + func synchronizeToken() + func refreshToken() } protocol PushNotificationsListener { @@ -20,45 +24,46 @@ protocol PushNotificationsListener { func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) } -extension PushNotificationsListener { - func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { - guard let dictionary = userInfo as? [String: AnyHashable], - shouldListenNotification(userinfo: userInfo), - let deepLinkManager = Container.shared.resolve(DeepLinkManager.self) - else { return } - let link = PushLink(dictionary: dictionary) - deepLinkManager.processLinkFromNotification(link) - } -} - -class PushNotificationsManager { +class PushNotificationsManager: NSObject { + + private let deepLinkManager: DeepLinkManager + private let storage: CoreStorage + private let api: API private var providers: [PushNotificationsProvider] = [] private var listeners: [PushNotificationsListener] = [] + public var hasProviders: Bool { + providers.count > 0 + } + // Init manager - public init(config: ConfigProtocol) { + public init(deepLinkManager: DeepLinkManager, storage: CoreStorage, api: API, config: ConfigProtocol) { + self.deepLinkManager = deepLinkManager + self.storage = storage + self.api = api + super.init() providers = providersFor(config: config) listeners = listenersFor(config: config) } private func providersFor(config: ConfigProtocol) -> [PushNotificationsProvider] { var pushProviders: [PushNotificationsProvider] = [] - if config.firebase.cloudMessagingEnabled { - pushProviders.append(FCMProvider()) - } if config.braze.pushNotificationsEnabled { pushProviders.append(BrazeProvider()) } + if config.firebase.cloudMessagingEnabled { + pushProviders.append(FCMProvider(storage: storage, api: api)) + } return pushProviders } private func listenersFor(config: ConfigProtocol) -> [PushNotificationsListener] { var pushListeners: [PushNotificationsListener] = [] - if config.firebase.cloudMessagingEnabled { - pushListeners.append(FCMListener()) - } if config.braze.pushNotificationsEnabled { - pushListeners.append(BrazeListener()) + pushListeners.append(BrazeListener(deepLinkManager: deepLinkManager)) + } + if config.firebase.cloudMessagingEnabled { + pushListeners.append(FCMListener(deepLinkManager: deepLinkManager)) } return pushListeners } @@ -67,9 +72,7 @@ class PushNotificationsManager { public func performRegistration() { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } + debugLog("Permission for push notifications granted.") } else if let error = error { debugLog("Push notifications permission error: \(error.localizedDescription)") } else { @@ -94,4 +97,49 @@ class PushNotificationsManager { listener.didReceiveRemoteNotification(userInfo: userInfo) } } + + func synchronizeToken() { + for provider in providers { + provider.synchronizeToken() + } + } + + func refreshToken() { + for provider in providers { + provider.refreshToken() + } + } +} + +// MARK: - MessagingDelegate +extension PushNotificationsManager: MessagingDelegate { + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + for provider in providers where provider is MessagingDelegate { + (provider as? MessagingDelegate)?.messaging?(messaging, didReceiveRegistrationToken: fcmToken) + } + } +} + +// MARK: - UNUserNotificationCenterDelegate +extension PushNotificationsManager: UNUserNotificationCenterDelegate { + @MainActor + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification + ) async -> UNNotificationPresentationOptions { + if UIApplication.shared.applicationState == .active { + didReceiveRemoteNotification(userInfo: notification.request.content.userInfo) + } + + return [[.list, .banner, .sound]] + } + + @MainActor + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse + ) async { + let userInfo = response.notification.request.content.userInfo + didReceiveRemoteNotification(userInfo: userInfo) + } } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 8dbbafa20..fce1cca5d 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -380,6 +380,10 @@ public class Router: AuthorizationRouter, lastVisitedBlockID: lastVisitedBlockID ) navigationController.pushViewController(controller, animated: true) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + Container.shared.resolve(PushNotificationsManager.self)?.performRegistration() + } } public func getCourseScreensController( diff --git a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift index e31e09eea..ce6c4477b 100644 --- a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift +++ b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift @@ -139,6 +139,11 @@ public class SettingsViewModel: ObservableObject { try? await downloadManager.cancelAllDownloading() router.showStartupScreen() analytics.userLogout(force: false) + NotificationCenter.default.post( + name: .userLoggedOut, + object: nil, + userInfo: [Notification.UserInfoKey.isForced: false] + ) } func trackProfileVideoSettingsClicked() { From ea6e24e8ac0012397a502e9e3b99ea8967dc5453 Mon Sep 17 00:00:00 2001 From: Shafqat Muneer Date: Wed, 26 Jun 2024 16:47:20 +0500 Subject: [PATCH 3/8] feat: Program Screen Error Handling (#448) --- Core/Core.xcodeproj/project.pbxproj | 12 ++ .../FullScreenErrorView.swift | 97 ++++++++++++ Core/Core/View/Base/Webview/WebView.swift | 29 +++- Course/Course.xcodeproj/project.pbxproj | 14 +- .../CalendarSyncProgressView.swift | 0 .../DatesSuccessView}/DatesSuccessView.swift | 0 .../Presentation/Unit/CourseUnitView.swift | 32 +--- .../WebDiscovery/DiscoveryWebview.swift | 129 +++++++++------- .../DiscoveryWebviewViewModel.swift | 6 + .../WebPrograms/ProgramWebviewView.swift | 139 +++++++++++------- .../WebPrograms/ProgramWebviewViewModel.swift | 5 + 11 files changed, 321 insertions(+), 142 deletions(-) create mode 100644 Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift rename Course/Course/{Views => Presentation/Subviews/CalendarSyncProgressView}/CalendarSyncProgressView.swift (100%) rename Course/Course/{Views => Presentation/Subviews/DatesSuccessView}/DatesSuccessView.swift (100%) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index ad4325f39..3e59ea038 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -144,6 +144,7 @@ 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */; }; 142EDD6C2B831D1400F9F320 /* BranchSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 142EDD6B2B831D1400F9F320 /* BranchSDK */; }; 14769D3C2B9822EE00AB36D4 /* CoreAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14769D3B2B9822EE00AB36D4 /* CoreAnalytics.swift */; }; + 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */; }; A51CDBE72B6D21F2009B6D4E /* SegmentConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */; }; A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; }; A595689B2B6173DF00ED4F90 /* BranchConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A595689A2B6173DF00ED4F90 /* BranchConfig.swift */; }; @@ -339,6 +340,7 @@ 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = ""; }; 60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = ""; }; + 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullScreenErrorView.swift; sourceTree = ""; }; 9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = ""; }; A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentConfig.swift; sourceTree = ""; }; A53A32342B233DEC005FE38A /* ThemeConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeConfig.swift; sourceTree = ""; }; @@ -715,6 +717,7 @@ 0770DE7728D0C49E006D8A5D /* Base */ = { isa = PBXGroup; children = ( + 9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */, 064987882B4D69FE0071642A /* Webview */, E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */, 02A4833B29B8C57800D33F33 /* DownloadView.swift */, @@ -766,6 +769,14 @@ path = Analytics; sourceTree = ""; }; + 9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */ = { + isa = PBXGroup; + children = ( + 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */, + ); + path = FullScreenErrorView; + sourceTree = ""; + }; BA30427C2B20B235009B64B7 /* SocialAuth */ = { isa = PBXGroup; children = ( @@ -1124,6 +1135,7 @@ 0260E58028FD792800BBBE18 /* WebUnitViewModel.swift in Sources */, 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */, 06078B702BA49C3100576798 /* Dictionary+JSON.swift in Sources */, + 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */, 027BD3AE2909475000392132 /* KeyboardScrollerOptions.swift in Sources */, BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */, 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */, diff --git a/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift b/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift new file mode 100644 index 000000000..dc5893621 --- /dev/null +++ b/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift @@ -0,0 +1,97 @@ +// +// FullScreenErrorView.swift +// Course +// +// Created by Shafqat Muneer on 5/14/24. +// + +import SwiftUI +import Theme + +public struct FullScreenErrorView: View { + + public enum ErrorType { + case noInternet + case noInternetWithReload + case generic + } + + private let errorType: ErrorType + private var action: () -> Void = {} + + public init( + type: ErrorType + ) { + self.errorType = type + } + + public init( + type: ErrorType, + action: @escaping () -> Void + ) { + self.errorType = type + self.action = action + } + + public var body: some View { + GeometryReader { proxy in + VStack(spacing: 28) { + Spacer() + switch errorType { + case .noInternet, .noInternetWithReload: + CoreAssets.noWifi.swiftUIImage + .renderingMode(.template) + .foregroundStyle(Color.primary) + .scaledToFit() + + Text(CoreLocalization.Error.Internet.noInternetTitle) + .font(Theme.Fonts.titleLarge) + .foregroundColor(Theme.Colors.textPrimary) + + Text(CoreLocalization.Error.Internet.noInternetDescription) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + case .generic: + CoreAssets.notAvaliable.swiftUIImage + .renderingMode(.template) + .foregroundStyle(Color.primary) + .scaledToFit() + + Text(CoreLocalization.View.Snackbar.tryAgainBtn) + .font(Theme.Fonts.titleLarge) + .foregroundColor(Theme.Colors.textPrimary) + + Text(CoreLocalization.Error.unknownError) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + } + + if errorType != .noInternet { + UnitButtonView( + type: .reload, + action: { + self.action() + } + ) + } + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: proxy.size.height) + .background( + Theme.Colors.background + ) + } + } +} + +#if DEBUG +struct FullScreenErrorView_Previews: PreviewProvider { + static var previews: some View { + FullScreenErrorView(type: .noInternetWithReload) + } +} +#endif diff --git a/Core/Core/View/Base/Webview/WebView.swift b/Core/Core/View/Base/Webview/WebView.swift index cb36ea255..1b12167db 100644 --- a/Core/Core/View/Base/Webview/WebView.swift +++ b/Core/Core/View/Base/Webview/WebView.swift @@ -17,6 +17,8 @@ public protocol WebViewNavigationDelegate: AnyObject { shouldLoad request: URLRequest, navigationAction: WKNavigationAction ) async -> Bool + + func showWebViewError() } public struct WebView: UIViewRepresentable { @@ -39,17 +41,20 @@ public struct WebView: UIViewRepresentable { var webViewNavDelegate: WebViewNavigationDelegate? var refreshCookies: () async -> Void + var webViewType: String? public init( viewModel: ViewModel, isLoading: Binding, refreshCookies: @escaping () async -> Void, - navigationDelegate: WebViewNavigationDelegate? = nil + navigationDelegate: WebViewNavigationDelegate? = nil, + webViewType: String? = nil ) { self.viewModel = viewModel self._isLoading = isLoading self.refreshCookies = refreshCookies self.webViewNavDelegate = navigationDelegate + self.webViewType = webViewType } public class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler { @@ -70,6 +75,10 @@ public struct WebView: UIViewRepresentable { public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { webView.isHidden = false + DispatchQueue.main.async { + self.parent.isLoading = false + self.parent.webViewNavDelegate?.showWebViewError() + } } public func webView( @@ -78,6 +87,10 @@ public struct WebView: UIViewRepresentable { withError error: Error ) { webView.isHidden = false + DispatchQueue.main.async { + self.parent.isLoading = false + self.parent.webViewNavDelegate?.showWebViewError() + } } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { @@ -172,7 +185,7 @@ public struct WebView: UIViewRepresentable { private func addObservers() { cancellables.removeAll() - NotificationCenter.default.publisher(for: .webviewReloadNotification, object: nil) + NotificationCenter.default.publisher(for: Notification.Name(parent.webViewType ?? ""), object: nil) .sink { [weak self] _ in self?.reload() } @@ -188,8 +201,16 @@ public struct WebView: UIViewRepresentable { fileprivate var webview: WKWebView? @objc private func reload() { - parent.isLoading = true - webview?.reload() + DispatchQueue.main.async { + self.parent.isLoading = true + } + if webview?.url?.absoluteString.isEmpty ?? true, + let url = URL(string: parent.viewModel.url) { + let request = URLRequest(url: url) + webview?.load(request) + } else { + webview?.reload() + } } public func userContentController( diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index efc45a860..cc778b214 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -314,7 +314,6 @@ 02B6B3B528E1D10700232911 /* Domain */, 02EAE2CA28E1F0A700529644 /* Presentation */, 97EA4D822B84EFA900663F58 /* Managers */, - 97CA95212B875EA200A9EDEA /* Views */, 02B6B3B428E1C49400232911 /* Localizable.strings */, 02C355372C08DCD700501342 /* Localizable.stringsdict */, ); @@ -529,13 +528,20 @@ path = Mock; sourceTree = ""; }; - 97CA95212B875EA200A9EDEA /* Views */ = { + 9784D4752BF39EEF00AFEFFF /* CalendarSyncProgressView */ = { isa = PBXGroup; children = ( 97C99C352B9A08FE004EEDE2 /* CalendarSyncProgressView.swift */, + ); + path = CalendarSyncProgressView; + sourceTree = ""; + }; + 9784D4762BF39EFD00AFEFFF /* DatesSuccessView */ = { + isa = PBXGroup; + children = ( 97CA95242B875EE200A9EDEA /* DatesSuccessView.swift */, ); - path = Views; + path = DatesSuccessView; sourceTree = ""; }; 97EA4D822B84EFA900663F58 /* Managers */ = { @@ -586,6 +592,8 @@ BAD9CA482B2C88D500DE790A /* Subviews */ = { isa = PBXGroup; children = ( + 9784D4762BF39EFD00AFEFFF /* DatesSuccessView */, + 9784D4752BF39EEF00AFEFFF /* CalendarSyncProgressView */, 02D4FC2C2BBD7C7500C47748 /* MessageSectionView */, BAC0E0D92B32F0A2006B68A9 /* CourseVideoDownloadBarView */, BA58CF622B471047005B102E /* VideoDownloadQualityBarView */, diff --git a/Course/Course/Views/CalendarSyncProgressView.swift b/Course/Course/Presentation/Subviews/CalendarSyncProgressView/CalendarSyncProgressView.swift similarity index 100% rename from Course/Course/Views/CalendarSyncProgressView.swift rename to Course/Course/Presentation/Subviews/CalendarSyncProgressView/CalendarSyncProgressView.swift diff --git a/Course/Course/Views/DatesSuccessView.swift b/Course/Course/Presentation/Subviews/DatesSuccessView/DatesSuccessView.swift similarity index 100% rename from Course/Course/Views/DatesSuccessView.swift rename to Course/Course/Presentation/Subviews/DatesSuccessView/DatesSuccessView.swift diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 44f1e4a1b..0248e2393 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -189,7 +189,7 @@ public struct CourseUnitView: View { Spacer(minLength: 150) } } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { @@ -219,7 +219,7 @@ public struct CourseUnitView: View { Spacer(minLength: 150) } } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } // MARK: Web @@ -233,7 +233,7 @@ public struct CourseUnitView: View { ) // not need to add frame limit there because we did that with injection } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -247,7 +247,7 @@ public struct CourseUnitView: View { Spacer() .frame(minHeight: 100) } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -275,7 +275,7 @@ public struct CourseUnitView: View { //No need iPad paddings there bacause they were added //to PostsView that placed inside DiscussionView } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -586,25 +586,3 @@ struct CourseUnitView_Previews: PreviewProvider { } //swiftlint:enable all #endif - -struct NoInternetView: View { - - var body: some View { - VStack(spacing: 28) { - Spacer() - CoreAssets.noWifi.swiftUIImage - .renderingMode(.template) - .foregroundStyle(Color.primary) - .scaledToFit() - Text(CoreLocalization.Error.Internet.noInternetTitle) - .font(Theme.Fonts.titleLarge) - .foregroundColor(Theme.Colors.textPrimary) - Text(CoreLocalization.Error.Internet.noInternetDescription) - .font(Theme.Fonts.bodyLarge) - .foregroundColor(Theme.Colors.textPrimary) - .multilineTextAlignment(.center) - .padding(.horizontal, 50) - Spacer() - }.frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift index be01b0be4..b69bb3af9 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift @@ -14,13 +14,24 @@ public enum DiscoveryWebviewType: Equatable { case discovery case courseDetail(String) case programDetail(String) + + var rawValue: String { + switch self { + case .discovery: + return "discovery" + case .courseDetail(let value): + return "courseDetail(\(value))" + case .programDetail(let value): + return "programDetail(\(value))" + } + } } public struct DiscoveryWebview: View { @State private var searchQuery: String = "" @State private var isLoading: Bool = true - @ObservedObject private var viewModel: DiscoveryWebviewViewModel + @StateObject private var viewModel: DiscoveryWebviewViewModel private var router: DiscoveryRouter private var discoveryType: DiscoveryWebviewType public var pathID: String @@ -70,78 +81,86 @@ public struct DiscoveryWebview: View { discoveryType: DiscoveryWebviewType = .discovery, pathID: String = "" ) { - self.viewModel = viewModel + self._viewModel = .init(wrappedValue: viewModel) self.router = router self._searchQuery = State(initialValue: searchQuery ?? "") self.discoveryType = discoveryType self.pathID = pathID - - if let url = URL(string: URLString) { - viewModel.request = URLRequest(url: url) - } } public var body: some View { GeometryReader { proxy in - VStack(alignment: .center) { - WebView( - viewModel: .init( - url: URLString, - baseURL: "" - ), - isLoading: $isLoading, - refreshCookies: {}, - navigationDelegate: viewModel - ) - .accessibilityIdentifier("discovery_webview") - - if isLoading || viewModel.showProgress { - HStack(alignment: .center) { - ProgressBar( - size: 40, - lineWidth: 8 - ) - .padding(.vertical, proxy.size.height / 2) - .accessibilityIdentifier("progress_bar") + ZStack(alignment: .center) { + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "" + ), + isLoading: $isLoading, + refreshCookies: {}, + navigationDelegate: viewModel, + webViewType: discoveryType.rawValue + ) + .accessibilityIdentifier("discovery_webview") + + if isLoading || viewModel.showProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + .accessibilityIdentifier("progress_bar") + } + .frame(width: proxy.size.width, height: proxy.size.height) } - .frame(width: proxy.size.width, height: proxy.size.height) - } - - // MARK: - Show Error - if viewModel.showError { - VStack { - SnackBarView(message: viewModel.errorMessage) + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } } - .padding(.bottom, 20) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil + + if !viewModel.userloggedIn, !isLoading { + LogistrationBottomView { buttonAction in + switch buttonAction { + case .signIn: + viewModel.router.showLoginScreen(sourceScreen: sourceScreen) + case .register: + viewModel.router.showRegisterScreen(sourceScreen: sourceScreen) + } } } } - if !viewModel.userloggedIn, !isLoading { - LogistrationBottomView { buttonAction in - switch buttonAction { - case .signIn: - viewModel.router.showLoginScreen(sourceScreen: sourceScreen) - case .register: - viewModel.router.showRegisterScreen(sourceScreen: sourceScreen) + if viewModel.webViewError { + FullScreenErrorView( + type: viewModel.connectivity.isInternetAvaliable ? .generic : .noInternetWithReload + ) { + if viewModel.connectivity.isInternetAvaliable { + viewModel.webViewError = false + NotificationCenter.default.post( + name: Notification.Name(discoveryType.rawValue), + object: nil + ) } } } } - - // MARK: - Offline mode SnackBar - OfflineSnackBarView( - connectivity: viewModel.connectivity, - reloadAction: { - NotificationCenter.default.post( - name: .webviewReloadNotification, - object: nil - ) - }) + .onFirstAppear { + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } } .navigationBarHidden(viewModel.sourceScreen == .default && discoveryType == .discovery) .navigationTitle(CoreLocalization.Mainscreen.discovery) diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index 836323072..e79afb8c1 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -14,6 +14,8 @@ public class DiscoveryWebviewViewModel: ObservableObject { @Published var courseDetails: CourseDetails? @Published private(set) var showProgress = false @Published var showError: Bool = false + @Published var webViewError: Bool = false + var errorMessage: String? { didSet { withAnimation { @@ -247,4 +249,8 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { private func isValidAppURLScheme(_ url: URL) -> Bool { return url.scheme ?? "" == config.URIScheme } + + public func showWebViewError() { + self.webViewError = true + } } diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift index ad28e6938..a646d5108 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift @@ -10,7 +10,7 @@ import SwiftUI import Theme import Core -public enum ProgramViewType: Equatable { +public enum ProgramViewType: String, Equatable { case program case programDetail } @@ -18,7 +18,7 @@ public enum ProgramViewType: Equatable { public struct ProgramWebviewView: View { @State private var isLoading: Bool = true - @ObservedObject private var viewModel: ProgramWebviewViewModel + @StateObject private var viewModel: ProgramWebviewViewModel private var router: DiscoveryRouter private var viewType: ProgramViewType public var pathID: String @@ -42,71 +42,84 @@ public struct ProgramWebviewView: View { viewType: ProgramViewType = .program, pathID: String = "" ) { - self.viewModel = viewModel + self._viewModel = .init(wrappedValue: viewModel) self.router = router self.viewType = viewType self.pathID = pathID - - if let url = URL(string: URLString) { - viewModel.request = URLRequest(url: url) - } } public var body: some View { GeometryReader { proxy in - VStack(alignment: .center) { - WebView( - viewModel: .init( - url: URLString, - baseURL: "", - injections: [.colorInversionCss] - ), - isLoading: $isLoading, - refreshCookies: { - await viewModel.updateCookies( - force: true - ) - }, - navigationDelegate: viewModel - ) - .accessibilityIdentifier("program_webview") - - if isLoading || viewModel.showProgress || viewModel.updatingCookies { - HStack(alignment: .center) { - ProgressBar( - size: 40, - lineWidth: 8 - ) - .padding(.vertical, proxy.size.height / 2) - .accessibilityIdentifier("progress_bar") + ZStack(alignment: .center) { + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "", + injections: [.colorInversionCss] + ), + isLoading: $isLoading, + refreshCookies: { + await viewModel.updateCookies( + force: true + ) + }, + navigationDelegate: viewModel, + webViewType: viewType.rawValue + ) + .accessibilityIdentifier("program_webview") + + let shouldShowProgress = ( + isLoading || + viewModel.showProgress || + viewModel.updatingCookies + ) + if shouldShowProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + .accessibilityIdentifier("progress_bar") + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } } - .frame(width: proxy.size.width, height: proxy.size.height) } - // MARK: - Show Error - if viewModel.showError { - VStack { - SnackBarView(message: viewModel.errorMessage) - } - .padding(.bottom, 20) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil + if viewModel.webViewError { + FullScreenErrorView( + type: viewModel.connectivity.isInternetAvaliable ? .generic : .noInternetWithReload + ) { + if viewModel.connectivity.isInternetAvaliable { + viewModel.webViewError = false + NotificationCenter.default.post( + name: Notification.Name(viewType.rawValue), + object: nil + ) } } } } - - // MARK: - Offline mode SnackBar - OfflineSnackBarView( - connectivity: viewModel.connectivity, - reloadAction: { - NotificationCenter.default.post( - name: .webviewReloadNotification, - object: nil - ) - }) + .onFirstAppear { + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } } .navigationBarHidden(viewType == .program) .navigationTitle(CoreLocalization.Mainscreen.programs) @@ -114,3 +127,23 @@ public struct ProgramWebviewView: View { .animation(.default, value: viewModel.showError) } } + +#if DEBUG +struct ProgramWebviewView_Previews: PreviewProvider { + static var previews: some View { + ProgramWebviewView( + viewModel: ProgramWebviewViewModel( + router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: DiscoveryInteractor.mock, + connectivity: Connectivity(), + analytics: DiscoveryAnalyticsMock(), + authInteractor: AuthInteractor.mock + ), + router: DiscoveryRouterMock(), + viewType: .program, + pathID: "" + ) + } +} +#endif diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift index ad0c89987..34ad47de2 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift @@ -14,6 +14,7 @@ public class ProgramWebviewViewModel: ObservableObject, WebviewCookiesUpdateProt @Published var courseDetails: CourseDetails? @Published private(set) var showProgress = false @Published var showError: Bool = false + @Published var webViewError: Bool = false @Published public var updatingCookies: Bool = false @Published public var cookiesReady: Bool = false @@ -235,4 +236,8 @@ extension ProgramWebviewViewModel: WebViewNavigationDelegate { private func isValidAppURLScheme(_ url: URL) -> Bool { return url.scheme ?? "" == config.URIScheme } + + public func showWebViewError() { + self.webViewError = true + } } From f6c9f4784b0f6027f6b1d1c5e39a76b78c187c98 Mon Sep 17 00:00:00 2001 From: Anton Yarmolenko <37253+rnr@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:27:54 +0300 Subject: [PATCH 4/8] chore: added Settings button for dashboard type 'list' --- .../Dashboard/Presentation/ListDashboardView.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index d3787ebad..90a3e5eba 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -27,6 +27,7 @@ public struct ListDashboardView: View { @StateObject private var viewModel: ListDashboardViewModel private let router: DashboardRouter + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } public init(viewModel: ListDashboardViewModel, router: DashboardRouter) { self._viewModel = StateObject(wrappedValue: { viewModel }()) @@ -103,6 +104,17 @@ public struct ListDashboardView: View { .frameLimit(width: proxy.size.width) }.accessibilityAction {} }.padding(.top, 8) + HStack { + Spacer() + Button(action: { + router.showSettings() + }, label: { + CoreAssets.settings.swiftUIImage.renderingMode(.template) + .foregroundColor(Theme.Colors.accentColor) + }) + } + .padding(.top, idiom == .pad ? 13 : 5) + .padding(.trailing, idiom == .pad ? 20 : 16) // MARK: - Offline mode SnackBar OfflineSnackBarView(connectivity: viewModel.connectivity, From 9901340ea443bb11df86c2d5973ae4bca588be10 Mon Sep 17 00:00:00 2001 From: Saeed Bashir Date: Mon, 8 Jul 2024 15:25:41 +0500 Subject: [PATCH 5/8] chore: fix double notification routing and segment callback (#473) * chore: fix double notification routing and segment callback * refactor: address review feedback * refactor: address review feedback --- .../Listeners/BrazeListener.swift | 6 ++++++ .../Providers/BrazeProvider.swift | 5 ++++- .../PushNotificationsManager.swift | 10 +++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift b/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift index b117843fc..0a78688e4 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift @@ -6,6 +6,7 @@ // import Foundation +import Swinject class BrazeListener: PushNotificationsListener { @@ -24,6 +25,11 @@ class BrazeListener: PushNotificationsListener { func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { guard let dictionary = userInfo as? [String: AnyHashable], shouldListenNotification(userinfo: userInfo) else { return } + + if let segmentService = Container.shared.resolve(SegmentAnalyticsService.self) { + segmentService.analytics?.receivedRemoteNotification(userInfo: userInfo) + } + let link = PushLink(dictionary: dictionary) deepLinkManager.processLinkFromNotification(link) } diff --git a/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift b/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift index 43ca0c780..3b93fec87 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift @@ -10,17 +10,20 @@ import SegmentBrazeUI import Swinject class BrazeProvider: PushNotificationsProvider { + func didRegisterWithDeviceToken(deviceToken: Data) { guard let segmentService = Container.shared.resolve(SegmentAnalyticsService.self) else { return } segmentService.analytics?.add( plugin: BrazeDestination( additionalConfiguration: { configuration in - configuration.logger.level = .debug + configuration.logger.level = .info }, additionalSetup: { braze in braze.notifications.register(deviceToken: deviceToken) } ) ) + + segmentService.analytics?.registeredForRemoteNotifications(deviceToken: deviceToken) } func didFailToRegisterForRemoteNotificationsWithError(error: Error) { diff --git a/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift b/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift index 1f6ab02b4..bd218ed02 100644 --- a/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift +++ b/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift @@ -29,6 +29,7 @@ class PushNotificationsManager: NSObject { private let deepLinkManager: DeepLinkManager private let storage: CoreStorage private let api: API + private var providers: [PushNotificationsProvider] = [] private var listeners: [PushNotificationsListener] = [] @@ -37,10 +38,16 @@ class PushNotificationsManager: NSObject { } // Init manager - public init(deepLinkManager: DeepLinkManager, storage: CoreStorage, api: API, config: ConfigProtocol) { + public init( + deepLinkManager: DeepLinkManager, + storage: CoreStorage, + api: API, + config: ConfigProtocol + ) { self.deepLinkManager = deepLinkManager self.storage = storage self.api = api + super.init() providers = providersFor(config: config) listeners = listenersFor(config: config) @@ -129,6 +136,7 @@ extension PushNotificationsManager: UNUserNotificationCenterDelegate { ) async -> UNNotificationPresentationOptions { if UIApplication.shared.applicationState == .active { didReceiveRemoteNotification(userInfo: notification.request.content.userInfo) + return [] } return [[.list, .banner, .sound]] From 5d99fba0656bac8982be191afec93a437f5ac693 Mon Sep 17 00:00:00 2001 From: Saeed Bashir Date: Mon, 8 Jul 2024 16:29:14 +0500 Subject: [PATCH 6/8] feat: FullStory Integration and Analytics Implementation (#471) * feat: fullstory SDK integration and analytics implementation * chore: adding new screen events * chore: update user-defined flag to build settings based on config --------- Co-authored-by: Anton Yarmolenko --- .gitignore | 1 + .../Presentation/AuthorizationAnalytics.swift | 3 + .../Presentation/Login/SignInView.swift | 3 + .../Presentation/Login/SignInViewModel.swift | 8 +- .../Registration/SignUpView.swift | 3 + .../Registration/SignUpViewModel.swift | 7 ++ .../Presentation/Startup/StartupView.swift | 3 + .../Startup/StartupViewModel.swift | 4 + .../AuthorizationMock.generated.swift | 95 ++++++++++++++++ Core/Core.xcodeproj/project.pbxproj | 9 +- Core/Core/Analytics/CoreAnalytics.swift | 20 ++++ Core/Core/Configuration/Config/Config.swift | 1 + .../Config/FullStoryConfig.swift | 30 ++++++ .../Configuration/FullStoryConfigTests.swift | 57 ++++++++++ .../Course/Presentation/CourseAnalytics.swift | 3 + .../Presentation/Handouts/HandoutsView.swift | 4 +- Course/CourseTests/CourseMock.generated.swift | 96 +++++++++++++++++ .../DashboardMock.generated.swift | 76 +++++++++++++ .../Presentation/DiscoveryAnalytics.swift | 4 +- .../DiscoveryWebviewViewModel.swift | 2 +- .../DiscoveryMock.generated.swift | 96 +++++++++++++++-- .../DiscussionMock.generated.swift | 76 +++++++++++++ Documentation/Theming_implementation.md | 2 + OpenEdX.xcodeproj/project.pbxproj | 54 ++++++++++ OpenEdX/DI/AppAssembly.swift | 4 + .../AnalyticsManager/AnalyticsManager.swift | 80 +++++++++++--- .../FirebaseAnalyticsService.swift | 4 + .../FullStoryAnalyticsService.swift | 25 +++++ .../SegmentAnalyticsService.swift | 4 + .../EditProfile/EditProfileView.swift | 3 + .../EditProfile/EditProfileViewModel.swift | 4 + .../Presentation/ProfileAnalytics.swift | 6 +- .../Settings/SettingsViewModel.swift | 2 +- .../ProfileTests/ProfileMock.generated.swift | 101 +++++++++++++++++- config_script/process_config.py | 9 ++ config_script/whitelabel.py | 98 ++++++++++++++++- 36 files changed, 959 insertions(+), 38 deletions(-) create mode 100644 Core/Core/Configuration/Config/FullStoryConfig.swift create mode 100644 Core/CoreTests/Configuration/FullStoryConfigTests.swift create mode 100644 OpenEdX/Managers/FullStoryAnalyticsService/FullStoryAnalyticsService.swift diff --git a/.gitignore b/.gitignore index 274b62ec6..75a5357e4 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,4 @@ default_config/ I18N/ *.lproj/ !en.lproj/ +/config_script/__pycache__ diff --git a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift index 00cb384a5..9fcd13b69 100644 --- a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift +++ b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift @@ -6,6 +6,7 @@ // import Foundation +import Core public enum AuthMethod: Equatable { case password @@ -40,6 +41,7 @@ public protocol AuthorizationAnalytics { func forgotPasswordClicked() func resetPasswordClicked() func resetPassword(success: Bool) + func authTrackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) } #if DEBUG @@ -54,5 +56,6 @@ class AuthorizationAnalyticsMock: AuthorizationAnalytics { public func forgotPasswordClicked() {} public func resetPasswordClicked() {} public func resetPassword(success: Bool) {} + public func authTrackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) {} } #endif diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index 20bfcb659..4369f2fab 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -212,6 +212,9 @@ public struct SignInView: View { .hideNavigationBar() .ignoresSafeArea(.all, edges: .horizontal) .background(Theme.Colors.background.ignoresSafeArea(.all)) + .onFirstAppear{ + viewModel.trackScreenEvent() + } } @ViewBuilder diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index ef573e047..5a87151f5 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -147,5 +147,11 @@ public class SignInViewModel: ObservableObject { func trackForgotPasswordClicked() { analytics.forgotPasswordClicked() } - + + func trackScreenEvent() { + analytics.authTrackScreenEvent( + .logistrationSignIn, + biValue: .logistrationSignIn + ) + } } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 2401ad846..7ec2c8ba5 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -196,6 +196,9 @@ public struct SignUpView: View { .ignoresSafeArea(.all, edges: .horizontal) .background(Theme.Colors.background.ignoresSafeArea(.all)) .hideNavigationBar() + .onFirstAppear{ + viewModel.trackScreenEvent() + } } } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift index 5c41badef..8b2fe1b22 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift @@ -213,4 +213,11 @@ public class SignUpViewModel: ObservableObject { func trackCreateAccountClicked() { analytics.createAccountClicked() } + + func trackScreenEvent() { + analytics.authTrackScreenEvent( + .logistrationRegister, + biValue: .logistrationRegister + ) + } } diff --git a/Authorization/Authorization/Presentation/Startup/StartupView.swift b/Authorization/Authorization/Presentation/Startup/StartupView.swift index a13c3e3c8..88551d824 100644 --- a/Authorization/Authorization/Presentation/Startup/StartupView.swift +++ b/Authorization/Authorization/Presentation/Startup/StartupView.swift @@ -126,6 +126,9 @@ public struct StartupView: View { .onTapGesture { UIApplication.shared.endEditing() } + .onFirstAppear { + viewModel.trackScreenEvent() + } } } diff --git a/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift index 650ae5f7f..b4cf50091 100644 --- a/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift +++ b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift @@ -33,4 +33,8 @@ public class StartupViewModel: ObservableObject { analytics.trackEvent(.logistrationExploreAllCourses, biValue: .logistrationExploreAllCourses) } } + + func trackScreenEvent() { + analytics.trackScreenEvent(.logistration, biValue: .logistration) + } } diff --git a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift index f17939bd4..34ce42889 100644 --- a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift +++ b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift @@ -569,6 +569,12 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { perform?(`success`) } + open func authTrackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_authTrackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_authTrackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + fileprivate enum MethodType { case m_identify__id_idusername_usernameemail_email(Parameter, Parameter, Parameter) @@ -581,6 +587,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case m_forgotPasswordClicked case m_resetPasswordClicked case m_resetPassword__success_success(Parameter) + case m_authTrackScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -617,6 +624,12 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSuccess, rhs: rhsSuccess, with: matcher), lhsSuccess, rhsSuccess, "success")) return Matcher.ComparisonResult(results) + + case (.m_authTrackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_authTrackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -633,6 +646,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case .m_forgotPasswordClicked: return 0 case .m_resetPasswordClicked: return 0 case let .m_resetPassword__success_success(p0): return p0.intValue + case let .m_authTrackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { @@ -647,6 +661,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case .m_forgotPasswordClicked: return ".forgotPasswordClicked()" case .m_resetPasswordClicked: return ".resetPasswordClicked()" case .m_resetPassword__success_success: return ".resetPassword(success:)" + case .m_authTrackScreenEvent__eventbiValue_biValue: return ".authTrackScreenEvent(_:biValue:)" } } } @@ -675,6 +690,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { public static func forgotPasswordClicked() -> Verify { return Verify(method: .m_forgotPasswordClicked)} public static func resetPasswordClicked() -> Verify { return Verify(method: .m_resetPasswordClicked)} public static func resetPassword(success: Parameter) -> Verify { return Verify(method: .m_resetPassword__success_success(`success`))} + public static func authTrackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_authTrackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -711,6 +727,9 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { public static func resetPassword(success: Parameter, perform: @escaping (Bool) -> Void) -> Perform { return Perform(method: .m_resetPassword__success_success(`success`), performs: perform) } + public static func authTrackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_authTrackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } } public func given(_ method: Given) { @@ -1964,6 +1983,18 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`, `parameters`) } + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void @@ -1988,14 +2019,30 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`) } + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + fileprivate enum MethodType { case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) case m_trackEvent__event(Parameter) case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -2012,6 +2059,19 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) return Matcher.ComparisonResult(results) + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) @@ -2038,6 +2098,17 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -2046,20 +2117,28 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { switch self { case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackEvent__event(p0): return p0.intValue case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" case .m_trackEvent__event: return ".trackEvent(_:)" case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" } } } @@ -2080,10 +2159,14 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -2096,6 +2179,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) } @@ -2108,6 +2197,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } } public func given(_ method: Given) { diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 3e59ea038..d52896f50 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -144,6 +144,8 @@ 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */; }; 142EDD6C2B831D1400F9F320 /* BranchSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 142EDD6B2B831D1400F9F320 /* BranchSDK */; }; 14769D3C2B9822EE00AB36D4 /* CoreAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14769D3B2B9822EE00AB36D4 /* CoreAnalytics.swift */; }; + 14D912D92C2553C70077CCCE /* FullStoryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D912D82C2553C70077CCCE /* FullStoryConfig.swift */; }; + 14D912DB2C257E9E0077CCCE /* FullStoryConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D912DA2C257E9E0077CCCE /* FullStoryConfigTests.swift */; }; 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */; }; A51CDBE72B6D21F2009B6D4E /* SegmentConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */; }; A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; }; @@ -335,6 +337,8 @@ 0E13E9173C9C4CFC19F8B6F2 /* Pods-App-Core.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugstage.xcconfig"; sourceTree = ""; }; 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebviewCookiesUpdateProtocol.swift; sourceTree = ""; }; 14769D3B2B9822EE00AB36D4 /* CoreAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreAnalytics.swift; sourceTree = ""; }; + 14D912D82C2553C70077CCCE /* FullStoryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullStoryConfig.swift; sourceTree = ""; }; + 14D912DA2C257E9E0077CCCE /* FullStoryConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullStoryConfigTests.swift; sourceTree = ""; }; 1A154A95AF4EE85A4A1C083B /* Pods-App-Core.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releasedev.xcconfig"; sourceTree = ""; }; 2B7E6FE7843FC4CF2BFA712D /* Pods-App-Core.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debug.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debug.xcconfig"; sourceTree = ""; }; 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -867,6 +871,7 @@ 02CA59832BD7DDBE00D517AA /* DashboardConfig.swift */, A53A32342B233DEC005FE38A /* ThemeConfig.swift */, E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */, + 14D912D82C2553C70077CCCE /* FullStoryConfig.swift */, ); path = Config; sourceTree = ""; @@ -884,6 +889,7 @@ children = ( E09179FC2B0F204D002AB695 /* ConfigTests.swift */, BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */, + 14D912DA2C257E9E0077CCCE /* FullStoryConfigTests.swift */, ); path = Configuration; sourceTree = ""; @@ -1071,6 +1077,7 @@ buildActionMask = 2147483647; files = ( BAD9CA422B2B140100DE790A /* AgreementConfigTests.swift in Sources */, + 14D912DB2C257E9E0077CCCE /* FullStoryConfigTests.swift in Sources */, E09179FD2B0F204E002AB695 /* ConfigTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1137,6 +1144,7 @@ 06078B702BA49C3100576798 /* Dictionary+JSON.swift in Sources */, 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */, 027BD3AE2909475000392132 /* KeyboardScrollerOptions.swift in Sources */, + 14D912D92C2553C70077CCCE /* FullStoryConfig.swift in Sources */, BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */, 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */, 064987992B4D69FF0071642A /* WebViewScriptInjectionProtocol.swift in Sources */, @@ -1223,7 +1231,6 @@ 02F164372902A9EB0090DDEF /* StringExtension.swift in Sources */, 0231CDBE2922422D00032416 /* CSSInjector.swift in Sources */, 064987982B4D69FF0071642A /* CSSInjectionProtocol.swift in Sources */, - BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */, 029EE3ED2BF6650500F64F33 /* Bundle.swift in Sources */, BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */, 0236961928F9A26900EEF206 /* AuthRepository.swift in Sources */, diff --git a/Core/Core/Analytics/CoreAnalytics.swift b/Core/Core/Analytics/CoreAnalytics.swift index c7d1eca7c..137bf094f 100644 --- a/Core/Core/Analytics/CoreAnalytics.swift +++ b/Core/Core/Analytics/CoreAnalytics.swift @@ -11,6 +11,8 @@ import Foundation public protocol CoreAnalytics { func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) + func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) + func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) func videoQualityChanged( _ event: AnalyticsEvent, @@ -28,6 +30,14 @@ public extension CoreAnalytics { func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { trackEvent(event, biValue: biValue, parameters: nil) } + + func trackScreenEvent(_ event: AnalyticsEvent) { + trackScreenEvent(event, parameters: nil) + } + + func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + trackScreenEvent(event, biValue: biValue, parameters: nil) + } } #if DEBUG @@ -35,6 +45,8 @@ public class CoreAnalyticsMock: CoreAnalytics { public init() {} public func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]? = nil) {} public func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) {} + public func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) {} + public func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) {} public func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String? = nil, rating: Int? = 0) {} public func videoQualityChanged( _ event: AnalyticsEvent, @@ -124,6 +136,10 @@ public enum AnalyticsEvent: String { case whatnewPopup = "WhatsNew:Pop up Viewed" case whatnewDone = "WhatsNew:Done" case whatnewClose = "WhatsNew:Close" + case logistration = "Logistration" + case logistrationSignIn = "Logistration:Sign In" + case logistrationRegister = "Logistration:Register" + case profileEdit = "Profile:Edit Profile" } public enum EventBIValue: String { @@ -205,6 +221,10 @@ public enum EventBIValue: String { case whatnewPopup = "edx.bi.app.whats_new.popup.viewed" case whatnewDone = "edx.bi.app.whats_new.done" case whatnewClose = "edx.bi.app.whats_new.close" + case logistration = "edx.bi.app.logistration" + case logistrationSignIn = "edx.bi.app.logistration.signin" + case logistrationRegister = "edx.bi.app.logistration.register" + case profileEdit = "edx.bi.app.profile.edit" } public struct EventParamKey { diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift index bd75f3f89..be5fd1941 100644 --- a/Core/Core/Configuration/Config/Config.swift +++ b/Core/Core/Configuration/Config/Config.swift @@ -31,6 +31,7 @@ public protocol ConfigProtocol { var segment: SegmentConfig { get } var program: DiscoveryConfig { get } var URIScheme: String { get } + var fullStory: FullStoryConfig { get } } public enum TokenType: String { diff --git a/Core/Core/Configuration/Config/FullStoryConfig.swift b/Core/Core/Configuration/Config/FullStoryConfig.swift new file mode 100644 index 000000000..4e1181dc3 --- /dev/null +++ b/Core/Core/Configuration/Config/FullStoryConfig.swift @@ -0,0 +1,30 @@ +// +// FullStoryConfig.swift +// Core +// +// Created by Saeed Bashir on 6/21/24. +// + +import Foundation + +private enum Keys: String, RawStringExtractable { + case enabled = "ENABLED" + case orgID = "ORG_ID" +} + +public final class FullStoryConfig { + public var enabled: Bool = false + public var orgID: String = "" + + init(dictionary: [String: AnyObject]) { + orgID = dictionary[Keys.orgID] as? String ?? "" + enabled = !orgID.isEmpty && dictionary[Keys.enabled] as? Bool ?? false + } +} + +private let configKey = "FULLSTORY" +extension Config { + public var fullStory: FullStoryConfig { + FullStoryConfig(dictionary: self[configKey] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/CoreTests/Configuration/FullStoryConfigTests.swift b/Core/CoreTests/Configuration/FullStoryConfigTests.swift new file mode 100644 index 000000000..092d9a5ee --- /dev/null +++ b/Core/CoreTests/Configuration/FullStoryConfigTests.swift @@ -0,0 +1,57 @@ +// +// FullStoryConfigTests.swift +// CoreTests +// +// Created by Saeed Bashir on 6/21/24. +// + +import XCTest +@testable import Core + +class FullStoryConfigTests: XCTestCase { + + func testNoFullStoryConfig() { + let config = Config(properties: [:]) + XCTAssertFalse(config.fullStory.enabled) + } + + func testFullStoryEnabled() { + let configDictionary = [ + "FULLSTORY": [ + "ENABLED": true, + "ORG_ID": "org_id" + ] + ] + + let config = Config(properties: configDictionary) + + XCTAssertTrue(config.fullStory.enabled) + XCTAssertNotNil(config.fullStory.orgID) + } + + func testFullStoryDisabled() { + let configDictionary = [ + "FULLSTORY": [ + "ENABLED": false, + "ORG_ID": "org_id" + ] + ] + + let config = Config(properties: configDictionary) + + XCTAssertFalse(config.fullStory.enabled) + XCTAssertNotNil(config.fullStory.orgID) + } + + func testFullStoryMissingORGID() { + let configDictionary = [ + "FULLSTORY": [ + "ENABLED": true + ] + ] + + let config = Config(properties: configDictionary) + XCTAssertFalse(config.fullStory.enabled) + XCTAssertEqual(config.fullStory.orgID, "") + } +} diff --git a/Course/Course/Presentation/CourseAnalytics.swift b/Course/Course/Presentation/CourseAnalytics.swift index bc531c152..612f9249c 100644 --- a/Course/Course/Presentation/CourseAnalytics.swift +++ b/Course/Course/Presentation/CourseAnalytics.swift @@ -87,6 +87,8 @@ public protocol CourseAnalytics { snackbar: SnackbarType ) func trackCourseEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) + func trackCourseScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) + func plsEvent( _ event: AnalyticsEvent, bivalue: EventBIValue, @@ -164,6 +166,7 @@ class CourseAnalyticsMock: CourseAnalytics { snackbar: SnackbarType ) {} public func trackCourseEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) {} + public func trackCourseScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) {} public func plsEvent( _ event: AnalyticsEvent, bivalue: EventBIValue, diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index de8bb631f..c9f7460de 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -54,7 +54,7 @@ struct HandoutsView: View { announcements: nil, router: viewModel.router, cssInjector: viewModel.cssInjector) - viewModel.analytics.trackCourseEvent( + viewModel.analytics.trackCourseScreenEvent( .courseHandouts, biValue: .courseHandouts, courseID: courseID @@ -68,7 +68,7 @@ struct HandoutsView: View { announcements: viewModel.updates, router: viewModel.router, cssInjector: viewModel.cssInjector) - viewModel.analytics.trackCourseEvent( + viewModel.analytics.trackCourseScreenEvent( .courseAnnouncement, biValue: .courseAnnouncement, courseID: courseID diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index 855432bc8..067556faf 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -1171,6 +1171,18 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`, `parameters`) } + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void @@ -1195,14 +1207,30 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`) } + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + fileprivate enum MethodType { case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) case m_trackEvent__event(Parameter) case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1219,6 +1247,19 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) return Matcher.ComparisonResult(results) + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) @@ -1245,6 +1286,17 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -1253,20 +1305,28 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { switch self { case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackEvent__event(p0): return p0.intValue case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" case .m_trackEvent__event: return ".trackEvent(_:)" case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" } } } @@ -1287,10 +1347,14 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -1303,6 +1367,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) } @@ -1315,6 +1385,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } } public func given(_ method: Given) { @@ -1542,6 +1618,12 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { perform?(`event`, `biValue`, `courseID`) } + open func trackCourseScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) { + addInvocation(.m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`courseID`))) as? (AnalyticsEvent, EventBIValue, String) -> Void + perform?(`event`, `biValue`, `courseID`) + } + open func plsEvent(_ event: AnalyticsEvent, bivalue: EventBIValue, courseID: String, screenName: String, type: String) { addInvocation(.m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`courseID`), Parameter.value(`screenName`), Parameter.value(`type`))) let perform = methodPerformValue(.m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`courseID`), Parameter.value(`screenName`), Parameter.value(`type`))) as? (AnalyticsEvent, EventBIValue, String, String, String) -> Void @@ -1592,6 +1674,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case m_calendarSyncDialogAction__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIddialog_dialogaction_action(Parameter, Parameter, Parameter, Parameter, Parameter) case m_calendarSyncSnackbar__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIdsnackbar_snackbar(Parameter, Parameter, Parameter, Parameter) case m_trackCourseEvent__eventbiValue_biValuecourseID_courseID(Parameter, Parameter, Parameter) + case m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(Parameter, Parameter, Parameter) case m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(Parameter, Parameter, Parameter, Parameter, Parameter) case m_plsSuccessEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_typesuccess_success(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter) case m_bulkDownloadVideosToggle__courseID_courseIDaction_action(Parameter, Parameter) @@ -1731,6 +1814,13 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) return Matcher.ComparisonResult(results) + case (.m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(let lhsEvent, let lhsBivalue, let lhsCourseid), .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(let rhsEvent, let rhsBivalue, let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + case (.m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(let lhsEvent, let lhsBivalue, let lhsCourseid, let lhsScreenname, let lhsType), .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(let rhsEvent, let rhsBivalue, let rhsCourseid, let rhsScreenname, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) @@ -1794,6 +1884,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case let .m_calendarSyncDialogAction__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIddialog_dialogaction_action(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue case let .m_calendarSyncSnackbar__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIdsnackbar_snackbar(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackCourseEvent__eventbiValue_biValuecourseID_courseID(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue case let .m_plsSuccessEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_typesuccess_success(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_bulkDownloadVideosToggle__courseID_courseIDaction_action(p0, p1): return p0.intValue + p1.intValue @@ -1821,6 +1912,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case .m_calendarSyncDialogAction__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIddialog_dialogaction_action: return ".calendarSyncDialogAction(enrollmentMode:pacing:courseId:dialog:action:)" case .m_calendarSyncSnackbar__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIdsnackbar_snackbar: return ".calendarSyncSnackbar(enrollmentMode:pacing:courseId:snackbar:)" case .m_trackCourseEvent__eventbiValue_biValuecourseID_courseID: return ".trackCourseEvent(_:biValue:courseID:)" + case .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID: return ".trackCourseScreenEvent(_:biValue:courseID:)" case .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type: return ".plsEvent(_:bivalue:courseID:screenName:type:)" case .m_plsSuccessEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_typesuccess_success: return ".plsSuccessEvent(_:bivalue:courseID:screenName:type:success:)" case .m_bulkDownloadVideosToggle__courseID_courseIDaction_action: return ".bulkDownloadVideosToggle(courseID:action:)" @@ -1862,6 +1954,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func calendarSyncDialogAction(enrollmentMode: Parameter, pacing: Parameter, courseId: Parameter, dialog: Parameter, action: Parameter) -> Verify { return Verify(method: .m_calendarSyncDialogAction__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIddialog_dialogaction_action(`enrollmentMode`, `pacing`, `courseId`, `dialog`, `action`))} public static func calendarSyncSnackbar(enrollmentMode: Parameter, pacing: Parameter, courseId: Parameter, snackbar: Parameter) -> Verify { return Verify(method: .m_calendarSyncSnackbar__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIdsnackbar_snackbar(`enrollmentMode`, `pacing`, `courseId`, `snackbar`))} public static func trackCourseEvent(_ event: Parameter, biValue: Parameter, courseID: Parameter) -> Verify { return Verify(method: .m_trackCourseEvent__eventbiValue_biValuecourseID_courseID(`event`, `biValue`, `courseID`))} + public static func trackCourseScreenEvent(_ event: Parameter, biValue: Parameter, courseID: Parameter) -> Verify { return Verify(method: .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(`event`, `biValue`, `courseID`))} public static func plsEvent(_ event: Parameter, bivalue: Parameter, courseID: Parameter, screenName: Parameter, type: Parameter) -> Verify { return Verify(method: .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(`event`, `bivalue`, `courseID`, `screenName`, `type`))} public static func plsSuccessEvent(_ event: Parameter, bivalue: Parameter, courseID: Parameter, screenName: Parameter, type: Parameter, success: Parameter) -> Verify { return Verify(method: .m_plsSuccessEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_typesuccess_success(`event`, `bivalue`, `courseID`, `screenName`, `type`, `success`))} public static func bulkDownloadVideosToggle(courseID: Parameter, action: Parameter) -> Verify { return Verify(method: .m_bulkDownloadVideosToggle__courseID_courseIDaction_action(`courseID`, `action`))} @@ -1927,6 +2020,9 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func trackCourseEvent(_ event: Parameter, biValue: Parameter, courseID: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String) -> Void) -> Perform { return Perform(method: .m_trackCourseEvent__eventbiValue_biValuecourseID_courseID(`event`, `biValue`, `courseID`), performs: perform) } + public static func trackCourseScreenEvent(_ event: Parameter, biValue: Parameter, courseID: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String) -> Void) -> Perform { + return Perform(method: .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(`event`, `biValue`, `courseID`), performs: perform) + } public static func plsEvent(_ event: Parameter, bivalue: Parameter, courseID: Parameter, screenName: Parameter, type: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String, String) -> Void) -> Perform { return Perform(method: .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(`event`, `bivalue`, `courseID`, `screenName`, `type`), performs: perform) } diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index 5dd6af2cc..82ae9be00 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -1171,6 +1171,18 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`, `parameters`) } + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void @@ -1195,14 +1207,30 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`) } + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + fileprivate enum MethodType { case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) case m_trackEvent__event(Parameter) case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1219,6 +1247,19 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) return Matcher.ComparisonResult(results) + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) @@ -1245,6 +1286,17 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -1253,20 +1305,28 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { switch self { case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackEvent__event(p0): return p0.intValue case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" case .m_trackEvent__event: return ".trackEvent(_:)" case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" } } } @@ -1287,10 +1347,14 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -1303,6 +1367,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) } @@ -1315,6 +1385,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } } public func given(_ method: Given) { diff --git a/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift b/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift index bfc9e7075..db2119038 100644 --- a/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift +++ b/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift @@ -18,7 +18,7 @@ public protocol DiscoveryAnalytics { func courseEnrollSuccess(courseId: String, courseName: String) func externalLinkOpen(url: String, screen: String) func externalLinkOpenAction(url: String, screen: String, action: String) - func discoveryEvent(event: AnalyticsEvent, biValue: EventBIValue) + func discoveryScreenEvent(event: AnalyticsEvent, biValue: EventBIValue) } #if DEBUG @@ -31,6 +31,6 @@ class DiscoveryAnalyticsMock: DiscoveryAnalytics { public func courseEnrollSuccess(courseId: String, courseName: String) {} public func externalLinkOpen(url: String, screen: String) {} public func externalLinkOpenAction(url: String, screen: String, action: String) {} - public func discoveryEvent(event: AnalyticsEvent, biValue: EventBIValue) {} + public func discoveryScreenEvent(event: AnalyticsEvent, biValue: EventBIValue) {} } #endif diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index e79afb8c1..b9f2bb515 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -189,7 +189,7 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { case .programDetail: guard let pathID = programDetailPathId(from: url) else { return false } - analytics.discoveryEvent(event: .discoveryProgramInfo, biValue: .discoveryProgramInfo) + analytics.discoveryScreenEvent(event: .discoveryProgramInfo, biValue: .discoveryProgramInfo) router.showWebDiscoveryDetails( pathID: pathID, discoveryType: .programDetail(pathID), diff --git a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift index 7b7d2c10a..1bcdcff78 100644 --- a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift +++ b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift @@ -1171,6 +1171,18 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`, `parameters`) } + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void @@ -1195,14 +1207,30 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`) } + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + fileprivate enum MethodType { case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) case m_trackEvent__event(Parameter) case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1219,6 +1247,19 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) return Matcher.ComparisonResult(results) + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) @@ -1245,6 +1286,17 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -1253,20 +1305,28 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { switch self { case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackEvent__event(p0): return p0.intValue case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" case .m_trackEvent__event: return ".trackEvent(_:)" case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" } } } @@ -1287,10 +1347,14 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -1303,6 +1367,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) } @@ -1315,6 +1385,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } } public func given(_ method: Given) { @@ -1482,9 +1558,9 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { perform?(`url`, `screen`, `action`) } - open func discoveryEvent(event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_discoveryEvent__event_eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_discoveryEvent__event_eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + open func discoveryScreenEvent(event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_discoveryScreenEvent__event_eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_discoveryScreenEvent__event_eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void perform?(`event`, `biValue`) } @@ -1498,7 +1574,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { case m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_externalLinkOpen__url_urlscreen_screen(Parameter, Parameter) case m_externalLinkOpenAction__url_urlscreen_screenaction_action(Parameter, Parameter, Parameter) - case m_discoveryEvent__event_eventbiValue_biValue(Parameter, Parameter) + case m_discoveryScreenEvent__event_eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1547,7 +1623,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) return Matcher.ComparisonResult(results) - case (.m_discoveryEvent__event_eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_discoveryEvent__event_eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + case (.m_discoveryScreenEvent__event_eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_discoveryScreenEvent__event_eventbiValue_biValue(let rhsEvent, let rhsBivalue)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) @@ -1566,7 +1642,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { case let .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_externalLinkOpen__url_urlscreen_screen(p0, p1): return p0.intValue + p1.intValue case let .m_externalLinkOpenAction__url_urlscreen_screenaction_action(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue - case let .m_discoveryEvent__event_eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_discoveryScreenEvent__event_eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { @@ -1579,7 +1655,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { case .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName: return ".courseEnrollSuccess(courseId:courseName:)" case .m_externalLinkOpen__url_urlscreen_screen: return ".externalLinkOpen(url:screen:)" case .m_externalLinkOpenAction__url_urlscreen_screenaction_action: return ".externalLinkOpenAction(url:screen:action:)" - case .m_discoveryEvent__event_eventbiValue_biValue: return ".discoveryEvent(event:biValue:)" + case .m_discoveryScreenEvent__event_eventbiValue_biValue: return ".discoveryScreenEvent(event:biValue:)" } } } @@ -1606,7 +1682,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { public static func courseEnrollSuccess(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func externalLinkOpen(url: Parameter, screen: Parameter) -> Verify { return Verify(method: .m_externalLinkOpen__url_urlscreen_screen(`url`, `screen`))} public static func externalLinkOpenAction(url: Parameter, screen: Parameter, action: Parameter) -> Verify { return Verify(method: .m_externalLinkOpenAction__url_urlscreen_screenaction_action(`url`, `screen`, `action`))} - public static func discoveryEvent(event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_discoveryEvent__event_eventbiValue_biValue(`event`, `biValue`))} + public static func discoveryScreenEvent(event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_discoveryScreenEvent__event_eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -1637,8 +1713,8 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { public static func externalLinkOpenAction(url: Parameter, screen: Parameter, action: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { return Perform(method: .m_externalLinkOpenAction__url_urlscreen_screenaction_action(`url`, `screen`, `action`), performs: perform) } - public static func discoveryEvent(event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_discoveryEvent__event_eventbiValue_biValue(`event`, `biValue`), performs: perform) + public static func discoveryScreenEvent(event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_discoveryScreenEvent__event_eventbiValue_biValue(`event`, `biValue`), performs: perform) } } diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index 85aa084a5..82022c6aa 100644 --- a/Discussion/DiscussionTests/DiscussionMock.generated.swift +++ b/Discussion/DiscussionTests/DiscussionMock.generated.swift @@ -1171,6 +1171,18 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`, `parameters`) } + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void @@ -1195,14 +1207,30 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`) } + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + fileprivate enum MethodType { case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) case m_trackEvent__event(Parameter) case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1219,6 +1247,19 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) return Matcher.ComparisonResult(results) + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) @@ -1245,6 +1286,17 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -1253,20 +1305,28 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { switch self { case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackEvent__event(p0): return p0.intValue case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" case .m_trackEvent__event: return ".trackEvent(_:)" case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" } } } @@ -1287,10 +1347,14 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -1303,6 +1367,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) } @@ -1315,6 +1385,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } } public func given(_ method: Given) { diff --git a/Documentation/Theming_implementation.md b/Documentation/Theming_implementation.md index 33e5b577d..e9d5b37dd 100644 --- a/Documentation/Theming_implementation.md +++ b/Documentation/Theming_implementation.md @@ -45,9 +45,11 @@ project_config: config1: # Build Configuration name in project app_bundle_id: "bundle.id.app.new1" # Bundle ID to be set product_name: "Mobile App Name1" # App Name to be set + env_config: 'prod' # env name for this configuration. possible values: prod/dev/stage (values which config_settings.yaml defines) config2: # Build Configuration name in project app_bundle_id: "bundle.id.app.new2" # Bundle ID to be set product_name: "Mobile App Name2" # App Name to be set + env_config: 'dev' # env name for this configuration. possible values: prod/dev/stage (values which config_settings.yaml defines) ``` ### Assets The config `whitelabel.yaml` can contain a few Asset items (every added Xcode project can have its own Assets). diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 5ab7602d4..24d602f45 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -48,6 +48,8 @@ 07D5DA3528D075AA00752FD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */; }; 07D5DA3E28D075AB00752FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */; }; 149FF39E2B9F1AB50034B33F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF39C2B9F1AB50034B33F /* LaunchScreen.storyboard */; }; + 14D912D32C25483F0077CCCE /* FullStory in Frameworks */ = {isa = PBXBuildFile; productRef = 14D912D22C25483F0077CCCE /* FullStory */; }; + 14D912D72C2551F60077CCCE /* FullStoryAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D912D62C2551F60077CCCE /* FullStoryAnalyticsService.swift */; }; 1924EDE8164C7AB17AD4946B /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC49621A0942E5EE74BDC895 /* Pods_App_OpenEdX.framework */; }; A500668B2B613ED10024680B /* PushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668A2B613ED10024680B /* PushNotificationsManager.swift */; }; A500668D2B6143000024680B /* FCMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668C2B6143000024680B /* FCMProvider.swift */; }; @@ -133,6 +135,7 @@ 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 149FF39D2B9F1AB50034B33F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 14D912D62C2551F60077CCCE /* FullStoryAnalyticsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullStoryAnalyticsService.swift; sourceTree = ""; }; 1BB1F1D0FABF8788646FBAF2 /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; 37C50995093E34142FDE0ED9 /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; A500668A2B613ED10024680B /* PushNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsManager.swift; sourceTree = ""; }; @@ -175,6 +178,7 @@ 027DB33028D8A063002B6862 /* Dashboard.framework in Frameworks */, A5BD3E302B83B0F7006A8983 /* SegmentFirebase in Frameworks */, A51CDBED2B6D2BEE009B6D4E /* SegmentBraze in Frameworks */, + 14D912D32C25483F0077CCCE /* FullStory in Frameworks */, A51CDBEF2B6D2BEE009B6D4E /* SegmentBrazeUI in Frameworks */, 1924EDE8164C7AB17AD4946B /* Pods_App_OpenEdX.framework in Frameworks */, ); @@ -263,6 +267,14 @@ path = OpenEdX; sourceTree = ""; }; + 14D912D52C2551CE0077CCCE /* FullStoryAnalyticsService */ = { + isa = PBXGroup; + children = ( + 14D912D62C2551F60077CCCE /* FullStoryAnalyticsService.swift */, + ); + path = FullStoryAnalyticsService; + sourceTree = ""; + }; 4E6FB43543890E90BB88D64D /* Frameworks */ = { isa = PBXGroup; children = ( @@ -315,6 +327,7 @@ A50066892B613E990024680B /* AnalyticsManager */, A59702272B83C84800CA064C /* FirebaseAnalyticsService */, A5C10D8D2B861A56008E864D /* SegmentAnalyticsService */, + 14D912D52C2551CE0077CCCE /* FullStoryAnalyticsService */, ); path = Managers; sourceTree = ""; @@ -413,6 +426,7 @@ 0770DE1528D07845006D8A5D /* Embed Frameworks */, DB97C0542B002EF00035C36F /* Process Config */, 02F175442A4E3B320019CD70 /* FirebaseCrashlytics */, + 14D912D42C25493C0077CCCE /* Run FullStory Asset Uploader */, ); buildRules = ( ); @@ -427,6 +441,7 @@ A5462D9E2B865713003B96A5 /* Segment */, 0780ABE22BFBA2E40093A4A6 /* FirebaseAnalytics */, 0780ABE42BFBA2E40093A4A6 /* FirebaseMessaging */, + 14D912D22C25483F0077CCCE /* FullStory */, ); productName = OpenEdX; productReference = 07D5DA3128D075AA00752FD9 /* OpenEdX.app */; @@ -463,6 +478,7 @@ A5BD3E2E2B83B0F7006A8983 /* XCRemoteSwiftPackageReference "analytics-swift-firebase" */, A5462D9D2B865713003B96A5 /* XCRemoteSwiftPackageReference "analytics-swift" */, 0780ABE12BFBA2E40093A4A6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + 14D912D12C25483F0077CCCE /* XCRemoteSwiftPackageReference "fullstory-swift-package-ios" */, ); productRefGroup = 07D5DA3228D075AA00752FD9 /* Products */; projectDirPath = ""; @@ -530,6 +546,24 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; + 14D912D42C25493C0077CCCE /* Run FullStory Asset Uploader */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run FullStory Asset Uploader"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [\"$ FULLSTORY_ENABLED\" = \"YES\"]; then\n \"${PODS_ROOT}/FullStory/tools/FullStoryCommandLine\" \"${CONFIGURATION_BUILD_DIR}/${WRAPPER_NAME}\"\nfi\n"; + }; B9442FD26CE9A85A43FC2CFA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -600,6 +634,7 @@ 024E69202AEFC3FB00FA0B59 /* MainScreenViewModel.swift in Sources */, 065275372BB1B4070093BCCA /* PipManager.swift in Sources */, 0770DE2028D0858A006D8A5D /* Router.swift in Sources */, + 14D912D72C2551F60077CCCE /* FullStoryAnalyticsService.swift in Sources */, 0293A2092A6FCDE50090A336 /* DashboardPersistence.swift in Sources */, 0770DE1728D080A1006D8A5D /* RouteController.swift in Sources */, A59568952B61630500ED4F90 /* DeepLinkManager.swift in Sources */, @@ -722,6 +757,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; @@ -810,6 +846,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; @@ -904,6 +941,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; @@ -992,6 +1030,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; @@ -1140,6 +1179,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; @@ -1174,6 +1214,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; @@ -1236,6 +1277,14 @@ requirement = { kind = upToNextMajorVersion; minimumVersion = 10.26.0; + }; + }; + 14D912D12C25483F0077CCCE /* XCRemoteSwiftPackageReference "fullstory-swift-package-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/fullstorydev/fullstory-swift-package-ios"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.49.0; }; }; A51CDBEB2B6D2BEE009B6D4E /* XCRemoteSwiftPackageReference "braze-segment-swift" */ = { @@ -1282,6 +1331,11 @@ isa = XCSwiftPackageProductDependency; package = 0780ABE12BFBA2E40093A4A6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseMessaging; + }; + 14D912D22C25483F0077CCCE /* FullStory */ = { + isa = XCSwiftPackageProductDependency; + package = 14D912D12C25483F0077CCCE /* XCRemoteSwiftPackageReference "fullstory-swift-package-ios" */; + productName = FullStory; }; A51CDBEC2B6D2BEE009B6D4E /* SegmentBraze */ = { isa = XCSwiftPackageProductDependency; diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index cabc57d33..a39285e7e 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -203,6 +203,10 @@ class AppAssembly: Assembly { FirebaseAnalyticsService() }.inObjectScope(.container) + container.register(FullStoryAnalyticsService.self) { r in + FullStoryAnalyticsService() + }.inObjectScope(.container) + container.register(PipManagerProtocol.self) { r in let config = r.resolve(ConfigProtocol.self)! return PipManager( diff --git a/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift b/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift index 8208d1047..404ffed0e 100644 --- a/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift +++ b/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift @@ -19,6 +19,7 @@ import Swinject protocol AnalyticsService { func identify(id: String, username: String?, email: String?) func logEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) + func logScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) } // swiftlint:disable type_body_length file_length @@ -52,6 +53,12 @@ class AnalyticsManager: AuthorizationAnalytics, let segmentService = Container.shared.resolve(SegmentAnalyticsService.self) { analyticsServices.append(segmentService) } + + if config.fullStory.enabled, + let fullStoryService = Container.shared.resolve(FullStoryAnalyticsService.self) { + analyticsServices.append(fullStoryService) + } + return analyticsServices } @@ -67,6 +74,12 @@ class AnalyticsManager: AuthorizationAnalytics, } } + private func logScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]? = nil) { + for service in services { + service.logScreenEvent(event, parameters: parameters) + } + } + // MARK: Generic event tracker functions public func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]? = nil) { logEvent(event, parameters: parameters) @@ -86,6 +99,24 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(event, parameters: [EventParamKey.name: biValue.rawValue]) } + public func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]? = nil) { + logScreenEvent(event, parameters: parameters) + } + + public func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + var eventParams: [String: Any] = [EventParamKey.name: biValue.rawValue] + + if let parameters { + eventParams.merge(parameters, uniquingKeysWith: { (first, _) in first }) + } + + logScreenEvent(event, parameters: eventParams) + } + + private func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + logScreenEvent(event, parameters: [EventParamKey.name: biValue.rawValue]) + } + // MARK: Pre Login public func userLogin(method: AuthMethod) { @@ -132,10 +163,14 @@ class AnalyticsManager: AuthorizationAnalytics, ) } + public func authTrackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + trackScreenEvent(event, biValue: biValue) + } + // MARK: MainScreenAnalytics public func mainDiscoveryTabClicked() { - trackEvent(.mainDiscoveryTabClicked, biValue: .mainDiscoveryTabClicked) + trackScreenEvent(.mainDiscoveryTabClicked, biValue: .mainDiscoveryTabClicked) } public func mainDashboardTabClicked() { @@ -143,11 +178,11 @@ class AnalyticsManager: AuthorizationAnalytics, } public func mainProgramsTabClicked() { - trackEvent(.mainProgramsTabClicked, biValue: .mainProgramsTabClicked) + trackScreenEvent(.mainProgramsTabClicked, biValue: .mainProgramsTabClicked) } public func mainProfileTabClicked() { - trackEvent(.mainProfileTabClicked, biValue: .mainProfileTabClicked) + trackScreenEvent(.mainProfileTabClicked, biValue: .mainProfileTabClicked) } // MARK: Discovery @@ -180,7 +215,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.dashboardCourseClicked.rawValue ] - logEvent(.dashboardCourseClicked, parameters: parameters) + logScreenEvent(.dashboardCourseClicked, parameters: parameters) } // MARK: Profile @@ -271,7 +306,7 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(event, parameters: parameters) } - public func profileEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + public func profileTrackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { let parameters = [ EventParamKey.category: EventCategory.profile, EventParamKey.name: biValue.rawValue @@ -280,6 +315,15 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(event, parameters: parameters) } + public func profileScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + let parameters = [ + EventParamKey.category: EventCategory.profile, + EventParamKey.name: biValue.rawValue + ] + + logScreenEvent(event, parameters: parameters) + } + public func privacyPolicyClicked() { let parameters = [ EventParamKey.name: EventBIValue.privacyPolicyClicked.rawValue, @@ -380,13 +424,13 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(.externalLinkOpenAlertAction, parameters: parameters) } - public func discoveryEvent(event: AnalyticsEvent, biValue: EventBIValue) { + public func discoveryScreenEvent(event: AnalyticsEvent, biValue: EventBIValue) { let parameters = [ EventParamKey.category: EventCategory.discovery, EventParamKey.name: biValue.rawValue ] - logEvent(event, parameters: parameters) + logScreenEvent(event, parameters: parameters) } public func viewCourseClicked(courseId: String, courseName: String) { @@ -395,7 +439,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.category: EventCategory.discovery ] - logEvent(.viewCourseClicked, parameters: parameters) + logScreenEvent(.viewCourseClicked, parameters: parameters) } public func resumeCourseClicked(courseId: String, courseName: String, blockId: String) { @@ -493,7 +537,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineCourseTabClicked.rawValue ] - logEvent(.courseOutlineCourseTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineCourseTabClicked, parameters: parameters) } public func courseOutlineVideosTabClicked(courseId: String, courseName: String) { @@ -502,7 +546,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineVideosTabClicked.rawValue ] - logEvent(.courseOutlineVideosTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineVideosTabClicked, parameters: parameters) } public func courseOutlineDatesTabClicked(courseId: String, courseName: String) { @@ -511,7 +555,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineDatesTabClicked.rawValue ] - logEvent(.courseOutlineDatesTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineDatesTabClicked, parameters: parameters) } public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) { @@ -520,7 +564,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineDiscussionTabClicked.rawValue ] - logEvent(.courseOutlineDiscussionTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineDiscussionTabClicked, parameters: parameters) } public func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) { @@ -529,7 +573,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineHandoutsTabClicked.rawValue ] - logEvent(.courseOutlineHandoutsTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineHandoutsTabClicked, parameters: parameters) } func datesComponentTapped( @@ -616,6 +660,16 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(event, parameters: parameters) } + public func trackCourseScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) { + let parameters = [ + EventParamKey.courseID: courseID, + EventParamKey.category: EventCategory.course, + EventParamKey.name: biValue.rawValue + ] + + logScreenEvent(event, parameters: parameters) + } + public func plsEvent( _ event: AnalyticsEvent, bivalue: EventBIValue, diff --git a/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift b/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift index fe49bc007..396af313f 100644 --- a/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift +++ b/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift @@ -26,6 +26,10 @@ class FirebaseAnalyticsService: AnalyticsService { Analytics.logEvent(name, parameters: formatParamaters(params: parameters)) } + + func logScreenEvent(_ event: Core.AnalyticsEvent, parameters: [String: Any]?) { + logEvent(event, parameters: parameters) + } } extension FirebaseAnalyticsService { diff --git a/OpenEdX/Managers/FullStoryAnalyticsService/FullStoryAnalyticsService.swift b/OpenEdX/Managers/FullStoryAnalyticsService/FullStoryAnalyticsService.swift new file mode 100644 index 000000000..69b594cd7 --- /dev/null +++ b/OpenEdX/Managers/FullStoryAnalyticsService/FullStoryAnalyticsService.swift @@ -0,0 +1,25 @@ +// +// FullStoryAnalyticsService.swift +// OpenEdX +// +// Created by Saeed Bashir on 4/17/24. +// + +import Foundation +import Core +import FullStory + +class FullStoryAnalyticsService: AnalyticsService { + + func identify(id: String, username: String?, email: String?) { + FS.identify(id, userVars: ["displayName": id]) + } + + func logEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + FS.event(event.rawValue, properties: parameters ?? [:]) + } + + func logScreenEvent(_ event: Core.AnalyticsEvent, parameters: [String: Any]?) { + FS.page(withName: event.rawValue, properties: parameters).start() + } +} diff --git a/OpenEdX/Managers/SegmentAnalyticsService/SegmentAnalyticsService.swift b/OpenEdX/Managers/SegmentAnalyticsService/SegmentAnalyticsService.swift index b8c2cd422..fad86a131 100644 --- a/OpenEdX/Managers/SegmentAnalyticsService/SegmentAnalyticsService.swift +++ b/OpenEdX/Managers/SegmentAnalyticsService/SegmentAnalyticsService.swift @@ -41,4 +41,8 @@ class SegmentAnalyticsService: AnalyticsService { properties: parameters ) } + + func logScreenEvent(_ event: Core.AnalyticsEvent, parameters: [String: Any]?) { + analytics?.screen(title: event.rawValue, properties: parameters) + } } diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift index 2ec255976..cf432b238 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift @@ -236,6 +236,9 @@ public struct EditProfileView: View { Theme.Colors.background .ignoresSafeArea() ) + .onFirstAppear { + viewModel.trackScreenEvent() + } } } } diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift index 4635416af..c877a6ecd 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift @@ -367,5 +367,9 @@ public class EditProfileViewModel: ObservableObject { func trackProfileEditDoneClicked() { analytics.profileEditDoneClicked() } + + func trackScreenEvent() { + analytics.profileScreenEvent(.profileEdit, biValue: .profileEdit) + } } // swiftlint:enable type_body_length diff --git a/Profile/Profile/Presentation/ProfileAnalytics.swift b/Profile/Profile/Presentation/ProfileAnalytics.swift index 3713c15ba..6af217a04 100644 --- a/Profile/Profile/Presentation/ProfileAnalytics.swift +++ b/Profile/Profile/Presentation/ProfileAnalytics.swift @@ -25,7 +25,8 @@ public protocol ProfileAnalytics { func profileWifiToggle(action: String) func profileUserDeleteAccountClicked() func profileDeleteAccountSuccess(success: Bool) - func profileEvent(_ event: AnalyticsEvent, biValue: EventBIValue) + func profileTrackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) + func profileScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) } #if DEBUG @@ -45,6 +46,7 @@ class ProfileAnalyticsMock: ProfileAnalytics { public func profileWifiToggle(action: String) {} public func profileUserDeleteAccountClicked() {} public func profileDeleteAccountSuccess(success: Bool) {} - public func profileEvent(_ event: AnalyticsEvent, biValue: EventBIValue) {} + public func profileTrackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) {} + public func profileScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) {} } #endif diff --git a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift index ce6c4477b..1fe8ada15 100644 --- a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift +++ b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift @@ -179,7 +179,7 @@ public class SettingsViewModel: ObservableObject { } func trackLogoutClickedClicked() { - analytics.profileEvent(.userLogoutClicked, biValue: .userLogoutClicked) + analytics.profileTrackEvent(.userLogoutClicked, biValue: .userLogoutClicked) } } diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index 35a5cae13..1817697a4 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -1171,6 +1171,18 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`, `parameters`) } + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void @@ -1195,14 +1207,30 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { perform?(`event`, `biValue`) } + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + fileprivate enum MethodType { case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) case m_trackEvent__event(Parameter) case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1219,6 +1247,19 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) return Matcher.ComparisonResult(results) + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) @@ -1245,6 +1286,17 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -1253,20 +1305,28 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { switch self { case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackEvent__event(p0): return p0.intValue case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" case .m_trackEvent__event: return ".trackEvent(_:)" case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" } } } @@ -1287,10 +1347,14 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -1303,6 +1367,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) } @@ -1315,6 +1385,12 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } } public func given(_ method: Given) { @@ -2153,9 +2229,15 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { perform?(`success`) } - open func profileEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_profileEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_profileEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + open func profileTrackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_profileEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(biValue))) + let perform = methodPerformValue(.m_profileEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(biValue))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, biValue) + } + + open func profileScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_profileScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_profileScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void perform?(`event`, `biValue`) } @@ -2177,6 +2259,7 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { case m_profileUserDeleteAccountClicked case m_profileDeleteAccountSuccess__success_success(Parameter) case m_profileEvent__eventbiValue_biValue(Parameter, Parameter) + case m_profileScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -2227,6 +2310,12 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) + + case (.m_profileScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_profileScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -2249,6 +2338,7 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { case .m_profileUserDeleteAccountClicked: return 0 case let .m_profileDeleteAccountSuccess__success_success(p0): return p0.intValue case let .m_profileEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_profileScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { @@ -2269,6 +2359,7 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { case .m_profileUserDeleteAccountClicked: return ".profileUserDeleteAccountClicked()" case .m_profileDeleteAccountSuccess__success_success: return ".profileDeleteAccountSuccess(success:)" case .m_profileEvent__eventbiValue_biValue: return ".profileEvent(_:biValue:)" + case .m_profileScreenEvent__eventbiValue_biValue: return ".profileScreenEvent(_:biValue:)" } } } @@ -2303,6 +2394,7 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { public static func profileUserDeleteAccountClicked() -> Verify { return Verify(method: .m_profileUserDeleteAccountClicked)} public static func profileDeleteAccountSuccess(success: Parameter) -> Verify { return Verify(method: .m_profileDeleteAccountSuccess__success_success(`success`))} public static func profileEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_profileEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func profileScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_profileScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -2357,6 +2449,9 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { public static func profileEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { return Perform(method: .m_profileEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) } + public static func profileScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_profileScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } } public func given(_ method: Given) { diff --git a/config_script/process_config.py b/config_script/process_config.py index 1fdea0270..c03e3d8f4 100644 --- a/config_script/process_config.py +++ b/config_script/process_config.py @@ -247,6 +247,14 @@ def add_microsoft_config(self, config, plist): scheme = ["msauth." + bundle_identifier] self.add_url_scheme(scheme, plist, False) self.add_application_query_schemes(["msauthv2", "msauthv3"], plist) + + def add_fullstory_config(self, config, plist): + fullstory = config.get('FULLSTORY', {}) + enabled = fullstory.get('ENABLED') + orgID = fullstory.get('ORG_ID') + + if enabled and orgID: + plist["FullStory"] = {"orgID": orgID} def update_info_plist(self, plist_data, plist_path): if not plist_path: @@ -303,6 +311,7 @@ def process_plist_files(configuration_manager, plist_manager, config): configuration_manager.add_google_config(config, info_plist_content) configuration_manager.add_microsoft_config(config, info_plist_content) configuration_manager.add_branch_config(config, info_plist_content) + configuration_manager.add_fullstory_config(config, info_plist_content) configuration_manager.update_info_plist(info_plist_content, info_plist_path) diff --git a/config_script/whitelabel.py b/config_script/whitelabel.py index 00eab9f92..24d1b01a9 100644 --- a/config_script/whitelabel.py +++ b/config_script/whitelabel.py @@ -9,6 +9,7 @@ from PIL import Image import re from textwrap import dedent +from process_config import PlistManager # type: ignore class WhitelabelApp: EXAMPLE_CONFIG_FILE = dedent(""" @@ -46,9 +47,11 @@ class WhitelabelApp: config1: # build configuration name in project app_bundle_id: "bundle.id.app.new1" # bundle ID which should be set product_name: "Mobile App Name1" # app name which should be set + env_config: 'prod' # env name for this configuration. possible values: prod/dev/stage (values which config_settings.yaml defines) config2: # build configuration name in project app_bundle_id: "bundle.id.app.new2" # bundle ID which should be set product_name: "Mobile App Name2" # app name which should be set + env_config: 'dev' # env name for this configuration. possible values: prod/dev/stage (values which config_settings.yaml defines) font: font_import_file_path: 'path/to/importing/Font_file.ttf' # path to ttf font file what should be imported to project project_font_file_path: 'path/to/font/file/in/project/font.ttf' # path to existing ttf font file in project @@ -90,6 +93,7 @@ def whitelabel(self): self.copy_project_files() if self.project_config: self.set_app_project_config() + self.set_flags_from_mobile_config() def copy_assets(self): if self.assets: @@ -326,12 +330,12 @@ def replace_parameter_for_build_config(self, config_file_string, config_name, ne # replace existing parameter value with new value config_string_out = config_string.replace(parameter_string, new_param_string) else: - errors_texts.append("project_config->configurations->"+config_name+": Check regex please. Can't find place in project file where insert '"+new_param_string+"'") + errors_texts.append(config_name+": Check regex please. Can't find place in project file where place '"+new_param_string+"'") # if something found if config_string != config_string_out: config_file_string = config_file_string.replace(config_string, config_string_out) else: - errors_texts.append("project_config->configurations->"+config_name+": not found in project file") + errors_texts.append(config_name+": not found in project file") return config_file_string def regex_string_for_build_config(self, build_config): @@ -507,6 +511,96 @@ def copy_project_files(self): else: logging.debug("Project's Files for copying not found in config") + # params from MOBILE CONFIG + CONFIG_SETTINGS_YAML_FILENAME = 'config_settings.yaml' + DEFAULT_CONFIG_PATH = './default_config/' + CONFIG_SETTINGS_YAML_FILENAME + CONFIG_DIRECTORY_NAME = 'config_directory' + CONFIG_MAPPINGS = 'config_mapping' + MAPPINGS_FILENAME = 'file_mappings.yaml' + + def parse_yaml(self, file_path): + try: + with open(file_path, 'r') as file: + return yaml.safe_load(file) + except Exception as e: + logging.error(f"Unable to open or read the file '{file_path}': {e}") + return None + + def get_mobile_config(self, config_directory, config_folder, errors_texts): + # get path to mappings file + path = os.path.join(config_directory, config_folder) + mappings_path = os.path.join(path, self.MAPPINGS_FILENAME) + # read mappings file + data = self.parse_yaml(mappings_path) + if data: + # get config for ios described in mappings file + ios_files = data.get('ios', {}).get('files', []) + # re-use PlistManager class from process_config.py script + plist_manager = PlistManager(path, ios_files) + config = plist_manager.load_config() + if config: + return config + else: + errors_texts.append("Unable to parse config for "+config_folder) + else: + errors_texts.append("Files mappings for "+config_folder+" not found") + return None + + def set_flags_from_mobile_config(self): + # get path to mobile config + config_settings = self.parse_yaml(self.CONFIG_SETTINGS_YAML_FILENAME) + if not config_settings: + config_settings = self.parse_yaml(self.DEFAULT_CONFIG_PATH) + config_directory = config_settings.get(self.CONFIG_DIRECTORY_NAME) + # check if we found config directory + if config_directory: + # check if configurations exist + if "configurations" in self.project_config: + configurations = self.project_config["configurations"] + # read project file + with open(self.config_project_path, 'r') as openfile: + project_file_string = openfile.read() + errors_texts = [] + # iterate for all configurations + for name, config in configurations.items(): + if 'env_config' in config: + # get folder name for mobile config for current configuration by env_config + config_folder = config_settings.get(self.CONFIG_MAPPINGS, {}).get(config['env_config']) + if config_folder: + # replace fullstory flag + project_file_string = self.replace_fullstory_flag(project_file_string, config_directory, name, config_folder, errors_texts) + else: + logging.error("Config folder for '"+config['env_config']+"' is not defined in config_settings.yaml->config_mapping") + else: + logging.error("'env_config' is not defined for "+name) + # write to project file + with open(self.config_project_path, 'w') as openfile: + openfile.write(project_file_string) + # print success message or errors if are presented + if len(errors_texts) == 0: + logging.debug("Mobile config user-defined flags were successfully changed") + else: + for error in errors_texts: + logging.error(error) + else: + logging.error("Project configurations are not defined") + else: + logging.error("Mobile config directory not found") + + def replace_fullstory_flag(self, project_file_string, config_directory, config_name, config_folder, errors_texts): + # get mobile config + mobile_config = self.get_mobile_config(config_directory, config_folder, errors_texts) + if mobile_config: + # get FULLSTORY settings from mobile config + fullstory_config = mobile_config.get('FULLSTORY', {}) + if fullstory_config: + fullstory_config_enabled = fullstory_config.get('ENABLED') + fullstory_string = "FULLSTORY_ENABLED = YES;" if fullstory_config_enabled else "FULLSTORY_ENABLED = NO;" + fullstory_regex = "FULLSTORY_ENABLED = .*;" + # serach by regex and replace + project_file_string = self.replace_parameter_for_build_config(project_file_string, config_name, fullstory_string, fullstory_regex, errors_texts) + return project_file_string + def main(): """ Parse the command line arguments, and pass them to WhitelabelApp. From bd270b7be92a31cfa9c07f7e50c9f4c06161075e Mon Sep 17 00:00:00 2001 From: Vadim Kuznetsov Date: Tue, 9 Jul 2024 09:49:23 +0300 Subject: [PATCH 7/8] Fix: CoreData crash (#480) * fix: crash with CoreData * chore: moved to coreData to background queue * chore: moved core data work to background * chore: review's changes --- .../Persistence/CorePersistenceProtocol.swift | 11 +- Core/Core/Network/DownloadManager.swift | 107 ++++---- Course/Course/Data/CourseRepository.swift | 8 +- .../CoursePersistenceProtocol.swift | 6 +- Course/Course/Domain/CourseInteractor.swift | 2 +- .../Container/CourseContainerViewModel.swift | 10 +- .../CourseVerticalViewModel.swift | 2 +- .../Dashboard/Data/DashboardRepository.swift | 8 +- .../DashboardPersistenceProtocol.swift | 4 +- .../Domain/DashboardInteractor.swift | 6 +- .../Presentation/ListDashboardViewModel.swift | 2 +- .../Discovery/Data/DiscoveryRepository.swift | 8 +- .../DiscoveryPersistenceProtocol.swift | 4 +- .../Domain/DiscoveryInteractor.swift | 6 +- .../NativeDiscovery/DiscoveryViewModel.swift | 2 +- OpenEdX/Data/CorePersistence.swift | 199 +++++++-------- OpenEdX/Data/CoursePersistence.swift | 228 +++++++++--------- OpenEdX/Data/DashboardPersistence.swift | 215 +++++++++-------- OpenEdX/Data/DiscoveryPersistence.swift | 84 ++++--- 19 files changed, 460 insertions(+), 452 deletions(-) diff --git a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift index 3a6ec2acf..49d69b1c3 100644 --- a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift +++ b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift @@ -12,15 +12,14 @@ public protocol CorePersistenceProtocol { func set(userId: Int) func getUserID() -> Int? func publisher() -> AnyPublisher - func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) - func nextBlockForDownloading() -> DownloadDataTask? + func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) async + func nextBlockForDownloading() async -> DownloadDataTask? func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) - func deleteDownloadDataTask(id: String) throws + func deleteDownloadDataTask(id: String) async throws func saveDownloadDataTask(_ task: DownloadDataTask) func downloadDataTask(for blockId: String) -> DownloadDataTask? - func downloadDataTask(for blockId: String, completion: @escaping (DownloadDataTask?) -> Void) - func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void) - func getDownloadDataTasksForCourse(_ courseId: String, completion: @escaping ([DownloadDataTask]) -> Void) + func getDownloadDataTasks() async -> [DownloadDataTask] + func getDownloadDataTasksForCourse(_ courseId: String) async -> [DownloadDataTask] } public final class CoreBundle { diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift index 1db912da9..bc3701695 100644 --- a/Core/Core/Network/DownloadManager.swift +++ b/Core/Core/Network/DownloadManager.swift @@ -106,7 +106,7 @@ public protocol DownloadManagerProtocol { func publisher() -> AnyPublisher func eventPublisher() -> AnyPublisher - func addToDownloadQueue(blocks: [CourseBlock]) throws + func addToDownloadQueue(blocks: [CourseBlock]) async throws func getDownloadTasks() async -> [DownloadDataTask] func getDownloadTasksForCourse(_ courseId: String) async -> [DownloadDataTask] @@ -119,10 +119,9 @@ public protocol DownloadManagerProtocol { func deleteFile(blocks: [CourseBlock]) async func deleteAllFiles() async - func fileUrl(for blockId: String) async -> URL? - - func resumeDownloading() throws func fileUrl(for blockId: String) -> URL? + + func resumeDownloading() async throws func isLargeVideosSize(blocks: [CourseBlock]) -> Bool func removeAppSupportDirectoryUnusedContent() @@ -142,7 +141,6 @@ public enum DownloadManagerEvent { } public class DownloadManager: DownloadManagerProtocol { - // MARK: - Properties public var currentDownloadTask: DownloadDataTask? @@ -173,7 +171,9 @@ public class DownloadManager: DownloadManagerProtocol { self.appStorage = appStorage self.connectivity = connectivity self.backgroundTask() - try? self.resumeDownloading() + Task { + try? await self.resumeDownloading() + } } // MARK: - Publishers @@ -197,37 +197,29 @@ public class DownloadManager: DownloadManagerProtocol { } public func getDownloadTasks() async -> [DownloadDataTask] { - await withCheckedContinuation { continuation in - persistence.getDownloadDataTasks { downloads in - continuation.resume(returning: downloads) - } - } + await persistence.getDownloadDataTasks() } public func getDownloadTasksForCourse(_ courseId: String) async -> [DownloadDataTask] { - await withCheckedContinuation { continuation in - persistence.getDownloadDataTasksForCourse(courseId) { downloads in - continuation.resume(returning: downloads) - } - } + await persistence.getDownloadDataTasksForCourse(courseId) } - public func addToDownloadQueue(blocks: [CourseBlock]) throws { + public func addToDownloadQueue(blocks: [CourseBlock]) async throws { if userCanDownload() { - persistence.addToDownloadQueue( + await persistence.addToDownloadQueue( blocks: blocks, downloadQuality: downloadQuality ) currentDownloadEventPublisher.send(.added) guard !isDownloadingInProgress else { return } - try newDownload() + try await newDownload() } else { throw NoWiFiError() } } - public func resumeDownloading() throws { - try newDownload() + public func resumeDownloading() async throws { + try await newDownload() } public func cancelDownloading(courseId: String, blocks: [CourseBlock]) async throws { @@ -240,13 +232,13 @@ public class DownloadManager: DownloadManagerProtocol { downloaded.forEach { currentDownloadEventPublisher.send(.canceled($0)) } - try newDownload() + try await newDownload() } public func cancelDownloading(task: DownloadDataTask) async throws { downloadRequest?.cancel() do { - try persistence.deleteDownloadDataTask(id: task.id) + try await persistence.deleteDownloadDataTask(id: task.id) if let fileUrl = await fileUrl(for: task.id) { try FileManager.default.removeItem(at: fileUrl) } @@ -254,7 +246,7 @@ public class DownloadManager: DownloadManagerProtocol { } catch { NSLog("Error deleting file: \(error.localizedDescription)") } - try newDownload() + try await newDownload() } public func cancelDownloading(courseId: String) async throws { @@ -262,7 +254,7 @@ public class DownloadManager: DownloadManagerProtocol { await cancel(tasks: tasks) currentDownloadEventPublisher.send(.courseCanceled(courseId)) downloadRequest?.cancel() - try newDownload() + try await newDownload() } public func cancelAllDownloading() async throws { @@ -270,7 +262,7 @@ public class DownloadManager: DownloadManagerProtocol { await cancel(tasks: tasks) currentDownloadEventPublisher.send(.allCanceled) downloadRequest?.cancel() - try newDownload() + try await newDownload() } public func deleteFile(blocks: [CourseBlock]) async { @@ -279,7 +271,7 @@ public class DownloadManager: DownloadManagerProtocol { if let fileURL = await fileUrl(for: block.id) { try FileManager.default.removeItem(at: fileURL) } - try persistence.deleteDownloadDataTask(id: block.id) + try await persistence.deleteDownloadDataTask(id: block.id) currentDownloadEventPublisher.send(.deletedFile(block.id)) } catch { debugLog("Error deleting file: \(error.localizedDescription)") @@ -301,24 +293,13 @@ public class DownloadManager: DownloadManagerProtocol { currentDownloadEventPublisher.send(.clearedAll) } - public func fileUrl(for blockId: String) async -> URL? { - await withCheckedContinuation { continuation in - persistence.downloadDataTask(for: blockId) { [weak self] data in - guard let data = data, data.url.count > 0, data.state == .finished else { - continuation.resume(returning: nil) - return - } - let path = self?.videosFolderUrl - let fileName = data.fileName - continuation.resume(returning: path?.appendingPathComponent(fileName)) - } - } - } - public func fileUrl(for blockId: String) -> URL? { guard let data = persistence.downloadDataTask(for: blockId), data.url.count > 0, - data.state == .finished else { return nil } + data.state == .finished + else { + return nil + } let path = videosFolderUrl let fileName = data.fileName return path?.appendingPathComponent(fileName) @@ -326,11 +307,11 @@ public class DownloadManager: DownloadManagerProtocol { // MARK: - Private Intents - private func newDownload() throws { + private func newDownload() async throws { guard userCanDownload() else { throw NoWiFiError() } - guard let downloadTask = persistence.nextBlockForDownloading() else { + guard let downloadTask = await persistence.nextBlockForDownloading() else { isDownloadingInProgress = false return } @@ -389,32 +370,30 @@ public class DownloadManager: DownloadManagerProtocol { ) self.currentDownloadTask?.state = .finished self.currentDownloadEventPublisher.send(.finished(download)) - try? self.newDownload() + Task { + try? await self.newDownload() + } } } } - private func waitingAll() { - persistence.getDownloadDataTasks { [weak self] tasks in - guard let self else { return } - Task { - for task in tasks.filter({ $0.state == .inProgress }) { - self.persistence.updateDownloadState( - id: task.id, - state: .waiting, - resumeData: nil - ) - self.currentDownloadEventPublisher.send(.added) - } - self.downloadRequest?.cancel() - } + private func waitingAll() async { + let tasks = await persistence.getDownloadDataTasks() + for task in tasks.filter({ $0.state == .inProgress }) { + self.persistence.updateDownloadState( + id: task.id, + state: .waiting, + resumeData: nil + ) + self.currentDownloadEventPublisher.send(.added) } + self.downloadRequest?.cancel() } private func cancel(tasks: [DownloadDataTask]) async { for task in tasks { do { - try persistence.deleteDownloadDataTask(id: task.id) + try await persistence.deleteDownloadDataTask(id: task.id) if let fileUrl = await fileUrl(for: task.id) { try FileManager.default.removeItem(at: fileUrl) } @@ -428,9 +407,11 @@ public class DownloadManager: DownloadManagerProtocol { backgroundTaskProvider.eventPublisher() .sink { [weak self] state in guard let self else { return } - switch state { - case.didBecomeActive: try? self.resumeDownloading() - case .didEnterBackground: self.waitingAll() + Task { + switch state { + case.didBecomeActive: try? await self.resumeDownloading() + case .didEnterBackground: await self.waitingAll() + } } } .store(in: &cancellables) diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index 612273fe5..e5d4106b5 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -10,7 +10,7 @@ import Core public protocol CourseRepositoryProtocol { func getCourseBlocks(courseID: String) async throws -> CourseStructure - func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure + func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure func blockCompletionRequest(courseID: String, blockID: String) async throws func getHandouts(courseID: String) async throws -> String? func getUpdates(courseID: String) async throws -> [CourseUpdate] @@ -50,8 +50,8 @@ public class CourseRepository: CourseRepositoryProtocol { return parsedStructure } - public func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure { - let localData = try persistence.loadCourseStructure(courseID: courseID) + public func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure { + let localData = try await persistence.loadCourseStructure(courseID: courseID) return parseCourseStructure(course: localData) } @@ -85,7 +85,7 @@ public class CourseRepository: CourseRepositoryProtocol { } public func getSubtitles(url: String, selectedLanguage: String) async throws -> String { - if let subtitlesOffline = persistence.loadSubtitles(url: url + selectedLanguage) { + if let subtitlesOffline = await persistence.loadSubtitles(url: url + selectedLanguage) { return subtitlesOffline } else { let result = try await api.requestData(CourseEndpoint.getSubtitles( diff --git a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift b/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift index 35f8328c2..ff3080a19 100644 --- a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift +++ b/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift @@ -9,12 +9,12 @@ import CoreData import Core public protocol CoursePersistenceProtocol { - func loadEnrollments() throws -> [Core.CourseItem] + func loadEnrollments() async throws -> [Core.CourseItem] func saveEnrollments(items: [Core.CourseItem]) - func loadCourseStructure(courseID: String) throws -> DataLayer.CourseStructure + func loadCourseStructure(courseID: String) async throws -> DataLayer.CourseStructure func saveCourseStructure(structure: DataLayer.CourseStructure) func saveSubtitles(url: String, subtitlesString: String) - func loadSubtitles(url: String) -> String? + func loadSubtitles(url: String) async -> String? func saveCourseDates(courseID: String, courseDates: CourseDates) func loadCourseDates(courseID: String) throws -> CourseDates } diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift index f76d5ed2e..dcd9eac1d 100644 --- a/Course/Course/Domain/CourseInteractor.swift +++ b/Course/Course/Domain/CourseInteractor.swift @@ -64,7 +64,7 @@ public class CourseInteractor: CourseInteractorProtocol { } public func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure { - return try repository.getLoadedCourseBlocks(courseID: courseID) + return try await repository.getLoadedCourseBlocks(courseID: courseID) } public func blockCompletionRequest(courseID: String, blockID: String) async throws { diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 51e687f3e..8f95d25b9 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -346,12 +346,12 @@ public class CourseContainerViewModel: BaseCourseViewModel { return tasks } - func continueDownload() { + func continueDownload() async { guard let blocks = waitingDownloads else { return } do { - try manager.addToDownloadQueue(blocks: blocks) + try await manager.addToDownloadQueue(blocks: blocks) } catch let error { if error is NoWiFiError { errorMessage = CoreLocalization.Error.wifi @@ -481,7 +481,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { do { switch state { case .available: - try manager.addToDownloadQueue(blocks: blocks) + try await manager.addToDownloadQueue(blocks: blocks) case .downloading: try await manager.cancelDownloading(courseId: courseStructure?.id ?? "", blocks: blocks) case .finished: @@ -507,7 +507,9 @@ public class CourseContainerViewModel: BaseCourseViewModel { self.router.dismiss(animated: true) }, okTapped: { - self.continueDownload() + Task { + await self.continueDownload() + } self.router.dismiss(animated: true) }, type: .default(positiveAction: CourseLocalization.Alert.accept, image: nil) diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift index 4ce5ebd70..5247ca700 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift @@ -67,7 +67,7 @@ public class CourseVerticalViewModel: BaseCourseViewModel { do { switch state { case .available: - try manager.addToDownloadQueue(blocks: blocks) + try await manager.addToDownloadQueue(blocks: blocks) downloadState[vertical.id] = .downloading case .downloading: try await manager.cancelDownloading(courseId: vertical.courseId, blocks: blocks) diff --git a/Dashboard/Dashboard/Data/DashboardRepository.swift b/Dashboard/Dashboard/Data/DashboardRepository.swift index df8cea243..00fe98df7 100644 --- a/Dashboard/Dashboard/Data/DashboardRepository.swift +++ b/Dashboard/Dashboard/Data/DashboardRepository.swift @@ -10,7 +10,7 @@ import Core public protocol DashboardRepositoryProtocol { func getEnrollments(page: Int) async throws -> [CourseItem] - func getEnrollmentsOffline() throws -> [CourseItem] + func getEnrollmentsOffline() async throws -> [CourseItem] func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment @@ -41,8 +41,8 @@ public class DashboardRepository: DashboardRepositoryProtocol { } - public func getEnrollmentsOffline() throws -> [CourseItem] { - return try persistence.loadEnrollments() + public func getEnrollmentsOffline() async throws -> [CourseItem] { + return try await persistence.loadEnrollments() } public func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment { @@ -59,7 +59,7 @@ public class DashboardRepository: DashboardRepositoryProtocol { } public func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment { - return try persistence.loadPrimaryEnrollment() + return try await persistence.loadPrimaryEnrollment() } public func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment { diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift index 3747d2c8e..2257c8238 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift +++ b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift @@ -9,9 +9,9 @@ import CoreData import Core public protocol DashboardPersistenceProtocol { - func loadEnrollments() throws -> [CourseItem] + func loadEnrollments() async throws -> [CourseItem] func saveEnrollments(items: [CourseItem]) - func loadPrimaryEnrollment() throws -> PrimaryEnrollment + func loadPrimaryEnrollment() async throws -> PrimaryEnrollment func savePrimaryEnrollment(enrollments: PrimaryEnrollment) } diff --git a/Dashboard/Dashboard/Domain/DashboardInteractor.swift b/Dashboard/Dashboard/Domain/DashboardInteractor.swift index 0d55f0a4e..60a920eaf 100644 --- a/Dashboard/Dashboard/Domain/DashboardInteractor.swift +++ b/Dashboard/Dashboard/Domain/DashboardInteractor.swift @@ -11,7 +11,7 @@ import Core //sourcery: AutoMockable public protocol DashboardInteractorProtocol { func getEnrollments(page: Int) async throws -> [CourseItem] - func getEnrollmentsOffline() throws -> [CourseItem] + func getEnrollmentsOffline() async throws -> [CourseItem] func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment @@ -30,8 +30,8 @@ public class DashboardInteractor: DashboardInteractorProtocol { return try await repository.getEnrollments(page: page) } - public func getEnrollmentsOffline() throws -> [CourseItem] { - return try repository.getEnrollmentsOffline() + public func getEnrollmentsOffline() async throws -> [CourseItem] { + return try await repository.getEnrollmentsOffline() } public func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment { diff --git a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift index bef00add1..f962824e9 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift @@ -75,7 +75,7 @@ public class ListDashboardViewModel: ObservableObject { } fetchInProgress = false } else { - courses = try interactor.getEnrollmentsOffline() + courses = try await interactor.getEnrollmentsOffline() self.nextPage += 1 fetchInProgress = false } diff --git a/Discovery/Discovery/Data/DiscoveryRepository.swift b/Discovery/Discovery/Data/DiscoveryRepository.swift index 1d6f5e0a9..fd2121693 100644 --- a/Discovery/Discovery/Data/DiscoveryRepository.swift +++ b/Discovery/Discovery/Data/DiscoveryRepository.swift @@ -13,7 +13,7 @@ import Alamofire public protocol DiscoveryRepositoryProtocol { func getDiscovery(page: Int) async throws -> [CourseItem] func searchCourses(page: Int, searchTerm: String) async throws -> [CourseItem] - func getDiscoveryOffline() throws -> [CourseItem] + func getDiscoveryOffline() async throws -> [CourseItem] func getCourseDetails(courseID: String) async throws -> CourseDetails func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails func enrollToCourse(courseID: String) async throws -> Bool @@ -44,8 +44,8 @@ public class DiscoveryRepository: DiscoveryRepositoryProtocol { return discoveryResponse } - public func getDiscoveryOffline() throws -> [CourseItem] { - return try persistence.loadDiscovery() + public func getDiscoveryOffline() async throws -> [CourseItem] { + try await persistence.loadDiscovery() } public func searchCourses(page: Int, searchTerm: String) async throws -> [CourseItem] { @@ -68,7 +68,7 @@ public class DiscoveryRepository: DiscoveryRepositoryProtocol { } public func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails { - return try persistence.loadCourseDetails(courseID: courseID) + try await persistence.loadCourseDetails(courseID: courseID) } public func enrollToCourse(courseID: String) async throws -> Bool { diff --git a/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift b/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift index 1c8b3fd6c..0445a690c 100644 --- a/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift +++ b/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift @@ -9,9 +9,9 @@ import CoreData import Core public protocol DiscoveryPersistenceProtocol { - func loadDiscovery() throws -> [CourseItem] + func loadDiscovery() async throws -> [CourseItem] func saveDiscovery(items: [CourseItem]) - func loadCourseDetails(courseID: String) throws -> CourseDetails + func loadCourseDetails(courseID: String) async throws -> CourseDetails func saveCourseDetails(course: CourseDetails) } diff --git a/Discovery/Discovery/Domain/DiscoveryInteractor.swift b/Discovery/Discovery/Domain/DiscoveryInteractor.swift index a0bffe3ca..403463dc5 100644 --- a/Discovery/Discovery/Domain/DiscoveryInteractor.swift +++ b/Discovery/Discovery/Domain/DiscoveryInteractor.swift @@ -11,7 +11,7 @@ import Core //sourcery: AutoMockable public protocol DiscoveryInteractorProtocol { func discovery(page: Int) async throws -> [CourseItem] - func discoveryOffline() throws -> [CourseItem] + func discoveryOffline() async throws -> [CourseItem] func search(page: Int, searchTerm: String) async throws -> [CourseItem] func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails func getCourseDetails(courseID: String) async throws -> CourseDetails @@ -34,8 +34,8 @@ public class DiscoveryInteractor: DiscoveryInteractorProtocol { return try await repository.searchCourses(page: page, searchTerm: searchTerm) } - public func discoveryOffline() throws -> [CourseItem] { - return try repository.getDiscoveryOffline() + public func discoveryOffline() async throws -> [CourseItem] { + try await repository.getDiscoveryOffline() } public func getCourseDetails(courseID: String) async throws -> CourseDetails { diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift index bcb8f1022..8f64f458e 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift @@ -112,7 +112,7 @@ public class DiscoveryViewModel: ObservableObject { fetchInProgress = false } else { - courses = try interactor.discoveryOffline() + courses = try await interactor.discoveryOffline() self.nextPage += 1 fetchInProgress = false } diff --git a/OpenEdX/Data/CorePersistence.swift b/OpenEdX/Data/CorePersistence.swift index 8af067d07..f9282bd27 100644 --- a/OpenEdX/Data/CorePersistence.swift +++ b/OpenEdX/Data/CorePersistence.swift @@ -11,7 +11,30 @@ import CoreData import Combine public class CorePersistence: CorePersistenceProtocol { - + struct CorePersistenceHelper { + static func fetchCDDownloadData( + predicate: CDPredicate? = nil, + fetchLimit: Int? = nil, + context: NSManagedObjectContext, + userId: Int32? + ) throws -> [CDDownloadData] { + let request = CDDownloadData.fetchRequest() + if let predicate = predicate { + request.predicate = predicate.predicate + } + if let fetchLimit = fetchLimit { + request.fetchLimit = fetchLimit + } + let data = try context.fetch(request).filter { + guard let userId = userId else { + return true + } + debugLog(userId, "-userId-") + return $0.userId == userId + } + return data + } + } // MARK: - Predicate enum CDPredicate { @@ -53,27 +76,29 @@ public class CorePersistence: CorePersistenceProtocol { public func addToDownloadQueue( blocks: [CourseBlock], downloadQuality: DownloadQuality - ) { + ) async { + let userId = getUserId32() ?? 0 for block in blocks { let downloadDataId = downloadDataId(from: block.id) - - let data = try? fetchCDDownloadData( - predicate: CDPredicate.id(downloadDataId) - ) - guard data?.first == nil else { continue } - - guard let video = block.encodedVideo?.video(downloadQuality: downloadQuality), - let url = video.url, - let fileExtension = URL(string: url)?.pathExtension - else { continue } - - let fileName = "\(block.id).\(fileExtension)" - context.performAndWait { + await context.perform {[context] in + let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: CDPredicate.id(downloadDataId), + context: context, + userId: userId + ) + guard data?.first == nil else { return } + + guard let video = block.encodedVideo?.video(downloadQuality: downloadQuality), + let url = video.url, + let fileExtension = URL(string: url)?.pathExtension + else { return } + + let fileName = "\(block.id).\(fileExtension)" let newDownloadData = CDDownloadData(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump newDownloadData.id = downloadDataId newDownloadData.blockId = block.id - newDownloadData.userId = getUserId32() ?? 0 + newDownloadData.userId = userId newDownloadData.courseId = block.courseId newDownloadData.url = url newDownloadData.fileName = fileName @@ -87,85 +112,82 @@ public class CorePersistence: CorePersistenceProtocol { } } - public func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void) { - context.performAndWait { - guard let data = try? fetchCDDownloadData() else { - completion([]) - return + public func getDownloadDataTasks() async -> [DownloadDataTask] { + let userId = getUserId32() ?? 0 + return await context.perform {[context] in + guard let data = try? CorePersistenceHelper.fetchCDDownloadData( + context: context, + userId: userId + ) else { + return [] } let downloads = data.downloadDataTasks() - completion(downloads) + return downloads } } public func getDownloadDataTasksForCourse( - _ courseId: String, - completion: @escaping ([DownloadDataTask]) -> Void - ) { - context.performAndWait { - guard let data = try? fetchCDDownloadData( - predicate: .courseId(courseId) + _ courseId: String + ) async -> [DownloadDataTask] { + let uID = userId + let int32Id = getUserId32() + return await context.perform {[context] in + guard let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: .courseId(courseId), + context: context, + userId: int32Id ) else { - completion([]) - return + return [] } if data.isEmpty { - completion([]) - return + return [] } let downloads = data .downloadDataTasks() - .filter(userId: userId) + .filter(userId: uID) - completion(downloads) + return downloads } } - public func downloadDataTask( - for blockId: String, - completion: @escaping (DownloadDataTask?) -> Void - ) { - context.performAndWait { - let data = try? fetchCDDownloadData( - predicate: .id(downloadDataId(from: blockId)) + public func downloadDataTask(for blockId: String) -> DownloadDataTask? { + let dataId = downloadDataId(from: blockId) + let userId = getUserId32() + return context.performAndWait {[context] in + let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: .id(dataId), + context: context, + userId: userId ) guard let downloadData = data?.first else { - completion(nil) - return + return nil } - let downloadDataTask = DownloadDataTask(sourse: downloadData) - - completion(downloadDataTask) + return DownloadDataTask(sourse: downloadData) } } - public func downloadDataTask(for blockId: String) -> DownloadDataTask? { - let data = try? fetchCDDownloadData( - predicate: .id(downloadDataId(from: blockId)) - ) - - guard let downloadData = data?.first else { return nil } - - return DownloadDataTask(sourse: downloadData) - } - - public func nextBlockForDownloading() -> DownloadDataTask? { - let data = try? fetchCDDownloadData( - predicate: .state(DownloadState.finished.rawValue), - fetchLimit: 1 - ) - - guard let downloadData = data?.first else { - return nil + public func nextBlockForDownloading() async -> DownloadDataTask? { + let userId = getUserId32() + return await context.perform {[context] in + let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: .state(DownloadState.finished.rawValue), + fetchLimit: 1, + context: context, + userId: userId + ) + + guard let downloadData = data?.first else { + return nil + } + + return DownloadDataTask(sourse: downloadData) } - - return DownloadDataTask(sourse: downloadData) } public func updateDownloadState( @@ -173,9 +195,13 @@ public class CorePersistence: CorePersistenceProtocol { state: DownloadState, resumeData: Data? ) { - context.performAndWait { - guard let data = try? fetchCDDownloadData( - predicate: .id(downloadDataId(from: id)) + let dataId = downloadDataId(from: id) + let userId = getUserId32() + context.perform {[context] in + guard let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: .id(dataId), + context: context, + userId: userId ) else { return } @@ -194,11 +220,15 @@ public class CorePersistence: CorePersistenceProtocol { } } - public func deleteDownloadDataTask(id: String) throws { - context.performAndWait { + public func deleteDownloadDataTask(id: String) async throws { + let dataId = downloadDataId(from: id) + let userId = getUserId32() + return await context.perform {[context] in do { - let records = try fetchCDDownloadData( - predicate: .id(downloadDataId(from: id)) + let records = try CorePersistenceHelper.fetchCDDownloadData( + predicate: .id(dataId), + context: context, + userId: userId ) for record in records { @@ -214,7 +244,7 @@ public class CorePersistence: CorePersistenceProtocol { } public func saveDownloadDataTask(_ task: DownloadDataTask) { - context.performAndWait { + context.perform {[context] in let newDownloadData = CDDownloadData(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump newDownloadData.id = task.id @@ -263,27 +293,6 @@ public class CorePersistence: CorePersistenceProtocol { // MARK: - Private Intents - private func fetchCDDownloadData( - predicate: CDPredicate? = nil, - fetchLimit: Int? = nil - ) throws -> [CDDownloadData] { - let request = CDDownloadData.fetchRequest() - if let predicate = predicate { - request.predicate = predicate.predicate - } - if let fetchLimit = fetchLimit { - request.fetchLimit = fetchLimit - } - let data = try context.fetch(request).filter { - guard let userId = getUserId32() else { - return true - } - debugLog(userId, "-userId-") - return $0.userId == userId - } - return data - } - private func getUserId32() -> Int32? { guard let userId else { return nil diff --git a/OpenEdX/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift index 94ded4e9b..8ba7bc45c 100644 --- a/OpenEdX/Data/CoursePersistence.swift +++ b/OpenEdX/Data/CoursePersistence.swift @@ -18,32 +18,35 @@ public class CoursePersistence: CoursePersistenceProtocol { self.context = context } - public func loadEnrollments() throws -> [CourseItem] { - let result = try? context.fetch(CDCourseItem.fetchRequest()) - .map { - CourseItem(name: $0.name ?? "", - org: $0.org ?? "", - shortDescription: $0.desc ?? "", - imageURL: $0.imageURL ?? "", - hasAccess: $0.hasAccess, - courseStart: $0.courseStart, - courseEnd: $0.courseEnd, - enrollmentStart: $0.enrollmentStart, - enrollmentEnd: $0.enrollmentEnd, - courseID: $0.courseID ?? "", - numPages: Int($0.numPages), - coursesCount: Int($0.courseCount), - progressEarned: 0, - progressPossible: 0)} - if let result, !result.isEmpty { - return result - } else { - throw NoCachedDataError() + public func loadEnrollments() async throws -> [CourseItem] { + try await context.perform { [context] in + let result = try? context.fetch(CDCourseItem.fetchRequest()) + .map { + CourseItem(name: $0.name ?? "", + org: $0.org ?? "", + shortDescription: $0.desc ?? "", + imageURL: $0.imageURL ?? "", + hasAccess: $0.hasAccess, + courseStart: $0.courseStart, + courseEnd: $0.courseEnd, + enrollmentStart: $0.enrollmentStart, + enrollmentEnd: $0.enrollmentEnd, + courseID: $0.courseID ?? "", + numPages: Int($0.numPages), + coursesCount: Int($0.courseCount), + progressEarned: 0, + progressPossible: 0) + } + if let result, !result.isEmpty { + return result + } else { + throw NoCachedDataError() + } } } public func saveEnrollments(items: [CourseItem]) { - context.performAndWait { + context.perform {[context] in for item in items { let newItem = CDCourseItem(context: context) newItem.name = item.name @@ -68,94 +71,97 @@ public class CoursePersistence: CoursePersistenceProtocol { } } - public func loadCourseStructure(courseID: String) throws -> DataLayer.CourseStructure { - let request = CDCourseStructure.fetchRequest() - request.predicate = NSPredicate(format: "id = %@", courseID) - guard let structure = try? context.fetch(request).first else { throw NoCachedDataError() } - - let requestBlocks = CDCourseBlock.fetchRequest() - requestBlocks.predicate = NSPredicate(format: "courseID = %@", courseID) - - let blocks = try? context.fetch(requestBlocks).map { - let userViewData = DataLayer.CourseDetailUserViewData( - transcripts: $0.transcripts?.jsonStringToDictionary() as? [String: String], - encodedVideo: DataLayer.CourseDetailEncodedVideoData( - youTube: DataLayer.EncodedVideoData( - url: $0.youTube?.url, - fileSize: Int($0.youTube?.fileSize ?? 0) - ), - fallback: DataLayer.EncodedVideoData( - url: $0.fallback?.url, - fileSize: Int($0.fallback?.fileSize ?? 0) - ), - desktopMP4: DataLayer.EncodedVideoData( - url: $0.desktopMP4?.url, - fileSize: Int($0.desktopMP4?.fileSize ?? 0) - ), - mobileHigh: DataLayer.EncodedVideoData( - url: $0.mobileHigh?.url, - fileSize: Int($0.mobileHigh?.fileSize ?? 0) - ), - mobileLow: DataLayer.EncodedVideoData( - url: $0.mobileLow?.url, - fileSize: Int($0.mobileLow?.fileSize ?? 0) + public func loadCourseStructure(courseID: String) async throws -> DataLayer.CourseStructure { + try await context.perform {[context] in + let request = CDCourseStructure.fetchRequest() + request.predicate = NSPredicate(format: "id = %@", courseID) + guard let structure = try? context.fetch(request).first else { throw NoCachedDataError() } + + let requestBlocks = CDCourseBlock.fetchRequest() + requestBlocks.predicate = NSPredicate(format: "courseID = %@", courseID) + + let blocks = try? context.fetch(requestBlocks).map { + let userViewData = DataLayer.CourseDetailUserViewData( + transcripts: $0.transcripts?.jsonStringToDictionary() as? [String: String], + encodedVideo: DataLayer.CourseDetailEncodedVideoData( + youTube: DataLayer.EncodedVideoData( + url: $0.youTube?.url, + fileSize: Int($0.youTube?.fileSize ?? 0) + ), + fallback: DataLayer.EncodedVideoData( + url: $0.fallback?.url, + fileSize: Int($0.fallback?.fileSize ?? 0) + ), + desktopMP4: DataLayer.EncodedVideoData( + url: $0.desktopMP4?.url, + fileSize: Int($0.desktopMP4?.fileSize ?? 0) + ), + mobileHigh: DataLayer.EncodedVideoData( + url: $0.mobileHigh?.url, + fileSize: Int($0.mobileHigh?.fileSize ?? 0) + ), + mobileLow: DataLayer.EncodedVideoData( + url: $0.mobileLow?.url, + fileSize: Int($0.mobileLow?.fileSize ?? 0) + ), + hls: DataLayer.EncodedVideoData( + url: $0.hls?.url, + fileSize: Int($0.hls?.fileSize ?? 0) + ) ), - hls: DataLayer.EncodedVideoData( - url: $0.hls?.url, - fileSize: Int($0.hls?.fileSize ?? 0) + topicID: "" + ) + return DataLayer.CourseBlock( + blockId: $0.blockId ?? "", + id: $0.id ?? "", + graded: $0.graded, + due: $0.due, + completion: $0.completion, + studentUrl: $0.studentUrl ?? "", + webUrl: $0.webUrl ?? "", + type: $0.type ?? "", + displayName: $0.displayName ?? "", + descendants: $0.descendants, + allSources: $0.allSources, + userViewData: userViewData, + multiDevice: $0.multiDevice, + assignmentProgress: DataLayer.AssignmentProgress( + assignmentType: $0.assignmentType, + numPointsEarned: $0.numPointsEarned, + numPointsPossible: $0.numPointsPossible + ) + ) + } + + let dictionary = blocks?.reduce(into: [:]) { result, block in + result[block.id] = block + } ?? [:] + + return DataLayer.CourseStructure( + rootItem: structure.rootItem ?? "", + dict: dictionary, + id: structure.id ?? "", + media: DataLayer.CourseMedia( + image: DataLayer.Image( + raw: structure.mediaRaw ?? "", + small: structure.mediaSmall ?? "", + large: structure.mediaLarge ?? "" ) ), - topicID: "" - ) - return DataLayer.CourseBlock( - blockId: $0.blockId ?? "", - id: $0.id ?? "", - graded: $0.graded, - due: $0.due, - completion: $0.completion, - studentUrl: $0.studentUrl ?? "", - webUrl: $0.webUrl ?? "", - type: $0.type ?? "", - displayName: $0.displayName ?? "", - descendants: $0.descendants, - allSources: $0.allSources, - userViewData: userViewData, - multiDevice: $0.multiDevice, - assignmentProgress: DataLayer.AssignmentProgress( - assignmentType: $0.assignmentType, - numPointsEarned: $0.numPointsEarned, - numPointsPossible: $0.numPointsPossible + certificate: DataLayer.Certificate(url: structure.certificate), + org: structure.org ?? "", + isSelfPaced: structure.isSelfPaced, + courseProgress: DataLayer.CourseProgress( + assignmentsCompleted: Int(structure.assignmentsCompleted), + totalAssignmentsCount: Int(structure.totalAssignmentsCount) ) ) } - let dictionary = blocks?.reduce(into: [:]) { result, block in - result[block.id] = block - } ?? [:] - - return DataLayer.CourseStructure( - rootItem: structure.rootItem ?? "", - dict: dictionary, - id: structure.id ?? "", - media: DataLayer.CourseMedia( - image: DataLayer.Image( - raw: structure.mediaRaw ?? "", - small: structure.mediaSmall ?? "", - large: structure.mediaLarge ?? "" - ) - ), - certificate: DataLayer.Certificate(url: structure.certificate), - org: structure.org ?? "", - isSelfPaced: structure.isSelfPaced, - courseProgress: DataLayer.CourseProgress( - assignmentsCompleted: Int(structure.assignmentsCompleted), - totalAssignmentsCount: Int(structure.totalAssignmentsCount) - ) - ) } public func saveCourseStructure(structure: DataLayer.CourseStructure) { - context.performAndWait { + context.perform {[context] in context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let newStructure = CDCourseStructure(context: self.context) newStructure.certificate = structure.certificate?.url @@ -256,7 +262,7 @@ public class CoursePersistence: CoursePersistenceProtocol { } public func saveSubtitles(url: String, subtitlesString: String) { - context.performAndWait { + context.perform {[context] in let newSubtitle = CDSubtitle(context: context) newSubtitle.url = url newSubtitle.subtitle = subtitlesString @@ -270,16 +276,18 @@ public class CoursePersistence: CoursePersistenceProtocol { } } - public func loadSubtitles(url: String) -> String? { - let request = CDSubtitle.fetchRequest() - request.predicate = NSPredicate(format: "url = %@", url) - - guard let subtitle = try? context.fetch(request).first, - let loaded = subtitle.uploadedAt else { return nil } - if Date().timeIntervalSince1970 - loaded.timeIntervalSince1970 < 5 * 3600 { - return subtitle.subtitle ?? "" + public func loadSubtitles(url: String) async -> String? { + await context.perform {[context] in + let request = CDSubtitle.fetchRequest() + request.predicate = NSPredicate(format: "url = %@", url) + + guard let subtitle = try? context.fetch(request).first, + let loaded = subtitle.uploadedAt else { return nil } + if Date().timeIntervalSince1970 - loaded.timeIntervalSince1970 < 5 * 3600 { + return subtitle.subtitle ?? "" + } + return nil } - return nil } public func saveCourseDates(courseID: String, courseDates: CourseDates) { diff --git a/OpenEdX/Data/DashboardPersistence.swift b/OpenEdX/Data/DashboardPersistence.swift index b7e0f062a..2ac59a583 100644 --- a/OpenEdX/Data/DashboardPersistence.swift +++ b/OpenEdX/Data/DashboardPersistence.swift @@ -18,32 +18,34 @@ public class DashboardPersistence: DashboardPersistenceProtocol { self.context = context } - public func loadEnrollments() throws -> [CourseItem] { - let result = try? context.fetch(CDDashboardCourse.fetchRequest()) - .map { CourseItem(name: $0.name ?? "", - org: $0.org ?? "", - shortDescription: $0.desc ?? "", - imageURL: $0.imageURL ?? "", - hasAccess: $0.hasAccess, - courseStart: $0.courseStart, - courseEnd: $0.courseEnd, - enrollmentStart: $0.enrollmentStart, - enrollmentEnd: $0.enrollmentEnd, - courseID: $0.courseID ?? "", - numPages: Int($0.numPages), - coursesCount: Int($0.courseCount), - progressEarned: 0, - progressPossible: 0)} - if let result, !result.isEmpty { - return result - } else { - throw NoCachedDataError() + public func loadEnrollments() async throws -> [CourseItem] { + try await context.perform {[context] in + let result = try? context.fetch(CDDashboardCourse.fetchRequest()) + .map { CourseItem(name: $0.name ?? "", + org: $0.org ?? "", + shortDescription: $0.desc ?? "", + imageURL: $0.imageURL ?? "", + hasAccess: $0.hasAccess, + courseStart: $0.courseStart, + courseEnd: $0.courseEnd, + enrollmentStart: $0.enrollmentStart, + enrollmentEnd: $0.enrollmentEnd, + courseID: $0.courseID ?? "", + numPages: Int($0.numPages), + coursesCount: Int($0.courseCount), + progressEarned: 0, + progressPossible: 0)} + if let result, !result.isEmpty { + return result + } else { + throw NoCachedDataError() + } } } public func saveEnrollments(items: [CourseItem]) { for item in items { - context.performAndWait { + context.perform {[context] in let newItem = CDDashboardCourse(context: self.context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump newItem.name = item.name @@ -67,89 +69,90 @@ public class DashboardPersistence: DashboardPersistenceProtocol { } } - public func loadPrimaryEnrollment() throws -> PrimaryEnrollment { - let request = CDMyEnrollments.fetchRequest() - if let result = try context.fetch(request).first { - let primaryCourse = result.primaryCourse.flatMap { cdPrimaryCourse -> PrimaryCourse? in + public func loadPrimaryEnrollment() async throws -> PrimaryEnrollment { + try await context.perform {[context] in + let request = CDMyEnrollments.fetchRequest() + if let result = try context.fetch(request).first { + let primaryCourse = result.primaryCourse.flatMap { cdPrimaryCourse -> PrimaryCourse? in - let futureAssignments = (cdPrimaryCourse.futureAssignments as? Set ?? []) - .map { future in - return Assignment( - type: future.type ?? "", - title: future.title ?? "", - description: future.descript ?? "", - date: future.date ?? Date(), - complete: future.complete, - firstComponentBlockId: future.firstComponentBlockId - ) - } - - let pastAssignments = (cdPrimaryCourse.pastAssignments as? Set ?? []) - .map { past in - return Assignment( - type: past.type ?? "", - title: past.title ?? "", - description: past.descript ?? "", - date: past.date ?? Date(), - complete: past.complete, - firstComponentBlockId: past.firstComponentBlockId + let futureAssignments = (cdPrimaryCourse.futureAssignments as? Set ?? []) + .map { future in + return Assignment( + type: future.type ?? "", + title: future.title ?? "", + description: future.descript ?? "", + date: future.date ?? Date(), + complete: future.complete, + firstComponentBlockId: future.firstComponentBlockId + ) + } + + let pastAssignments = (cdPrimaryCourse.pastAssignments as? Set ?? []) + .map { past in + return Assignment( + type: past.type ?? "", + title: past.title ?? "", + description: past.descript ?? "", + date: past.date ?? Date(), + complete: past.complete, + firstComponentBlockId: past.firstComponentBlockId + ) + } + + return PrimaryCourse( + name: cdPrimaryCourse.name ?? "", + org: cdPrimaryCourse.org ?? "", + courseID: cdPrimaryCourse.courseID ?? "", + hasAccess: cdPrimaryCourse.hasAccess, + courseStart: cdPrimaryCourse.courseStart, + courseEnd: cdPrimaryCourse.courseEnd, + courseBanner: cdPrimaryCourse.courseBanner ?? "", + futureAssignments: futureAssignments, + pastAssignments: pastAssignments, + progressEarned: Int(cdPrimaryCourse.progressEarned), + progressPossible: Int(cdPrimaryCourse.progressPossible), + lastVisitedBlockID: cdPrimaryCourse.lastVisitedBlockID ?? "", + resumeTitle: cdPrimaryCourse.resumeTitle + ) + } + + let courses = (result.courses as? Set ?? []) + .map { cdCourse in + return CourseItem( + name: cdCourse.name ?? "", + org: cdCourse.org ?? "", + shortDescription: cdCourse.desc ?? "", + imageURL: cdCourse.imageURL ?? "", + hasAccess: cdCourse.hasAccess, + courseStart: cdCourse.courseStart, + courseEnd: cdCourse.courseEnd, + enrollmentStart: cdCourse.enrollmentStart, + enrollmentEnd: cdCourse.enrollmentEnd, + courseID: cdCourse.courseID ?? "", + numPages: Int(cdCourse.numPages), + coursesCount: Int(cdCourse.courseCount), + progressEarned: Int(cdCourse.progressEarned), + progressPossible: Int(cdCourse.progressPossible) ) } - return PrimaryCourse( - name: cdPrimaryCourse.name ?? "", - org: cdPrimaryCourse.org ?? "", - courseID: cdPrimaryCourse.courseID ?? "", - hasAccess: cdPrimaryCourse.hasAccess, - courseStart: cdPrimaryCourse.courseStart, - courseEnd: cdPrimaryCourse.courseEnd, - courseBanner: cdPrimaryCourse.courseBanner ?? "", - futureAssignments: futureAssignments, - pastAssignments: pastAssignments, - progressEarned: Int(cdPrimaryCourse.progressEarned), - progressPossible: Int(cdPrimaryCourse.progressPossible), - lastVisitedBlockID: cdPrimaryCourse.lastVisitedBlockID ?? "", - resumeTitle: cdPrimaryCourse.resumeTitle + return PrimaryEnrollment( + primaryCourse: primaryCourse, + courses: courses, + totalPages: Int(result.totalPages), + count: Int(result.count) ) + } else { + throw NoCachedDataError() } - - let courses = (result.courses as? Set ?? []) - .map { cdCourse in - return CourseItem( - name: cdCourse.name ?? "", - org: cdCourse.org ?? "", - shortDescription: cdCourse.desc ?? "", - imageURL: cdCourse.imageURL ?? "", - hasAccess: cdCourse.hasAccess, - courseStart: cdCourse.courseStart, - courseEnd: cdCourse.courseEnd, - enrollmentStart: cdCourse.enrollmentStart, - enrollmentEnd: cdCourse.enrollmentEnd, - courseID: cdCourse.courseID ?? "", - numPages: Int(cdCourse.numPages), - coursesCount: Int(cdCourse.courseCount), - progressEarned: Int(cdCourse.progressEarned), - progressPossible: Int(cdCourse.progressPossible) - ) - } - - return PrimaryEnrollment( - primaryCourse: primaryCourse, - courses: courses, - totalPages: Int(result.totalPages), - count: Int(result.count) - ) - } else { - throw NoCachedDataError() } } // swiftlint:disable function_body_length public func savePrimaryEnrollment(enrollments: PrimaryEnrollment) { - context.performAndWait { - // Deleting all old data before saving new ones - clearOldEnrollmentsData() - + // Deleting all old data before saving new ones + clearOldEnrollmentsData() + context.perform {[context] in let newEnrollment = CDMyEnrollments(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump @@ -228,21 +231,23 @@ public class DashboardPersistence: DashboardPersistenceProtocol { // swiftlint:enable function_body_length func clearOldEnrollmentsData() { - let fetchRequest1: NSFetchRequest = CDDashboardCourse.fetchRequest() - let batchDeleteRequest1 = NSBatchDeleteRequest(fetchRequest: fetchRequest1) + context.perform {[context] in + let fetchRequest1: NSFetchRequest = CDDashboardCourse.fetchRequest() + let batchDeleteRequest1 = NSBatchDeleteRequest(fetchRequest: fetchRequest1) - let fetchRequest2: NSFetchRequest = CDPrimaryCourse.fetchRequest() - let batchDeleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) + let fetchRequest2: NSFetchRequest = CDPrimaryCourse.fetchRequest() + let batchDeleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) - let fetchRequest3: NSFetchRequest = CDMyEnrollments.fetchRequest() - let batchDeleteRequest3 = NSBatchDeleteRequest(fetchRequest: fetchRequest3) + let fetchRequest3: NSFetchRequest = CDMyEnrollments.fetchRequest() + let batchDeleteRequest3 = NSBatchDeleteRequest(fetchRequest: fetchRequest3) - do { - try context.execute(batchDeleteRequest1) - try context.execute(batchDeleteRequest2) - try context.execute(batchDeleteRequest3) - } catch { - print("Error when deleting old data:", error) + do { + try context.execute(batchDeleteRequest1) + try context.execute(batchDeleteRequest2) + try context.execute(batchDeleteRequest3) + } catch { + print("Error when deleting old data:", error) + } } } } diff --git a/OpenEdX/Data/DiscoveryPersistence.swift b/OpenEdX/Data/DiscoveryPersistence.swift index 2e6d443bd..b36f58fad 100644 --- a/OpenEdX/Data/DiscoveryPersistence.swift +++ b/OpenEdX/Data/DiscoveryPersistence.swift @@ -18,32 +18,34 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { self.context = context } - public func loadDiscovery() throws -> [CourseItem] { - let result = try? context.fetch(CDDiscoveryCourse.fetchRequest()) - .map { CourseItem(name: $0.name ?? "", - org: $0.org ?? "", - shortDescription: $0.desc ?? "", - imageURL: $0.imageURL ?? "", - hasAccess: $0.hasAccess, - courseStart: $0.courseStart, - courseEnd: $0.courseEnd, - enrollmentStart: $0.enrollmentStart, - enrollmentEnd: $0.enrollmentEnd, - courseID: $0.courseID ?? "", - numPages: Int($0.numPages), - coursesCount: Int($0.courseCount), - progressEarned: 0, - progressPossible: 0)} - if let result, !result.isEmpty { - return result - } else { - throw NoCachedDataError() + public func loadDiscovery() async throws -> [CourseItem] { + try await context.perform {[context] in + let result = try? context.fetch(CDDiscoveryCourse.fetchRequest()) + .map { CourseItem(name: $0.name ?? "", + org: $0.org ?? "", + shortDescription: $0.desc ?? "", + imageURL: $0.imageURL ?? "", + hasAccess: $0.hasAccess, + courseStart: $0.courseStart, + courseEnd: $0.courseEnd, + enrollmentStart: $0.enrollmentStart, + enrollmentEnd: $0.enrollmentEnd, + courseID: $0.courseID ?? "", + numPages: Int($0.numPages), + coursesCount: Int($0.courseCount), + progressEarned: 0, + progressPossible: 0)} + if let result, !result.isEmpty { + return result + } else { + throw NoCachedDataError() + } } } public func saveDiscovery(items: [CourseItem]) { for item in items { - context.performAndWait { + context.perform {[context] in let newItem = CDDiscoveryCourse(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump newItem.name = item.name @@ -67,28 +69,30 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { } } - public func loadCourseDetails(courseID: String) throws -> CourseDetails { - let request = CDCourseDetails.fetchRequest() - request.predicate = NSPredicate(format: "courseID = %@", courseID) - guard let courseDetails = try? context.fetch(request).first else { throw NoCachedDataError() } - return CourseDetails( - courseID: courseDetails.courseID ?? "", - org: courseDetails.org ?? "", - courseTitle: courseDetails.courseTitle ?? "", - courseDescription: courseDetails.courseDescription ?? "", - courseStart: courseDetails.courseStart, - courseEnd: courseDetails.courseEnd, - enrollmentStart: courseDetails.enrollmentStart, - enrollmentEnd: courseDetails.enrollmentEnd, - isEnrolled: courseDetails.isEnrolled, - overviewHTML: courseDetails.overviewHTML ?? "", - courseBannerURL: courseDetails.courseBannerURL ?? "", - courseVideoURL: nil - ) + public func loadCourseDetails(courseID: String) async throws -> CourseDetails { + try await context.perform {[context] in + let request = CDCourseDetails.fetchRequest() + request.predicate = NSPredicate(format: "courseID = %@", courseID) + guard let courseDetails = try? context.fetch(request).first else { throw NoCachedDataError() } + return CourseDetails( + courseID: courseDetails.courseID ?? "", + org: courseDetails.org ?? "", + courseTitle: courseDetails.courseTitle ?? "", + courseDescription: courseDetails.courseDescription ?? "", + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, + isEnrolled: courseDetails.isEnrolled, + overviewHTML: courseDetails.overviewHTML ?? "", + courseBannerURL: courseDetails.courseBannerURL ?? "", + courseVideoURL: nil + ) + } } public func saveCourseDetails(course: CourseDetails) { - context.performAndWait { + context.perform {[context] in let newCourseDetails = CDCourseDetails(context: self.context) newCourseDetails.courseID = course.courseID newCourseDetails.org = course.org From 5f4268c556d12aa6478b7c023eb2a1c1276b782a Mon Sep 17 00:00:00 2001 From: Saeed Bashir Date: Tue, 9 Jul 2024 12:48:39 +0500 Subject: [PATCH 8/8] chore: theme enhancements (#476) * chore: theme enhancement * chore: theme enhancements * chore: theme improvements * chore: address design team feedback * chore: adding new color for coursecardbackground * fix: using correct colors for openedX * fix: using irreversibleAlert in registration to highlight errors * fix: comment added pop-up message text is not visible in dark mode * refactor: fix typo --- Core/Core/View/Base/AlertView.swift | 19 +++------- Core/Core/View/Base/CourseCellView.swift | 2 +- .../View/Base/LogistrationBottomView.swift | 2 +- Core/Core/View/Base/PickerView.swift | 4 +- .../View/Base/RegistrationTextField.swift | 8 ++-- Core/Core/View/Base/UnitButtonView.swift | 29 +++++++------- .../Presentation/Handouts/HandoutsView.swift | 3 ++ .../Subviews/CustomDisclosureGroup.swift | 2 +- .../Elements/CourseCardView.swift | 2 +- .../Presentation/Elements/DropDownMenu.swift | 3 +- .../Elements/PrimaryCardView.swift | 6 +-- .../Elements/ProgressLineView.swift | 2 +- .../Comments/Base/CommentCell.swift | 6 +-- .../Comments/Base/ParentCommentView.swift | 6 +-- .../Comments/Thread/ThreadView.swift | 2 +- .../Elements/NewCalendarView.swift | 15 ++++---- .../Presentation/Profile/ProfileView.swift | 2 +- .../Settings/ManageAccountView.swift | 2 +- .../Contents.json | 38 +++++++++++++++++++ .../Colors/SecondaryButton/Contents.json | 6 +++ .../Contents.json | 38 +++++++++++++++++++ .../Contents.json | 0 .../Contents.json | 0 Theme/Theme/SwiftGen/ThemeAssets.swift | 2 + Theme/Theme/Theme.swift | 2 + .../Elements/WhatsNewNavigationButton.swift | 2 +- 26 files changed, 143 insertions(+), 60 deletions(-) create mode 100644 Theme/Theme/Assets.xcassets/Colors/CourseCardBackground.colorset/Contents.json create mode 100644 Theme/Theme/Assets.xcassets/Colors/SecondaryButton/Contents.json create mode 100644 Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBGColor.colorset/Contents.json rename Theme/Theme/Assets.xcassets/Colors/{ => SecondaryButton}/SecondaryButtonBorderColor.colorset/Contents.json (100%) rename Theme/Theme/Assets.xcassets/Colors/{ => SecondaryButton}/SecondaryButtonTextColor.colorset/Contents.json (100%) diff --git a/Core/Core/View/Base/AlertView.swift b/Core/Core/View/Base/AlertView.swift index 388c70c54..286fdd2e7 100644 --- a/Core/Core/View/Base/AlertView.swift +++ b/Core/Core/View/Base/AlertView.swift @@ -109,15 +109,8 @@ public struct AlertView: View { .fixedSize(horizontal: false, vertical: false) ) .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke( - style: .init( - lineWidth: 1, - lineCap: .round, - lineJoin: .round, - miterLimit: 1 - ) - ) + Theme.Shapes.buttonShape + .stroke(lineWidth: 1) .foregroundColor(Theme.Colors.backgroundStroke) .fixedSize(horizontal: false, vertical: false) ) @@ -256,7 +249,7 @@ public struct AlertView: View { .frame(maxWidth: 215) } UnitButtonView(type: .custom(action), - bgColor: .clear, + bgColor: Theme.Colors.secondaryButtonBGColor, action: { okTapped() }) .frame(maxWidth: 215) @@ -417,7 +410,7 @@ public struct AlertView: View { } label: { ZStack { Text(primaryButtonTitle) - .foregroundColor(Theme.Colors.primaryButtonTextColor) + .foregroundColor(Theme.Colors.styledButtonText) .font(Theme.Fonts.labelLarge) .frame(maxWidth: .infinity) .padding(.horizontal, 16) @@ -426,7 +419,7 @@ public struct AlertView: View { } .background( Theme.Shapes.buttonShape - .fill(Theme.Colors.accentColor) + .fill(Theme.Colors.accentButtonColor) ) .overlay( RoundedRectangle(cornerRadius: 8) @@ -454,7 +447,7 @@ public struct AlertView: View { }) .background( Theme.Shapes.buttonShape - .fill(.clear) + .fill(Theme.Colors.secondaryButtonBGColor) ) .overlay( RoundedRectangle(cornerRadius: 8) diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index 72f02b2bb..a996c3383 100644 --- a/Core/Core/View/Base/CourseCellView.swift +++ b/Core/Core/View/Base/CourseCellView.swift @@ -116,7 +116,7 @@ public struct CourseCellView: View { .overlay(Theme.Colors.cardViewStroke) .padding(.vertical, 18) .padding(.horizontal, 3) - .accessibilityIdentifier("devider") + .accessibilityIdentifier("divider") } } } diff --git a/Core/Core/View/Base/LogistrationBottomView.swift b/Core/Core/View/Base/LogistrationBottomView.swift index fca95cc04..fc0aa0ef4 100644 --- a/Core/Core/View/Base/LogistrationBottomView.swift +++ b/Core/Core/View/Base/LogistrationBottomView.swift @@ -48,7 +48,7 @@ public struct LogistrationBottomView: View { action: { action(.signIn) }, - color: Theme.Colors.background, + color: Theme.Colors.secondaryButtonBGColor, textColor: Theme.Colors.secondaryButtonTextColor, borderColor: Theme.Colors.secondaryButtonBorderColor ) diff --git a/Core/Core/View/Base/PickerView.swift b/Core/Core/View/Base/PickerView.swift index d0655ddb4..e0403b822 100644 --- a/Core/Core/View/Base/PickerView.swift +++ b/Core/Core/View/Base/PickerView.swift @@ -64,7 +64,7 @@ public struct PickerView: View { .stroke(lineWidth: 1) .fill(config.error == "" ? Theme.Colors.textInputStroke - : Theme.Colors.alert) + : Theme.Colors.irreversibleAlert) ) .shake($config.shake) Text(config.error == "" ? config.field.instructions @@ -72,7 +72,7 @@ public struct PickerView: View { .font(Theme.Fonts.labelMedium) .foregroundColor(config.error == "" ? Theme.Colors.textPrimary - : Theme.Colors.alert) + : Theme.Colors.irreversibleAlert) .accessibilityIdentifier("\(config.field.name)_instructions_text") } } diff --git a/Core/Core/View/Base/RegistrationTextField.swift b/Core/Core/View/Base/RegistrationTextField.swift index 9ed039763..45a8345ce 100644 --- a/Core/Core/View/Base/RegistrationTextField.swift +++ b/Core/Core/View/Base/RegistrationTextField.swift @@ -60,7 +60,7 @@ public struct RegistrationTextField: View { .fill( config.error == "" ? Theme.Colors.textInputStroke - : Theme.Colors.alert + : Theme.Colors.irreversibleAlert ) ) .shake($config.shake) @@ -83,7 +83,7 @@ public struct RegistrationTextField: View { .fill( config.error == "" ? Theme.Colors.textInputStroke - : Theme.Colors.alert + : Theme.Colors.irreversibleAlert ) ) .shake($config.shake) @@ -107,7 +107,7 @@ public struct RegistrationTextField: View { .fill( config.error == "" ? Theme.Colors.textInputStroke - : Theme.Colors.alert + : Theme.Colors.irreversibleAlert ) ) .shake($config.shake) @@ -119,7 +119,7 @@ public struct RegistrationTextField: View { .font(Theme.Fonts.bodySmall) .foregroundColor(config.error == "" ? Theme.Colors.textSecondaryLight - : Theme.Colors.alert) + : Theme.Colors.irreversibleAlert) .accessibilityIdentifier("\(config.field.name)_instructions_text") } } diff --git a/Core/Core/View/Base/UnitButtonView.swift b/Core/Core/View/Base/UnitButtonView.swift index d347cde97..6778bb8dd 100644 --- a/Core/Core/View/Base/UnitButtonView.swift +++ b/Core/Core/View/Base/UnitButtonView.swift @@ -75,23 +75,23 @@ public struct UnitButtonView: View { case .first: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .font(Theme.Fonts.labelLarge) CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .rotationEffect(Angle.degrees(nextButtonDegrees)) }.padding(.horizontal, 16) case .next, .nextBig: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .padding(.leading, 20) .font(Theme.Fonts.labelLarge) if type != .nextBig { Spacer() } CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .rotationEffect(Angle.degrees(nextButtonDegrees)) .padding(.trailing, 20) } @@ -99,19 +99,19 @@ public struct UnitButtonView: View { HStack { if isVerticalNavigation { Text(type.stringValue()) - .foregroundColor(Theme.Colors.secondaryButtonTextColor) + .foregroundColor(Theme.Colors.accentColor) .font(Theme.Fonts.labelLarge) .padding(.leading, 20) CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) .rotationEffect(Angle.degrees(90)) .padding(.trailing, 20) - .foregroundColor(Theme.Colors.secondaryButtonTextColor) + .foregroundColor(Theme.Colors.accentColor) } else { CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) .padding(.leading, 20) - .foregroundColor(Theme.Colors.secondaryButtonTextColor) + .foregroundColor(Theme.Colors.accentColor) Text(type.stringValue()) - .foregroundColor(Theme.Colors.secondaryButtonTextColor) + .foregroundColor(Theme.Colors.accentColor) .font(Theme.Fonts.labelLarge) .padding(.trailing, 20) } @@ -119,22 +119,22 @@ public struct UnitButtonView: View { case .last: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .padding(.leading, 8) .font(Theme.Fonts.labelLarge) .scaledToFit() Spacer() CoreAssets.check.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .padding(.trailing, 8) } case .finish: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .font(Theme.Fonts.labelLarge) CoreAssets.check.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) }.padding(.horizontal, 16) case .reload, .custom: VStack(alignment: .center) { @@ -163,7 +163,7 @@ public struct UnitButtonView: View { Theme.Shapes.buttonShape .fill(type == .previous ? Theme.Colors.background - : Theme.Colors.accentButtonColor) + : Theme.Colors.accentColor) .shadow(color: Color.black.opacity(0.25), radius: 21, y: 4) .overlay( Theme.Shapes.buttonShape @@ -174,8 +174,7 @@ public struct UnitButtonView: View { miterLimit: 1) ) .foregroundColor( - type == .previous ? Theme.Colors.secondaryButtonBorderColor - : Theme.Colors.accentButtonColor + Theme.Colors.accentColor ) ) diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index c9f7460de..c9a2ed6cf 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -61,6 +61,9 @@ struct HandoutsView: View { ) }) Divider() + .frame(height: 1) + .overlay(Theme.Colors.cardViewStroke) + .accessibilityIdentifier("divider") HandoutsItemCell(type: .announcements, onTapAction: { if !viewModel.updates.isEmpty { viewModel.router.showHandoutsUpdatesView( diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index 10ab50625..8f152fdd9 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -174,7 +174,7 @@ struct CustomDisclosureGroup: View { .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 8) - .fill(Theme.Colors.tabbarColor) + .fill(Theme.Colors.datesSectionBackground) ) .overlay( RoundedRectangle(cornerRadius: 8) diff --git a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift index 53a0911db..2c93c7d33 100644 --- a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift @@ -66,7 +66,7 @@ struct CourseCardView: View { .padding(8) } } - .background(Theme.Colors.background) + .background(Theme.Colors.courseCardBackground) .cornerRadius(8) .shadow(color: Theme.Colors.courseCardShadow, radius: 6, x: 2, y: 2) } diff --git a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift index 85ba10548..beaf3bec5 100644 --- a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift +++ b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift @@ -58,7 +58,8 @@ struct DropDownMenu: View { Text(option.text) .font(Theme.Fonts.titleSmall) .foregroundColor( - option == selectedOption ? Theme.Colors.white : Theme.Colors.textPrimary + option == selectedOption ? Theme.Colors.primaryButtonTextColor : + Theme.Colors.textPrimary ) Spacer() } diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index 27e2abfb5..fc18526a9 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -73,7 +73,7 @@ public struct PrimaryCardView: View { assignments } } - .background(Theme.Colors.background) + .background(Theme.Colors.courseCardBackground) .cornerRadius(8) .shadow(color: Theme.Colors.courseCardShadow, radius: 4, x: 0, y: 3) .padding(20) @@ -206,12 +206,12 @@ public struct PrimaryCardView: View { } .padding(.top, 8) .padding(.bottom, selected ? 10 : 0) - }.background(selected ? Theme.Colors.accentColor : .clear) + }.background(selected ? Theme.Colors.accentButtonColor : .clear) }) } private func foregroundColor(_ selected: Bool) -> SwiftUI.Color { - return selected ? Theme.Colors.primaryButtonTextColor : Theme.Colors.textPrimary + return selected ? Theme.Colors.white : Theme.Colors.textPrimary } private var courseBanner: some View { diff --git a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift index 611ddbcb1..80ef325a1 100644 --- a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift @@ -30,7 +30,7 @@ struct ProgressLineView: View { Rectangle() .foregroundStyle(Theme.Colors.cardViewStroke) Rectangle() - .foregroundStyle(Theme.Colors.accentColor) + .foregroundStyle(Theme.Colors.accentButtonColor) .frame(width: geometry.size.width * progressValue) }.frame(height: height) } diff --git a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift index ff11baedd..7d8c8b155 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift @@ -118,13 +118,13 @@ public struct CommentCell: View { onLikeTap() }, label: { comment.voted - ? CoreAssets.voted.swiftUIImage - : CoreAssets.vote.swiftUIImage + ? (CoreAssets.voted.swiftUIImage.renderingMode(.template)) + : CoreAssets.vote.swiftUIImage.renderingMode(.template) Text("\(comment.votesCount)") Text(DiscussionLocalization.votesCount(comment.votesCount)) .font(Theme.Fonts.labelLarge) }).foregroundColor(comment.voted - ? Theme.Colors.accentColor + ? Theme.Colors.accentXColor : Theme.Colors.textSecondary) Spacer() diff --git a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift index 23aae3d52..3594f26c5 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift @@ -107,15 +107,15 @@ public struct ParentCommentView: View { onLikeTap() }, label: { comments.voted - ? CoreAssets.voted.swiftUIImage - : CoreAssets.vote.swiftUIImage + ? (CoreAssets.voted.swiftUIImage.renderingMode(.template)) + : (CoreAssets.vote.swiftUIImage.renderingMode(.template)) Text("\(comments.votesCount)") .foregroundColor(Theme.Colors.textPrimary) Text(DiscussionLocalization.votesCount(comments.votesCount)) .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.textPrimary) }).foregroundColor(comments.voted - ? Theme.Colors.accentColor + ? Theme.Colors.accentXColor : Theme.Colors.textSecondaryLight) Spacer() Button(action: { diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index 84f2b6816..b764ed0d6 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -210,7 +210,7 @@ public struct ThreadView: View { Text(viewModel.alertMessage ?? "") .shadowCardStyle( bgColor: Theme.Colors.accentColor, - textColor: Theme.Colors.white + textColor: Theme.Colors.primaryButtonTextColor ) .padding(.top, 80) Spacer() diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift index 64d9342a1..264914c75 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift @@ -112,13 +112,14 @@ struct NewCalendarView: View { ) VStack(spacing: 16) { - StyledButton(ProfileLocalization.Calendar.cancel, - action: { - onCloseTapped() - }, - color: Theme.Colors.background, - textColor: Theme.Colors.accentColor, - borderColor: Theme.Colors.accentColor + StyledButton( + ProfileLocalization.Calendar.cancel, + action: { + onCloseTapped() + }, + color: Theme.Colors.background, + textColor: Theme.Colors.accentColor, + borderColor: Theme.Colors.accentColor ) StyledButton(ProfileLocalization.Calendar.beginSyncing) { diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 5b2a04c77..37a23c6de 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -106,7 +106,7 @@ public struct ProfileView: View { } ) }, - color: .clear, + color: Theme.Colors.background, textColor: Theme.Colors.accentColor, borderColor: Theme.Colors.accentColor ).padding(.all, 24) diff --git a/Profile/Profile/Presentation/Settings/ManageAccountView.swift b/Profile/Profile/Presentation/Settings/ManageAccountView.swift index f4a38ab34..7f9475f76 100644 --- a/Profile/Profile/Presentation/Settings/ManageAccountView.swift +++ b/Profile/Profile/Presentation/Settings/ManageAccountView.swift @@ -182,7 +182,7 @@ public struct ManageAccountView: View { } ) }, - color: .clear, + color: Theme.Colors.background, textColor: Theme.Colors.accentColor, borderColor: Theme.Colors.accentColor ).padding(.horizontal, 24) diff --git a/Theme/Theme/Assets.xcassets/Colors/CourseCardBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CourseCardBackground.colorset/Contents.json new file mode 100644 index 000000000..8fef18d07 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/CourseCardBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.184", + "green" : "0.129", + "red" : "0.098" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBGColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBGColor.colorset/Contents.json new file mode 100644 index 000000000..8fef18d07 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBGColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.184", + "green" : "0.129", + "red" : "0.098" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButtonBorderColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBorderColor.colorset/Contents.json similarity index 100% rename from Theme/Theme/Assets.xcassets/Colors/SecondaryButtonBorderColor.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBorderColor.colorset/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButtonTextColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonTextColor.colorset/Contents.json similarity index 100% rename from Theme/Theme/Assets.xcassets/Colors/SecondaryButtonTextColor.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonTextColor.colorset/Contents.json diff --git a/Theme/Theme/SwiftGen/ThemeAssets.swift b/Theme/Theme/SwiftGen/ThemeAssets.swift index 2daf3d16b..5a9ed5fe8 100644 --- a/Theme/Theme/SwiftGen/ThemeAssets.swift +++ b/Theme/Theme/SwiftGen/ThemeAssets.swift @@ -35,6 +35,7 @@ public enum ThemeAssets { public static let cardViewStroke = ColorAsset(name: "CardViewStroke") public static let certificateForeground = ColorAsset(name: "CertificateForeground") public static let commentCellBackground = ColorAsset(name: "CommentCellBackground") + public static let courseCardBackground = ColorAsset(name: "CourseCardBackground") public static let courseCardShadow = ColorAsset(name: "CourseCardShadow") public static let datesSectionBackground = ColorAsset(name: "DatesSectionBackground") public static let datesSectionStroke = ColorAsset(name: "DatesSectionStroke") @@ -54,6 +55,7 @@ public enum ThemeAssets { public static let progressDone = ColorAsset(name: "ProgressDone") public static let progressSkip = ColorAsset(name: "ProgressSkip") public static let selectedAndDone = ColorAsset(name: "SelectedAndDone") + public static let secondaryButtonBGColor = ColorAsset(name: "SecondaryButtonBGColor") public static let secondaryButtonBorderColor = ColorAsset(name: "SecondaryButtonBorderColor") public static let secondaryButtonTextColor = ColorAsset(name: "SecondaryButtonTextColor") public static let shadowColor = ColorAsset(name: "ShadowColor") diff --git a/Theme/Theme/Theme.swift b/Theme/Theme/Theme.swift index 73a4b4939..50fa75878 100644 --- a/Theme/Theme/Theme.swift +++ b/Theme/Theme/Theme.swift @@ -57,6 +57,7 @@ public struct Theme { public private(set) static var navigationBarTintColor = ThemeAssets.navigationBarTintColor.swiftUIColor public private(set) static var secondaryButtonBorderColor = ThemeAssets.secondaryButtonBorderColor.swiftUIColor public private(set) static var secondaryButtonTextColor = ThemeAssets.secondaryButtonTextColor.swiftUIColor + public private(set) static var secondaryButtonBGColor = ThemeAssets.secondaryButtonBGColor.swiftUIColor public private(set) static var success = ThemeAssets.success.swiftUIColor public private(set) static var tabbarColor = ThemeAssets.tabbarColor.swiftUIColor public private(set) static var primaryButtonTextColor = ThemeAssets.primaryButtonTextColor.swiftUIColor @@ -71,6 +72,7 @@ public struct Theme { public private(set) static var primaryHeaderColor = ThemeAssets.primaryHeaderColor.swiftUIColor public private(set) static var secondaryHeaderColor = ThemeAssets.secondaryHeaderColor.swiftUIColor public private(set) static var courseCardShadow = ThemeAssets.courseCardShadow.swiftUIColor + public private(set) static var courseCardBackground = ThemeAssets.courseCardBackground.swiftUIColor public static func update( accentColor: Color = ThemeAssets.accentColor.swiftUIColor, diff --git a/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift b/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift index 640aa5545..d439058ba 100644 --- a/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift +++ b/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift @@ -50,7 +50,7 @@ struct WhatsNewNavigationButton: View { Theme.Shapes.buttonShape .fill( type == .previous - ? Theme.Colors.background + ? Theme.Colors.secondaryButtonBGColor : Theme.Colors.accentButtonColor ) )