diff --git a/Kickstarter-iOS/Features/ManagePledge/Controllers/ManagePledgeViewControllerTests.swift b/Kickstarter-iOS/Features/ManagePledge/Controllers/ManagePledgeViewControllerTests.swift index 072855cb44..11ceb66576 100644 --- a/Kickstarter-iOS/Features/ManagePledge/Controllers/ManagePledgeViewControllerTests.swift +++ b/Kickstarter-iOS/Features/ManagePledge/Controllers/ManagePledgeViewControllerTests.swift @@ -363,4 +363,69 @@ final class ManagePledgeViewControllerTests: TestCase { } } } + + func testView_CurrentUser_IsBacker_PledgeOverTime() { + let user = User.template + |> User.lens.id .~ 1 + + let reward = Reward.template + |> Reward.lens.shipping.enabled .~ true + |> Reward.lens.remaining .~ 49 + |> Reward.lens.localPickup .~ nil + + let addOns = [Reward.postcards |> Reward.lens.minimum .~ 10] + + let backing = Backing.template + |> Backing.lens.addOns .~ addOns + |> Backing.lens.amount .~ 22 + |> Backing.lens.reward .~ reward + |> Backing.lens.rewardId .~ reward.id + |> Backing.lens.paymentSource .~ Backing.PaymentSource.template + + let project = Project.cosmicSurgery + |> Project.lens.personalization.backing .~ backing + + let env = ProjectAndBackingEnvelope(project: project, backing: backing) + + let mockService = MockService( + fetchManagePledgeViewBackingResult: .success(env), + fetchProjectResult: .success(project), + fetchProjectRewardsResult: .success([reward]) + ) + + let mockConfigClient = MockRemoteConfigClient() + mockConfigClient.features = [ + RemoteConfigFeature.pledgeOverTime.rawValue: true + ] + + // TODO: Update to `all` languages when the string translations task is finished. [MBL-1860](https://kickstarter.atlassian.net/browse/MBL-1860) + orthogonalCombos([Language.en], [Device.phone4_7inch, Device.pad]).forEach { language, device in + withEnvironment( + apiService: mockService, + currentUser: user, + language: language, + remoteConfigClient: mockConfigClient + ) { + let controller = ManagePledgeViewController.instantiate() + controller.configureWith(params: (Param.slug("project-slug"), Param.id(1))) + let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller) + parent.view.frame.size.height = 1_200 + + // Network request completes + self.scheduler.advance() + + // endRefreshing is delayed by 300ms for animation duration + self.scheduler.advance(by: .milliseconds(300)) + + controller.tableView.layoutIfNeeded() + controller.tableView.reloadData() + + assertSnapshot( + matching: parent.view, + as: .image(perceptualPrecision: 0.98), + named: "lang_\(language)_device_\(device)" + ) + } + } + } } diff --git a/Kickstarter-iOS/Features/ManagePledge/Controllers/__Snapshots__/ManagePledgeViewControllerTests/testView_CurrentUser_IsBacker_PledgeOverTime.lang_en_device_pad.png b/Kickstarter-iOS/Features/ManagePledge/Controllers/__Snapshots__/ManagePledgeViewControllerTests/testView_CurrentUser_IsBacker_PledgeOverTime.lang_en_device_pad.png new file mode 100644 index 0000000000..c258498019 Binary files /dev/null and b/Kickstarter-iOS/Features/ManagePledge/Controllers/__Snapshots__/ManagePledgeViewControllerTests/testView_CurrentUser_IsBacker_PledgeOverTime.lang_en_device_pad.png differ diff --git a/Kickstarter-iOS/Features/ManagePledge/Controllers/__Snapshots__/ManagePledgeViewControllerTests/testView_CurrentUser_IsBacker_PledgeOverTime.lang_en_device_phone4_7inch.png b/Kickstarter-iOS/Features/ManagePledge/Controllers/__Snapshots__/ManagePledgeViewControllerTests/testView_CurrentUser_IsBacker_PledgeOverTime.lang_en_device_phone4_7inch.png new file mode 100644 index 0000000000..1c5ea91b86 Binary files /dev/null and b/Kickstarter-iOS/Features/ManagePledge/Controllers/__Snapshots__/ManagePledgeViewControllerTests/testView_CurrentUser_IsBacker_PledgeOverTime.lang_en_device_phone4_7inch.png differ diff --git a/Library/ViewModels/ManagePledgeSummaryViewModel.swift b/Library/ViewModels/ManagePledgeSummaryViewModel.swift index d3c89c5964..1e546f0e65 100644 --- a/Library/ViewModels/ManagePledgeSummaryViewModel.swift +++ b/Library/ViewModels/ManagePledgeSummaryViewModel.swift @@ -23,6 +23,7 @@ public struct ManagePledgeSummaryViewData: Equatable { public let shippingAmount: Double? public let shippingAmountHidden: Bool public let rewardIsLocalPickup: Bool + public let paymentIncrements: [PledgePaymentIncrement]? } public protocol ManagePledgeSummaryViewModelInputs { @@ -157,7 +158,8 @@ private func pledgeStatusLabelViewData(with data: ManagePledgeSummaryViewData) - projectCurrencyCountry: data.projectCurrencyCountry, projectDeadline: data.projectDeadline, projectState: data.projectState, - backingState: data.backingState + backingState: data.backingState, + paymentIncrements: data.paymentIncrements ) } diff --git a/Library/ViewModels/ManagePledgeSummaryViewModelTests.swift b/Library/ViewModels/ManagePledgeSummaryViewModelTests.swift index d334ad92fe..9f96bd264e 100644 --- a/Library/ViewModels/ManagePledgeSummaryViewModelTests.swift +++ b/Library/ViewModels/ManagePledgeSummaryViewModelTests.swift @@ -55,7 +55,8 @@ final class ManagePledgeSummaryViewModelTests: TestCase { rewardMinimum: 30, shippingAmount: nil, shippingAmountHidden: true, - rewardIsLocalPickup: false + rewardIsLocalPickup: false, + paymentIncrements: nil ) self.vm.inputs.configureWith(data) @@ -86,7 +87,8 @@ final class ManagePledgeSummaryViewModelTests: TestCase { rewardMinimum: 30, shippingAmount: nil, shippingAmountHidden: true, - rewardIsLocalPickup: false + rewardIsLocalPickup: false, + paymentIncrements: nil ) self.vm.inputs.configureWith(data) @@ -121,7 +123,8 @@ final class ManagePledgeSummaryViewModelTests: TestCase { rewardMinimum: 30, shippingAmount: nil, shippingAmountHidden: true, - rewardIsLocalPickup: false + rewardIsLocalPickup: false, + paymentIncrements: nil ) withEnvironment(currentUser: user) { @@ -159,7 +162,8 @@ final class ManagePledgeSummaryViewModelTests: TestCase { rewardMinimum: 30, shippingAmount: nil, shippingAmountHidden: true, - rewardIsLocalPickup: false + rewardIsLocalPickup: false, + paymentIncrements: nil ) withEnvironment(currentUser: user) { @@ -194,7 +198,8 @@ final class ManagePledgeSummaryViewModelTests: TestCase { rewardMinimum: 30, shippingAmount: nil, shippingAmountHidden: true, - rewardIsLocalPickup: true + rewardIsLocalPickup: true, + paymentIncrements: nil ) self.vm.inputs.configureWith(data) diff --git a/Library/ViewModels/ManagePledgeViewModel.swift b/Library/ViewModels/ManagePledgeViewModel.swift index 4b53f4b1a5..6c7e6a8ad9 100644 --- a/Library/ViewModels/ManagePledgeViewModel.swift +++ b/Library/ViewModels/ManagePledgeViewModel.swift @@ -570,6 +570,22 @@ private func managePledgeSummaryViewData( let projectCurrencyCountry = projectCountry(forCurrency: project.stats.currency) ?? project.country + /* + TODO: Replace mock data with backing.PaymentIncrements list. + + Context: + - Adding mock data when `featurePledgeOverTimeEnabled()` is `true`. + + Pending: + - Awaiting implementation of the real backing.PaymentIncrements data source. + + Ticket: [MBL-1851](https://kickstarter.atlassian.net/browse/MBL-1851) + */ + var paymentIncrements: [PledgePaymentIncrement]? + if featurePledgeOverTimeEnabled() { + paymentIncrements = mockPledgePaymentIncrement() + } + return ManagePledgeSummaryViewData( backerId: backer.id, backerName: backer.name, @@ -589,7 +605,8 @@ private func managePledgeSummaryViewData( rewardMinimum: allRewardsTotal(for: backing), shippingAmount: backing.shippingAmount.flatMap(Double.init), shippingAmountHidden: backing.reward?.shipping.enabled == false || backing.shippingAmount == 0, - rewardIsLocalPickup: isRewardLocalPickup + rewardIsLocalPickup: isRewardLocalPickup, + paymentIncrements: paymentIncrements ) } diff --git a/Library/ViewModels/ManagePledgeViewModelTests.swift b/Library/ViewModels/ManagePledgeViewModelTests.swift index 1e2f713f9a..9c86cbe3de 100644 --- a/Library/ViewModels/ManagePledgeViewModelTests.swift +++ b/Library/ViewModels/ManagePledgeViewModelTests.swift @@ -197,7 +197,8 @@ internal final class ManagePledgeViewModelTests: TestCase { rewardMinimum: 10.0, shippingAmount: envelope.backing.shippingAmount.flatMap(Double.init), shippingAmountHidden: true, - rewardIsLocalPickup: false + rewardIsLocalPickup: false, + paymentIncrements: nil ) withEnvironment(apiService: mockService) { @@ -872,7 +873,8 @@ internal final class ManagePledgeViewModelTests: TestCase { rewardMinimum: 0, shippingAmount: envelope.backing.shippingAmount.flatMap(Double.init), shippingAmountHidden: true, - rewardIsLocalPickup: false + rewardIsLocalPickup: false, + paymentIncrements: nil ) // Pledge amount 50 @@ -895,7 +897,8 @@ internal final class ManagePledgeViewModelTests: TestCase { rewardMinimum: 0, shippingAmount: envelope.backing.shippingAmount.flatMap(Double.init), shippingAmountHidden: true, - rewardIsLocalPickup: false + rewardIsLocalPickup: false, + paymentIncrements: nil ) let pledgePaymentMethodViewData = ManagePledgePaymentMethodViewData( diff --git a/Library/ViewModels/NoShippingPledgeViewModel.swift b/Library/ViewModels/NoShippingPledgeViewModel.swift index 8fdbcc457e..b90cc69f53 100644 --- a/Library/ViewModels/NoShippingPledgeViewModel.swift +++ b/Library/ViewModels/NoShippingPledgeViewModel.swift @@ -1266,10 +1266,10 @@ private func pledgeAmountSummaryViewData( } // TODO: Remove this when implementing the API [MBL-1838](https://kickstarter.atlassian.net/browse/MBL-1838) -private func mockPledgePaymentIncrement() -> [PledgePaymentIncrement] { +public func mockPledgePaymentIncrement() -> [PledgePaymentIncrement] { var increments: [PledgePaymentIncrement] = [] #if DEBUG - var timeStamp = Date().timeIntervalSince1970 + var timeStamp = TimeInterval(1_733_931_903) for _ in 1...4 { timeStamp += 30 * 24 * 60 * 60 increments.append(PledgePaymentIncrement( diff --git a/Library/ViewModels/PledgeStatusLabelViewModel.swift b/Library/ViewModels/PledgeStatusLabelViewModel.swift index b4ff89578c..353c5de5d8 100644 --- a/Library/ViewModels/PledgeStatusLabelViewModel.swift +++ b/Library/ViewModels/PledgeStatusLabelViewModel.swift @@ -11,6 +11,15 @@ public struct PledgeStatusLabelViewData { public let projectDeadline: TimeInterval public let projectState: Project.State public let backingState: Backing.Status + public let paymentIncrements: [PledgePaymentIncrement]? +} + +extension PledgeStatusLabelViewData { + public var isPledgeOverTime: Bool { + guard let paymentIncrements = self.paymentIncrements, !paymentIncrements.isEmpty else { return false } + + return true + } } public protocol PledgeStatusLabelViewModelInputs { @@ -72,30 +81,32 @@ private func statusLabelText(with data: PledgeStatusLabelViewData) -> NSAttribut let string: String - switch (data.backingState, currentUserIsCreatorOfProject) { + switch (data.backingState, currentUserIsCreatorOfProject, data.isPledgeOverTime) { // Backer context - case (.canceled, false): + case (.canceled, false, _): string = Strings.You_canceled_your_pledge_for_this_project() - case (.collected, false): + case (.collected, false, _): string = Strings.We_collected_your_pledge_for_this_project() - case (.dropped, false): + case (.dropped, false, _): string = Strings.Your_pledge_was_dropped_because_of_payment_errors() - case (.errored, false): + case (.errored, false, _): string = Strings.We_cant_process_your_pledge_Please_update_your_payment_method() - case (.pledged, _): + case (.pledged, _, false): return attributedConfirmationString(with: data) - case (.preauth, false): + case (.pledged, _, true): + return attributedPledgeOverTimeConfirmationString(with: data) + case (.preauth, false, _): string = Strings.We_re_processing_your_pledge_pull_to_refresh() // Creator context - case (.canceled, true): + case (.canceled, true, _): string = Strings.The_backer_canceled_their_pledge_for_this_project() - case (.collected, true): + case (.collected, true, _): string = Strings.We_collected_the_backers_pledge_for_this_project() - case (.dropped, true): + case (.dropped, true, _): string = Strings.This_pledge_was_dropped_because_of_payment_errors() - case (.errored, true): + case (.errored, true, _): string = Strings.We_cant_process_this_pledge_because_of_a_problem_with_the_backers_payment_method() - case (.preauth, true): + case (.preauth, true, _): string = Strings.We_re_processing_this_pledge_pull_to_refresh() } @@ -173,3 +184,33 @@ private func attributedConfirmationString(with data: PledgeStatusLabelViewData) with: font, foregroundColor: foregroundColor, attributes: attributes, bolding: [pledgeTotal, date] ) } + +private func attributedPledgeOverTimeConfirmationString(with data: PledgeStatusLabelViewData) + -> NSAttributedString { + guard let firstPaymentIncrement = data.paymentIncrements?.first, + !data.currentUserIsCreatorOfProject else { + return attributedConfirmationString(with: data) + } + + let date = Format.date(secondsInUTC: firstPaymentIncrement.scheduledCollection, template: "MMMM d, yyyy") + let paymentAmount = Format.currency( + firstPaymentIncrement.amount.amount, + country: data.projectCurrencyCountry + ) + + let font = UIFont.ksr_subhead() + let foregroundColor = UIColor.ksr_support_700 + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let attributes = [ + NSAttributedString.Key.paragraphStyle: paragraphStyle + ] + + // TODO: add strings translations [MBL-1860](https://kickstarter.atlassian.net/browse/MBL-1860) + return "You have selected Pledge Over Time. If the project reaches its funding goal, the first charge of \(paymentAmount) will be collected on \(date)." + .attributed( + with: font, foregroundColor: foregroundColor, attributes: attributes, bolding: [paymentAmount, date] + ) +} diff --git a/Library/ViewModels/PledgeStatusLabelViewModelTests.swift b/Library/ViewModels/PledgeStatusLabelViewModelTests.swift index 453ae8e88f..0974a52a53 100644 --- a/Library/ViewModels/PledgeStatusLabelViewModelTests.swift +++ b/Library/ViewModels/PledgeStatusLabelViewModelTests.swift @@ -25,7 +25,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.canceled, - backingState: Backing.Status.pledged + backingState: Backing.Status.pledged, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -43,7 +44,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.failed, - backingState: Backing.Status.pledged + backingState: Backing.Status.pledged, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -61,7 +63,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.live, - backingState: Backing.Status.canceled + backingState: Backing.Status.canceled, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -79,7 +82,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.collected + backingState: Backing.Status.collected, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -97,7 +101,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.dropped + backingState: Backing.Status.dropped, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -115,7 +120,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.errored + backingState: Backing.Status.errored, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -133,7 +139,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.live, - backingState: Backing.Status.preauth + backingState: Backing.Status.preauth, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -151,7 +158,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.pledged + backingState: Backing.Status.pledged, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -170,7 +178,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.pledged + backingState: Backing.Status.pledged, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -193,7 +202,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: backingState + backingState: backingState, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -210,7 +220,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.canceled, - backingState: Backing.Status.pledged + backingState: Backing.Status.pledged, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -228,7 +239,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.failed, - backingState: Backing.Status.pledged + backingState: Backing.Status.pledged, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -246,7 +258,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.live, - backingState: Backing.Status.canceled + backingState: Backing.Status.canceled, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -264,7 +277,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.collected + backingState: Backing.Status.collected, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -282,7 +296,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.dropped + backingState: Backing.Status.dropped, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -300,7 +315,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.errored + backingState: Backing.Status.errored, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -318,7 +334,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.live, - backingState: Backing.Status.pledged + backingState: Backing.Status.pledged, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -337,7 +354,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.pledged + backingState: Backing.Status.pledged, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -356,7 +374,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: Backing.Status.preauth + backingState: Backing.Status.preauth, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -378,7 +397,8 @@ final class PledgeStatusLabelViewModelTests: TestCase { projectCurrencyCountry: Project.Country.hk, projectDeadline: 1_476_657_315, projectState: Project.State.successful, - backingState: backingState + backingState: backingState, + paymentIncrements: nil ) self.vm.inputs.configure(with: data) @@ -386,4 +406,23 @@ final class PledgeStatusLabelViewModelTests: TestCase { self.labelTextString.assertValues([]) } + + func testBackingStatus_Pledged_Backer_PledgeOverTime() { + let data = PledgeStatusLabelViewData( + currentUserIsCreatorOfProject: false, + needsConversion: false, + pledgeAmount: 10, + projectCurrencyCountry: Project.Country.us, + projectDeadline: 1_476_657_315, + projectState: Project.State.successful, + backingState: Backing.Status.pledged, + paymentIncrements: mockPledgePaymentIncrement() + ) + + self.vm.inputs.configure(with: data) + + self.labelTextString.assertValues([ + "You have selected Pledge Over Time. If the project reaches its funding goal, the first charge of $250 will be collected on January 10, 2025." + ]) + } }