From 4daec6df6305b8df185406276d2e7b49f838ef1c Mon Sep 17 00:00:00 2001 From: Vadim Kuznetsov Date: Thu, 11 Jul 2024 17:35:38 +0300 Subject: [PATCH 1/4] feat: added upgrade button to gallery --- .../View/UpgradeCourseView.swift | 16 +++++----- .../Data/Model/Data_PrimaryEnrollment.swift | 29 ++++++++++++++++++- .../Core/Domain/Model/PrimaryEnrollment.swift | 15 ++++++++-- .../Container/CourseContainerViewModel.swift | 6 ++-- .../Dashboard/Data/DashboardRepository.swift | 6 +++- .../DashboardCoreModel.xcdatamodel/contents | 6 +++- .../Elements/PrimaryCardView.swift | 23 +++++++++++++-- .../PrimaryCourseDashboardView.swift | 14 +++++++++ OpenEdX/Data/DashboardPersistence.swift | 15 ++++++++-- 9 files changed, 112 insertions(+), 18 deletions(-) diff --git a/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift b/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift index 302fc432..ea895b59 100644 --- a/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift +++ b/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift @@ -11,8 +11,8 @@ import Swinject public enum CourseAccessErrorHelperType { case isEndDateOld(date: Date) case startDateError(date: Date?) - case auditExpired(date: Date?, sku: String, courseID: String, pacing: String, screen: CourseUpgradeScreen) - case upgradeable(date: Date?, sku: String, courseID: String, pacing: String, screen: CourseUpgradeScreen) + case auditExpired(date: Date?, sku: String, courseID: String, pacing: String, screen: CourseUpgradeScreen, lmsPrice: Double) + case upgradeable(date: Date?, sku: String, courseID: String, pacing: String, screen: CourseUpgradeScreen, lmsPrice: Double) } public struct UpgradeCourseView: View { @@ -45,8 +45,8 @@ public struct UpgradeCourseView: View { public var body: some View { switch type { - case let .upgradeable(date, sku, courseID, pacing, screen), - let .auditExpired(date, sku, courseID, pacing, screen): + case let .upgradeable(date, sku, courseID, pacing, screen, lmsPrice), + let .auditExpired(date, sku, courseID, pacing, screen, lmsPrice): VStack { let message = CoreLocalization.CourseUpgrade.View.auditMessage .replacingOccurrences( @@ -57,7 +57,7 @@ public struct UpgradeCourseView: View { isFindCourseButtonVisible: true, viewModel: Container.shared.resolve( UpgradeInfoViewModel.self, - arguments: "", message, sku, courseID, screen, pacing + arguments: "", message, sku, courseID, screen, pacing, lmsPrice )!, findAction: { findAction?() @@ -126,7 +126,8 @@ public struct UpgradeCourseView: View { sku: "some sku", courseID: "courseID", pacing: "pacing", - screen: .unknown + screen: .unknown, + lmsPrice: .zero ), coordinate: .constant(0), collapsed: .constant(false), @@ -141,7 +142,8 @@ public struct UpgradeCourseView: View { sku: "some sku", courseID: "courseID", pacing: "pacing", - screen: .unknown + screen: .unknown, + lmsPrice: .zero ), coordinate: .constant(0), collapsed: .constant(false), diff --git a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift index 024f4124..6cbc2879 100644 --- a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift +++ b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift @@ -52,6 +52,29 @@ public extension DataLayer { case courseAssignments = "course_assignments" } + public var sku: String? { + let mode = courseModes?.first { $0.slug == .verified } + return mode?.iosSku + } + public var lmsPrice: Double? { + let mode = courseModes?.first { $0.slug == .verified } + return mode?.lmsPrice + } + + var isUpgradeable: Bool { + guard let start = course?.start, + let upgradeDeadline = course?.dynamicUpgradeDeadline, + mode == "audit" + else { return false } + + let startDate = Date(iso8601: start) + let dynamicUpgradeDeadline = Date(iso8601: upgradeDeadline) + + return startDate.isInPast() + && sku?.isEmpty == false + && !dynamicUpgradeDeadline.isInPast() + } + public init( auditAccessExpires: Date?, created: String?, @@ -216,7 +239,11 @@ public extension DataLayer.PrimaryEnrollment { resumeTitle: primary.courseStatus?.lastVisitedUnitDisplayName, auditAccessExpires: primary.auditAccessExpires, startDisplay: primary.course?.startDisplay.flatMap { Date(iso8601: $0) }, - startType: DisplayStartType(value: primary.course?.startType.rawValue) + startType: DisplayStartType(value: primary.course?.startType.rawValue), + isUpgradeable: primary.isUpgradeable, + sku: primary.sku, + lmsPrice: primary.lmsPrice, + isSelfPaced: primary.course?.isSelfPaced ?? false ) } diff --git a/Core/Core/Domain/Model/PrimaryEnrollment.swift b/Core/Core/Domain/Model/PrimaryEnrollment.swift index 994a073d..585a8179 100644 --- a/Core/Core/Domain/Model/PrimaryEnrollment.swift +++ b/Core/Core/Domain/Model/PrimaryEnrollment.swift @@ -49,7 +49,10 @@ public struct PrimaryCourse: Hashable { public let auditAccessExpires: Date? public let startDisplay: Date? public let startType: DisplayStartType? - + public let isUpgradeable: Bool + public let sku: String? + public let lmsPrice: Double? + public let isSelfPaced: Bool public init( name: String, org: String, @@ -66,7 +69,11 @@ public struct PrimaryCourse: Hashable { resumeTitle: String?, auditAccessExpires: Date?, startDisplay: Date?, - startType: DisplayStartType? + startType: DisplayStartType?, + isUpgradeable: Bool, + sku: String?, + lmsPrice: Double?, + isSelfPaced: Bool ) { self.name = name self.org = org @@ -84,6 +91,10 @@ public struct PrimaryCourse: Hashable { self.auditAccessExpires = auditAccessExpires self.startDisplay = startDisplay self.startType = startType + self.isUpgradeable = isUpgradeable + self.sku = sku + self.lmsPrice = lmsPrice + self.isSelfPaced = isSelfPaced } } diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 5ff97a6b..e3e4c4da 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -328,7 +328,8 @@ public class CourseContainerViewModel: BaseCourseViewModel { sku: courseStructure.sku ?? "", courseID: courseID, pacing: courseStructure.isSelfPaced ? Pacing.selfPace.rawValue : Pacing.instructor.rawValue, - screen: .courseDashboard + screen: .courseDashboard, + lmsPrice: courseStructure.lmsPrice ?? .zero ) } else { return .isEndDateOld(date: courseEnd) @@ -351,7 +352,8 @@ public class CourseContainerViewModel: BaseCourseViewModel { sku: courseStructure.sku ?? "", courseID: courseID, pacing: courseStructure.isSelfPaced ? Pacing.selfPace.rawValue : Pacing.instructor.rawValue, - screen: .courseDashboard + screen: .courseDashboard, + lmsPrice: courseStructure.lmsPrice ?? .zero ) default: diff --git a/Dashboard/Dashboard/Data/DashboardRepository.swift b/Dashboard/Dashboard/Data/DashboardRepository.swift index d3c8e730..cd6df09e 100644 --- a/Dashboard/Dashboard/Data/DashboardRepository.swift +++ b/Dashboard/Dashboard/Data/DashboardRepository.swift @@ -192,7 +192,11 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { resumeTitle: nil, auditAccessExpires: nil, startDisplay: nil, - startType: .unknown + startType: .unknown, + isUpgradeable: false, + sku: nil, + lmsPrice: nil, + isSelfPaced: false ) return PrimaryEnrollment(primaryCourse: primaryCourse, courses: courses, totalPages: 1, count: 1) } diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents index 3253fdb1..9b3e0884 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents +++ b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -67,12 +67,16 @@ + + + + diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index 8100e6bd..1d746f13 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -29,6 +29,8 @@ public struct PrimaryCardView: View { private var assignmentAction: (String?) -> Void private var openCourseAction: () -> Void private var resumeAction: () -> Void + private var upgradeAction: () -> Void + private var isUpgradeable: Bool public init( courseName: String, @@ -47,7 +49,9 @@ public struct PrimaryCardView: View { startType: DisplayStartType?, assignmentAction: @escaping (String?) -> Void, openCourseAction: @escaping () -> Void, - resumeAction: @escaping () -> Void + resumeAction: @escaping () -> Void, + isUpgradeable: Bool, + upgradeAction: @escaping () -> Void ) { self.courseName = courseName self.org = org @@ -66,6 +70,8 @@ public struct PrimaryCardView: View { self.auditAccessExpires = auditAccessExpires self.startDisplay = startDisplay self.startType = startType + self.isUpgradeable = isUpgradeable + self.upgradeAction = upgradeAction } public var body: some View { @@ -147,6 +153,17 @@ public struct PrimaryCardView: View { } } + // Upgrade button + if isUpgradeable { + courseButton( + title: CoreLocalization.CourseUpgrade.Button.upgrade, + description: nil, + icon: Image(systemName: "trophy.fill"), + selected: false, + action: upgradeAction + ) + } + // ResumeButton if canResume { courseButton( @@ -282,7 +299,9 @@ struct PrimaryCardView_Previews: PreviewProvider { startType: .unknown, assignmentAction: {_ in }, openCourseAction: {}, - resumeAction: {} + resumeAction: {}, + isUpgradeable: false, + upgradeAction: {} ) .loadFonts() } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 5a65c214..a5e61848 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -125,6 +125,20 @@ public struct PrimaryCourseDashboardView: View { showDates: false, lastVisitedBlockID: primary.lastVisitedBlockID ) + }, + isUpgradeable: primary.isUpgradeable, + upgradeAction: { + Task {@MainActor in + await self.router.showUpgradeInfo( + productName: primary.name, + message: "", + sku: primary.sku ?? "", + courseID: primary.courseID, + screen: .dashboard, + pacing: primary.isSelfPaced ? Pacing.selfPace.rawValue : Pacing.instructor.rawValue, + lmsPrice: primary.lmsPrice ?? .zero + ) + } } ) } diff --git a/OpenEdX/Data/DashboardPersistence.swift b/OpenEdX/Data/DashboardPersistence.swift index 3ccfd1ab..002b02cf 100644 --- a/OpenEdX/Data/DashboardPersistence.swift +++ b/OpenEdX/Data/DashboardPersistence.swift @@ -158,7 +158,11 @@ public class DashboardPersistence: DashboardPersistenceProtocol { resumeTitle: cdPrimaryCourse.resumeTitle, auditAccessExpires: cdPrimaryCourse.auditAccessExpires, startDisplay: cdPrimaryCourse.startDisplay, - startType: DisplayStartType(value: cdPrimaryCourse.startType) + startType: DisplayStartType(value: cdPrimaryCourse.startType), + isUpgradeable: cdPrimaryCourse.isUpgradeable, + sku: cdPrimaryCourse.sku, + lmsPrice: cdPrimaryCourse.lmsPrice?.doubleValue, + isSelfPaced: cdPrimaryCourse.isSelfPaced ) } @@ -278,7 +282,10 @@ public class DashboardPersistence: DashboardPersistenceProtocol { return cdAssignment } cdPrimaryCourse.pastAssignments = NSSet(array: pastAssignments) - + var lmsPrice: NSNumber? + if let price = primaryCourse.lmsPrice { + lmsPrice = NSNumber(value: price) + } cdPrimaryCourse.name = primaryCourse.name cdPrimaryCourse.org = primaryCourse.org cdPrimaryCourse.courseID = primaryCourse.courseID @@ -290,6 +297,10 @@ public class DashboardPersistence: DashboardPersistenceProtocol { cdPrimaryCourse.progressPossible = Int32(primaryCourse.progressPossible) cdPrimaryCourse.lastVisitedBlockID = primaryCourse.lastVisitedBlockID cdPrimaryCourse.resumeTitle = primaryCourse.resumeTitle + cdPrimaryCourse.sku = primaryCourse.sku + cdPrimaryCourse.lmsPrice = lmsPrice + cdPrimaryCourse.isUpgradeable = primaryCourse.isUpgradeable + cdPrimaryCourse.isSelfPaced = primaryCourse.isSelfPaced ?? false newEnrollment.primaryCourse = cdPrimaryCourse } From 9602f9a1a0683374ab687df077eefd3d8e98cea5 Mon Sep 17 00:00:00 2001 From: Vadim Kuznetsov Date: Fri, 12 Jul 2024 08:55:49 +0300 Subject: [PATCH 2/4] chore: merge resolve --- OpenEdX/Data/DashboardPersistence.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/OpenEdX/Data/DashboardPersistence.swift b/OpenEdX/Data/DashboardPersistence.swift index ffe04351..2e9b3dc4 100644 --- a/OpenEdX/Data/DashboardPersistence.swift +++ b/OpenEdX/Data/DashboardPersistence.swift @@ -273,7 +273,11 @@ public class DashboardPersistence: DashboardPersistenceProtocol { resumeTitle: cdPrimaryCourse.resumeTitle, auditAccessExpires: cdPrimaryCourse.auditAccessExpires, startDisplay: cdPrimaryCourse.startDisplay, - startType: DisplayStartType(value: cdPrimaryCourse.startType) + startType: DisplayStartType(value: cdPrimaryCourse.startType), + isUpgradeable: cdPrimaryCourse.isUpgradeable, + sku: cdPrimaryCourse.sku, + lmsPrice: cdPrimaryCourse.lmsPrice?.doubleValue, + isSelfPaced: cdPrimaryCourse.isSelfPaced ) } From 30f87c1bfdc4a0c4d69f63569522acdcb91352b8 Mon Sep 17 00:00:00 2001 From: Vadim Kuznetsov Date: Fri, 12 Jul 2024 09:33:18 +0300 Subject: [PATCH 3/4] chore: fix for PR review --- .../CourseUpgrade/View/UpgradeCourseView.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift b/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift index ea895b59..57cd6d82 100644 --- a/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift +++ b/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift @@ -11,8 +11,22 @@ import Swinject public enum CourseAccessErrorHelperType { case isEndDateOld(date: Date) case startDateError(date: Date?) - case auditExpired(date: Date?, sku: String, courseID: String, pacing: String, screen: CourseUpgradeScreen, lmsPrice: Double) - case upgradeable(date: Date?, sku: String, courseID: String, pacing: String, screen: CourseUpgradeScreen, lmsPrice: Double) + case auditExpired( + date: Date?, + sku: String, + courseID: String, + pacing: String, + screen: CourseUpgradeScreen, + lmsPrice: Double + ) + case upgradeable( + date: Date?, + sku: String, + courseID: String, + pacing: String, + screen: CourseUpgradeScreen, + lmsPrice: Double + ) } public struct UpgradeCourseView: View { From 2faffd7cb76e72478bbc33b7816d9362e9a961c5 Mon Sep 17 00:00:00 2001 From: Vadim Kuznetsov Date: Fri, 12 Jul 2024 12:54:35 +0300 Subject: [PATCH 4/4] chore: use outline trophy icon --- Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index 19752da9..47e5c9a7 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -158,7 +158,7 @@ public struct PrimaryCardView: View { courseButton( title: CoreLocalization.CourseUpgrade.Button.upgrade, description: nil, - icon: Image(systemName: "trophy.fill"), + icon: Image(systemName: "trophy"), selected: false, action: upgradeAction )