diff --git a/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift b/Core/Core/CourseUpgrade/View/UpgradeCourseView.swift index 302fc432e..57cd6d82a 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) - 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 +59,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 +71,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 +140,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 +156,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 024f41244..6cbc2879d 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 994a073db..585a8179f 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 0fb286132..44bb80f5c 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 497edb29a..bc6fee000 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 3253fdb13..9b3e08845 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 683984668..47e5c9a70 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"), + 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 5a65c2147..a5e61848b 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 dee29a4a3..2e9b3dc40 100644 --- a/OpenEdX/Data/DashboardPersistence.swift +++ b/OpenEdX/Data/DashboardPersistence.swift @@ -161,7 +161,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 ) } @@ -269,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 ) } @@ -389,7 +397,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 @@ -401,6 +412,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 }