diff --git a/Kickstarter-iOS/Features/PledgeOverTime/Controller/PledgePaymentPlansViewController.swift b/Kickstarter-iOS/Features/PledgeOverTime/Controller/PledgePaymentPlansViewController.swift index ea659dace3..3bf19a2d1d 100644 --- a/Kickstarter-iOS/Features/PledgeOverTime/Controller/PledgePaymentPlansViewController.swift +++ b/Kickstarter-iOS/Features/PledgeOverTime/Controller/PledgePaymentPlansViewController.swift @@ -85,16 +85,20 @@ final class PledgePaymentPlansViewController: UIViewController { guard let self = self else { return } self.pledgeInFullOption.configureWith(value: PledgePaymentPlanOptionData( + ineligible: data.ineligible, type: .pledgeInFull, selectedType: data.selectedPlan, paymentIncrements: data.paymentIncrements, - project: data.project + project: data.project, + thresholdAmount: data.thresholdAmount )) self.pledgeOverTimeOption.configureWith(value: PledgePaymentPlanOptionData( + ineligible: data.ineligible, type: .pledgeOverTime, selectedType: data.selectedPlan, paymentIncrements: data.paymentIncrements, - project: data.project + project: data.project, + thresholdAmount: data.thresholdAmount )) } diff --git a/Kickstarter-iOS/Features/PledgeOverTime/Controller/PledgePaymentPlansViewControllerTest.swift b/Kickstarter-iOS/Features/PledgeOverTime/Controller/PledgePaymentPlansViewControllerTest.swift index aa06a22fc9..d891082e42 100644 --- a/Kickstarter-iOS/Features/PledgeOverTime/Controller/PledgePaymentPlansViewControllerTest.swift +++ b/Kickstarter-iOS/Features/PledgeOverTime/Controller/PledgePaymentPlansViewControllerTest.swift @@ -6,6 +6,7 @@ import SnapshotTesting import UIKit final class PledgePaymentPlansViewControllerTest: TestCase { + private let thresholdAmount = 125.0 override func setUp() { super.setUp() AppEnvironment.pushEnvironment(mainBundle: Bundle.framework) @@ -25,7 +26,11 @@ final class PledgePaymentPlansViewControllerTest: TestCase { withEnvironment(language: language) { let controller = PledgePaymentPlansViewController.instantiate() - let data = PledgePaymentPlansAndSelectionData(selectedPlan: .pledgeInFull, project: project) + let data = PledgePaymentPlansAndSelectionData( + selectedPlan: .pledgeInFull, + project: project, + thresholdAmount: thresholdAmount + ) controller.configure(with: data) let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller) @@ -48,7 +53,9 @@ final class PledgePaymentPlansViewControllerTest: TestCase { let data = PledgePaymentPlansAndSelectionData( selectedPlan: .pledgeOverTime, increments: testIncrements, - project: project + ineligible: false, + project: project, + thresholdAmount: thresholdAmount ) controller.configure(with: data) @@ -60,7 +67,32 @@ final class PledgePaymentPlansViewControllerTest: TestCase { let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller) parent.view.frame.size.height = 400 - self.scheduler.advance(by: .seconds(3)) + self.scheduler.advance(by: .seconds(1)) + + assertSnapshot(matching: parent.view, as: .image, named: "lang_\(language)_device_\(device)") + } + } + } + + func testView_PledgeOverTimeIneligible() { + orthogonalCombos([Language.en], [Device.pad, Device.phone4_7inch]).forEach { language, device in + withEnvironment(language: language) { + let controller = PledgePaymentPlansViewController.instantiate() + + let data = PledgePaymentPlansAndSelectionData( + selectedPlan: .pledgeInFull, + increments: testPledgePaymentIncrement(), + ineligible: true, + project: Project.template, + thresholdAmount: self.thresholdAmount + ) + + controller.configure(with: data) + + let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller) + parent.view.frame.size.height = 120 + + self.scheduler.advance(by: .seconds(1)) assertSnapshot(matching: parent.view, as: .image, named: "lang_\(language)_device_\(device)") } diff --git a/Kickstarter-iOS/Features/PledgeOverTime/Controller/__Snapshots__/PledgePaymentPlansViewControllerTest/testView_PledgeOverTimeIneligible.lang_en_device_pad.png b/Kickstarter-iOS/Features/PledgeOverTime/Controller/__Snapshots__/PledgePaymentPlansViewControllerTest/testView_PledgeOverTimeIneligible.lang_en_device_pad.png new file mode 100644 index 0000000000..5847f4c0fe Binary files /dev/null and b/Kickstarter-iOS/Features/PledgeOverTime/Controller/__Snapshots__/PledgePaymentPlansViewControllerTest/testView_PledgeOverTimeIneligible.lang_en_device_pad.png differ diff --git a/Kickstarter-iOS/Features/PledgeOverTime/Controller/__Snapshots__/PledgePaymentPlansViewControllerTest/testView_PledgeOverTimeIneligible.lang_en_device_phone4_7inch.png b/Kickstarter-iOS/Features/PledgeOverTime/Controller/__Snapshots__/PledgePaymentPlansViewControllerTest/testView_PledgeOverTimeIneligible.lang_en_device_phone4_7inch.png new file mode 100644 index 0000000000..bd618a4cfe Binary files /dev/null and b/Kickstarter-iOS/Features/PledgeOverTime/Controller/__Snapshots__/PledgePaymentPlansViewControllerTest/testView_PledgeOverTimeIneligible.lang_en_device_phone4_7inch.png differ diff --git a/Kickstarter-iOS/Features/PledgeOverTime/Views/PledgePaymentPlanOptionView.swift b/Kickstarter-iOS/Features/PledgeOverTime/Views/PledgePaymentPlanOptionView.swift index 4182f534a3..d03fce85f8 100644 --- a/Kickstarter-iOS/Features/PledgeOverTime/Views/PledgePaymentPlanOptionView.swift +++ b/Kickstarter-iOS/Features/PledgeOverTime/Views/PledgePaymentPlanOptionView.swift @@ -8,9 +8,14 @@ private enum Constants { public static let detailsStackViewSpacing = Styles.grid(6) public static let incrementStackViewSpacing = Styles.gridHalf(1) public static let optionDescriptorStackViewSpacing = Styles.grid(1) + public static let ineligibleBadgeTopButtonPadding = 6.0 + public static let ineligibleBadgeLeadingTrailingPadding = 8.0 /// Size public static let selectionIndicatorImageWith = Styles.grid(4) + + /// Corner radius + public static let defaultCornerRadius = Styles.grid(1) } protocol PledgePaymentPlanOptionViewDelegate: AnyObject { @@ -34,6 +39,8 @@ final class PledgePaymentPlanOptionView: UIView { private lazy var selectionIndicatorImageView: UIImageView = { UIImageView(frame: .zero) }() private lazy var termsOfUseButton: UIButton = { UIButton(frame: .zero) }() private lazy var paymentIncrementsStackView: UIStackView = { UIStackView(frame: .zero) }() + private lazy var ineligibleBadgeLabel: UILabel = { UILabel(frame: .zero) }() + private lazy var ineligibleBadgeView: UIView = { UIView(frame: .zero) }() private let viewModel: PledgePaymentPlansOptionViewModelType = PledgePaymentPlansOptionViewModel() @@ -64,6 +71,7 @@ final class PledgePaymentPlanOptionView: UIView { self.optionDescriptorStackView.addArrangedSubviews( self.titleLabel, self.subtitleLabel, + self.ineligibleBadgeView, self.termsOfUseButton, self.paymentIncrementsStackView ) @@ -130,6 +138,30 @@ final class PledgePaymentPlanOptionView: UIView { ]) self.termsOfUseButton.setContentHuggingPriority(.required, for: .horizontal) + + self.ineligibleBadgeView.addSubview(self.ineligibleBadgeLabel) + self.ineligibleBadgeLabel.translatesAutoresizingMaskIntoConstraints = false + self.ineligibleBadgeLabel.setContentHuggingPriority(.required, for: .horizontal) + self.ineligibleBadgeView.setContentHuggingPriority(.required, for: .horizontal) + + NSLayoutConstraint.activate([ + self.ineligibleBadgeLabel.topAnchor.constraint( + equalTo: self.ineligibleBadgeView.topAnchor, + constant: Constants.ineligibleBadgeTopButtonPadding + ), + self.ineligibleBadgeLabel.bottomAnchor.constraint( + equalTo: self.ineligibleBadgeView.bottomAnchor, + constant: -Constants.ineligibleBadgeTopButtonPadding + ), + self.ineligibleBadgeLabel.leadingAnchor.constraint( + equalTo: self.ineligibleBadgeView.leadingAnchor, + constant: Constants.ineligibleBadgeLeadingTrailingPadding + ), + self.ineligibleBadgeLabel.trailingAnchor.constraint( + equalTo: self.ineligibleBadgeView.trailingAnchor, + constant: -Constants.ineligibleBadgeLeadingTrailingPadding + ) + ]) } private func configureTapGestureAndActions() { @@ -158,6 +190,8 @@ final class PledgePaymentPlanOptionView: UIView { applySelectionIndicatorImageViewStyle(self.selectionIndicatorImageView) applyTermsOfUseStyle(self.termsOfUseButton) applyPaymentIncrementsStackViewStyle(self.paymentIncrementsStackView) + applyIneligibleBadgeViewStyle(self.ineligibleBadgeView) + applyIneligibleBadgeLabelStyle(self.ineligibleBadgeLabel) } // MARK: - View model @@ -192,9 +226,21 @@ final class PledgePaymentPlanOptionView: UIView { self.delegate?.pledgePaymentPlansViewController(self, didTapTermsOfUseWith: helpType) } + self.ineligibleBadgeView.rac.hidden = self.viewModel.outputs.ineligibleBadgeHidden + self.ineligibleBadgeLabel.rac.text = self.viewModel.outputs.ineligibleBadgeText + self.termsOfUseButton.rac.hidden = self.viewModel.outputs.termsOfUseButtonHidden self.paymentIncrementsStackView.rac.hidden = self.viewModel.outputs.paymentIncrementsHidden + self.viewModel.outputs.optionViewEnabled + .observeForUI() + .observeValues { [weak self] isOptionViewEnabled in + guard let self = self else { return } + + self.isUserInteractionEnabled = isOptionViewEnabled + applyTextColorByState(self.titleLabel, isEnabled: isOptionViewEnabled) + } + self.viewModel.outputs.paymentIncrements .observeForUI() .observeValues { [weak self] increments in @@ -284,7 +330,6 @@ private func applyTitleLabelStyle(_ label: UILabel) { label.adjustsFontForContentSizeCategory = true label.numberOfLines = 0 label.font = UIFont.ksr_subhead().bolded - label.textColor = .ksr_black } private func applySubtitleLabelStyle(_ label: UILabel) { @@ -317,6 +362,7 @@ private func applyIncrementStackViewStyle(_ stackView: UIStackView) { private func applyIncrementDetailsStackViewStyle(_ stackview: UIStackView) { stackview.axis = .horizontal + stackview.distribution = .fill stackview.spacing = Constants.detailsStackViewSpacing } @@ -334,3 +380,20 @@ private func applyIncrementDateLabelStyle(_ label: UILabel) { label.textAlignment = .left label.adjustsFontForContentSizeCategory = true } + +private func applyIneligibleBadgeViewStyle(_ view: UIView) { + view.backgroundColor = .ksr_support_100 + view.rounded(with: Constants.defaultCornerRadius) +} + +private func applyIneligibleBadgeLabelStyle(_ label: UILabel) { + label.font = UIFont.ksr_caption1().bolded + label.textColor = .ksr_support_500 + label.textAlignment = .center + label.numberOfLines = 0 + label.adjustsFontForContentSizeCategory = true +} + +private func applyTextColorByState(_ label: UILabel, isEnabled: Bool) { + label.textColor = isEnabled ? .ksr_black : .ksr_support_300 +} diff --git a/Kickstarter-iOS/Features/PledgePaymentMethods/Controller/PostCampaignPledgeRewardsSummaryTotalViewController.swift b/Kickstarter-iOS/Features/PledgePaymentMethods/Controller/PostCampaignPledgeRewardsSummaryTotalViewController.swift index cc6d2a1cac..f0fca9eaab 100644 --- a/Kickstarter-iOS/Features/PledgePaymentMethods/Controller/PostCampaignPledgeRewardsSummaryTotalViewController.swift +++ b/Kickstarter-iOS/Features/PledgePaymentMethods/Controller/PostCampaignPledgeRewardsSummaryTotalViewController.swift @@ -3,6 +3,16 @@ import Library import Prelude import UIKit +private enum Constants { + /// Spacing & Padding + public static let badgeTopButtonPadding = 6.0 + public static let badgeLeadingTrailingPadding = 8.0 + public static let defaultStackViewSpacing = Styles.grid(1) + + /// Corner radius + public static let defaultCornerRadius = Styles.grid(1) +} + final class PostCampaignPledgeRewardsSummaryTotalViewController: UIViewController { // MARK: - Properties @@ -17,6 +27,10 @@ final class PostCampaignPledgeRewardsSummaryTotalViewController: UIViewControlle private lazy var totalConversionLabel: UILabel = { UILabel(frame: .zero) }() private lazy var confirmationLabel: UILabel = { UILabel(frame: .zero) }() private lazy var totalStackView: UIStackView = { UIStackView(frame: .zero) }() + private lazy var pledgeOverTimeStackView: UIStackView = { UIStackView(frame: .zero) }() + private lazy var pledgeOverTimeBadgeView: UIView = { UIView(frame: .zero) }() + private lazy var pledgeOverTimeBadgeLabel: UILabel = { UILabel(frame: .zero) }() + private lazy var pledgeOverTimeChargesLabel: UILabel = { UILabel(frame: .zero) }() private let viewModel: PledgeSummaryViewModelType = PledgeSummaryViewModel() // MARK: - Lifecycle @@ -25,6 +39,7 @@ final class PostCampaignPledgeRewardsSummaryTotalViewController: UIViewControlle super.viewDidLoad() self.configureSubviews() + self.setupConstraints() self.bindStyles() self.viewModel.inputs.viewDidLoad() @@ -60,14 +75,24 @@ final class PostCampaignPledgeRewardsSummaryTotalViewController: UIViewControlle |> totalConversionLabelStyle applyConfirmationLabelStyle(self.confirmationLabel) + + applyPledgeOverTimeStackViewStyle(self.pledgeOverTimeStackView) + applyPledgeOverTimeBadgeViewStyle(self.pledgeOverTimeBadgeView) + applyPledgeOverTimeBadgeLabelStyle(self.pledgeOverTimeBadgeLabel) + applyPledgeOverTimeChargesLabelStyle(self.pledgeOverTimeChargesLabel) } + // MARK: - Configuration + private func configureSubviews() { _ = (self.rootStackView, self.view) |> ksr_addSubviewToParent() |> ksr_constrainViewToEdgesInParent() - _ = ([self.titleAndTotalStackView, self.confirmationLabel], self.rootStackView) + _ = ( + [self.titleAndTotalStackView, self.pledgeOverTimeStackView, self.confirmationLabel], + self.rootStackView + ) |> ksr_addArrangedSubviewsToStackView() _ = ([self.titleLabel, self.totalStackView], self.titleAndTotalStackView) @@ -75,6 +100,41 @@ final class PostCampaignPledgeRewardsSummaryTotalViewController: UIViewControlle _ = ([self.amountLabel, self.totalConversionLabel], self.totalStackView) |> ksr_addArrangedSubviewsToStackView() + + self.pledgeOverTimeBadgeView.addSubview(self.pledgeOverTimeBadgeLabel) + self.pledgeOverTimeStackView.addArrangedSubviews( + self.pledgeOverTimeBadgeView, + self.pledgeOverTimeChargesLabel + ) + + // TODO: add strings translations [MBL-1860](https://kickstarter.atlassian.net/browse/MBL-1860) + self.pledgeOverTimeBadgeLabel.text = "Pledge Over Time" + } + + private func setupConstraints() { + self.pledgeOverTimeBadgeLabel.translatesAutoresizingMaskIntoConstraints = false + self.pledgeOverTimeBadgeLabel.setContentHuggingPriority(.required, for: .horizontal) + self.pledgeOverTimeBadgeView.setContentHuggingPriority(.required, for: .horizontal) + self.pledgeOverTimeChargesLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + + NSLayoutConstraint.activate([ + self.pledgeOverTimeBadgeLabel.topAnchor.constraint( + equalTo: self.pledgeOverTimeBadgeView.topAnchor, + constant: Constants.badgeTopButtonPadding + ), + self.pledgeOverTimeBadgeLabel.bottomAnchor.constraint( + equalTo: self.pledgeOverTimeBadgeView.bottomAnchor, + constant: -Constants.badgeTopButtonPadding + ), + self.pledgeOverTimeBadgeLabel.leadingAnchor.constraint( + equalTo: self.pledgeOverTimeBadgeView.leadingAnchor, + constant: Constants.badgeLeadingTrailingPadding + ), + self.pledgeOverTimeBadgeLabel.trailingAnchor.constraint( + equalTo: self.pledgeOverTimeBadgeView.trailingAnchor, + constant: -Constants.badgeLeadingTrailingPadding + ) + ]) } // MARK: - View model @@ -93,6 +153,9 @@ final class PostCampaignPledgeRewardsSummaryTotalViewController: UIViewControlle self.confirmationLabel.rac.hidden = self.viewModel.outputs.confirmationLabelHidden self.confirmationLabel.rac.attributedText = self.viewModel.outputs.confirmationLabelAttributedText + + self.pledgeOverTimeStackView.rac.hidden = self.viewModel.outputs.pledgeOverTimeStackViewHidden + self.pledgeOverTimeChargesLabel.rac.text = self.viewModel.outputs.pledgeOverTimeChargesText } // MARK: - Configuration @@ -166,3 +229,30 @@ private func applyConfirmationLabelStyle(_ label: UILabel) { label.numberOfLines = 0 label.backgroundColor = UIColor.ksr_white } + +private func applyPledgeOverTimeStackViewStyle(_ stackView: UIStackView) { + stackView.axis = .horizontal + stackView.spacing = Constants.defaultStackViewSpacing + stackView.alignment = .center +} + +private func applyPledgeOverTimeBadgeViewStyle(_ view: UIView) { + view.backgroundColor = .ksr_create_100 + view.rounded(with: Constants.defaultCornerRadius) +} + +private func applyPledgeOverTimeBadgeLabelStyle(_ label: UILabel) { + label.font = UIFont.ksr_caption1().bolded + label.textColor = .ksr_create_700 + label.textAlignment = .center + label.numberOfLines = 1 + label.adjustsFontForContentSizeCategory = true +} + +private func applyPledgeOverTimeChargesLabelStyle(_ label: UILabel) { + label.font = UIFont.ksr_footnote() + label.textColor = .ksr_support_700 + label.textAlignment = .right + label.numberOfLines = 0 + label.adjustsFontForContentSizeCategory = true +} diff --git a/Kickstarter-iOS/Features/PledgeView/Controllers/NoShippingPledgeViewController.swift b/Kickstarter-iOS/Features/PledgeView/Controllers/NoShippingPledgeViewController.swift index 3436f66708..ef2c66979b 100644 --- a/Kickstarter-iOS/Features/PledgeView/Controllers/NoShippingPledgeViewController.swift +++ b/Kickstarter-iOS/Features/PledgeView/Controllers/NoShippingPledgeViewController.swift @@ -295,6 +295,7 @@ final class NoShippingPledgeViewController: UIViewController, } self.viewModel.outputs.pledgeOverTimeConfigData + .skipNil() .observeForUI() .observeValues { [weak self] data in self?.paymentPlansViewController.configure(with: data) @@ -730,10 +731,9 @@ private func applySectionStackViewStyle(_ stackView: UIStackView) { extension NoShippingPledgeViewController: PledgePaymentPlansViewControllerDelegate { func pledgePaymentPlansViewController( _: PledgePaymentPlansViewController, - didSelectPaymentPlan paymentPlan: Library.PledgePaymentPlansType + didSelectPaymentPlan paymentPlan: PledgePaymentPlansType ) { - // TODO: Implement the necessary functionality once the ticket [MBL-1853] is resolved - debugPrint("pledgePaymentPlansViewController:didSelectPaymentPlan: \(paymentPlan)") + self.viewModel.inputs.paymentPlanSelected(paymentPlan) } func pledgePaymentPlansViewController( diff --git a/Kickstarter-iOS/Features/PledgeView/Controllers/NoShippingPledgeViewControllerTests.swift b/Kickstarter-iOS/Features/PledgeView/Controllers/NoShippingPledgeViewControllerTests.swift index c366e84605..8b1f338b47 100644 --- a/Kickstarter-iOS/Features/PledgeView/Controllers/NoShippingPledgeViewControllerTests.swift +++ b/Kickstarter-iOS/Features/PledgeView/Controllers/NoShippingPledgeViewControllerTests.swift @@ -311,6 +311,54 @@ final class NoShippingPledgeViewControllerTests: TestCase { RemoteConfigFeature.pledgeOverTime.rawValue: true ] + orthogonalCombos([Language.en], [Device.phone4_7inch, Device.pad]) + .forEach { language, device in + withEnvironment( + apiService: mockService, + currentUser: User.template, + language: language, + remoteConfigClient: mockConfigClient + ) { + let controller = NoShippingPledgeViewController.instantiate() + let data = PledgeViewData( + project: project, + rewards: [reward], + selectedShippingRule: .template, + selectedQuantities: [reward.id: 15], // To pass the threshold validation + selectedLocationId: nil, + refTag: nil, + context: .pledge + ) + controller.configure(with: data) + let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller) + + self.scheduler.advance(by: .seconds(1)) + + self.allowLayoutPass() + + assertSnapshot( + matching: parent.view, + as: .image(perceptualPrecision: 0.98), + named: "lang_\(language)_device_\(device)" + ) + } + } + } + + func testView_ShowCollectionPlans_Ineligible() { + let response = UserEnvelope(me: self.userWithCards) + let mockService = MockService(fetchGraphUserResult: .success(response)) + let project = Project.template + |> \.availableCardTypes .~ [CreditCardType.discover.rawValue] + |> Project.lens.isPledgeOverTimeAllowed .~ true + let reward = Reward.template + + let mockConfigClient = MockRemoteConfigClient() + mockConfigClient.features = [ + RemoteConfigFeature.noShippingAtCheckout.rawValue: true, + RemoteConfigFeature.pledgeOverTime.rawValue: true + ] + orthogonalCombos([Language.en], [Device.phone4_7inch, Device.pad]) .forEach { language, device in withEnvironment( diff --git a/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans.lang_en_device_pad.png b/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans.lang_en_device_pad.png index c6ce49bbf8..e7b9b2819b 100644 Binary files a/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans.lang_en_device_pad.png and b/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans.lang_en_device_pad.png differ diff --git a/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans.lang_en_device_phone4_7inch.png b/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans.lang_en_device_phone4_7inch.png index fe20ca5e42..f442ea4eb8 100644 Binary files a/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans.lang_en_device_phone4_7inch.png and b/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans.lang_en_device_phone4_7inch.png differ diff --git a/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans_Ineligible.lang_en_device_pad.png b/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans_Ineligible.lang_en_device_pad.png new file mode 100644 index 0000000000..4fcbee9b9b Binary files /dev/null and b/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans_Ineligible.lang_en_device_pad.png differ diff --git a/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans_Ineligible.lang_en_device_phone4_7inch.png b/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans_Ineligible.lang_en_device_phone4_7inch.png new file mode 100644 index 0000000000..b7171ebdd1 Binary files /dev/null and b/Kickstarter-iOS/Features/PledgeView/Controllers/__Snapshots__/NoShippingPledgeViewControllerTests/testView_ShowCollectionPlans_Ineligible.lang_en_device_phone4_7inch.png differ diff --git a/Kickstarter.xcodeproj/project.pbxproj b/Kickstarter.xcodeproj/project.pbxproj index 8ee7cc8bb2..255066c946 100644 --- a/Kickstarter.xcodeproj/project.pbxproj +++ b/Kickstarter.xcodeproj/project.pbxproj @@ -267,6 +267,7 @@ 3385CF012CF6116B00A33D86 /* UIStackView+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3385CF002CF6115D00A33D86 /* UIStackView+Helper.swift */; }; 3386546E2CE29AEC00AB16A9 /* PledgePaymentPlansOptionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3386546D2CE29AD500AB16A9 /* PledgePaymentPlansOptionViewModel.swift */; }; 338654752CE3D22600AB16A9 /* PledgePaymentPlansViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 338654732CE3D17200AB16A9 /* PledgePaymentPlansViewModelTest.swift */; }; + 33914D6D2D07D24500A67C47 /* UIView+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33914D6C2D07D23B00A67C47 /* UIView+Helper.swift */; }; 339ED5602CE41015004B301D /* PledgePaymentPlansViewControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339ED55E2CE40FC5004B301D /* PledgePaymentPlansViewControllerTest.swift */; }; 339ED5662CE43099004B301D /* PledgePaymentPlansOptionViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339ED5642CE41AFC004B301D /* PledgePaymentPlansOptionViewModelTest.swift */; }; 33C9F0BE2CF5104C00B62E14 /* PledgePaymentPlanOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33C9F0BC2CED6DEF00B62E14 /* PledgePaymentPlanOptionView.swift */; }; @@ -1926,6 +1927,7 @@ 3385CF002CF6115D00A33D86 /* UIStackView+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Helper.swift"; sourceTree = ""; }; 3386546D2CE29AD500AB16A9 /* PledgePaymentPlansOptionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgePaymentPlansOptionViewModel.swift; sourceTree = ""; }; 338654732CE3D17200AB16A9 /* PledgePaymentPlansViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgePaymentPlansViewModelTest.swift; sourceTree = ""; }; + 33914D6C2D07D23B00A67C47 /* UIView+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Helper.swift"; sourceTree = ""; }; 339ED55E2CE40FC5004B301D /* PledgePaymentPlansViewControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgePaymentPlansViewControllerTest.swift; sourceTree = ""; }; 339ED5642CE41AFC004B301D /* PledgePaymentPlansOptionViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgePaymentPlansOptionViewModelTest.swift; sourceTree = ""; }; 33C9F0BC2CED6DEF00B62E14 /* PledgePaymentPlanOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgePaymentPlanOptionView.swift; sourceTree = ""; }; @@ -5882,6 +5884,7 @@ 778F891A22D3E35600D095C5 /* Extensions */ = { isa = PBXGroup; children = ( + 33914D6C2D07D23B00A67C47 /* UIView+Helper.swift */, 3385CF002CF6115D00A33D86 /* UIStackView+Helper.swift */, 77C9122623C4F99400F3D2C9 /* Double+Currency.swift */, 77C9123023C637FD00F3D2C9 /* DoubleCurrencyTests.swift */, @@ -8082,6 +8085,7 @@ 8AC3E13A269F781D00168BF8 /* ErrorEnvelope+LocalizedDescription.swift in Sources */, D04AAC34218BB70D00CF713E /* SettingsAccountPickerCellViewModel.swift in Sources */, A75511691C8642C3005355CF /* UILabel+IBClear.swift in Sources */, + 33914D6D2D07D24500A67C47 /* UIView+Helper.swift in Sources */, 06BD75C926C431A000A12D4E /* CommentDialogViewModel.swift in Sources */, 064B007827A463D2007B21FE /* ImageViewElementCellViewModel.swift in Sources */, A7F441E51D005A9400FE6FC5 /* SearchViewModel.swift in Sources */, diff --git a/KsApi/GraphAPI.swift b/KsApi/GraphAPI.swift index af60a84d9e..fd6b39a6b5 100644 --- a/KsApi/GraphAPI.swift +++ b/KsApi/GraphAPI.swift @@ -4281,7 +4281,9 @@ public enum GraphAPI { case partialRefunds_2024 case notificationBannerUpdate_2024 case multipleShipfromLocations_2024 + case pledgeRedemptionUnifiedCreatorUi case reactBackedProjects + case copyAddons /// Auto generated constant for unknown enum values case __unknown(RawValue) @@ -4396,7 +4398,9 @@ public enum GraphAPI { case "partial_refunds_2024": self = .partialRefunds_2024 case "notification_banner_update_2024": self = .notificationBannerUpdate_2024 case "multiple_shipfrom_locations_2024": self = .multipleShipfromLocations_2024 + case "pledge_redemption_unified_creator_ui": self = .pledgeRedemptionUnifiedCreatorUi case "react_backed_projects": self = .reactBackedProjects + case "copy_addons": self = .copyAddons default: self = .__unknown(rawValue) } } @@ -4512,7 +4516,9 @@ public enum GraphAPI { case .partialRefunds_2024: return "partial_refunds_2024" case .notificationBannerUpdate_2024: return "notification_banner_update_2024" case .multipleShipfromLocations_2024: return "multiple_shipfrom_locations_2024" + case .pledgeRedemptionUnifiedCreatorUi: return "pledge_redemption_unified_creator_ui" case .reactBackedProjects: return "react_backed_projects" + case .copyAddons: return "copy_addons" case .__unknown(let value): return value } } @@ -4628,7 +4634,9 @@ public enum GraphAPI { case (.partialRefunds_2024, .partialRefunds_2024): return true case (.notificationBannerUpdate_2024, .notificationBannerUpdate_2024): return true case (.multipleShipfromLocations_2024, .multipleShipfromLocations_2024): return true + case (.pledgeRedemptionUnifiedCreatorUi, .pledgeRedemptionUnifiedCreatorUi): return true case (.reactBackedProjects, .reactBackedProjects): return true + case (.copyAddons, .copyAddons): return true case (.__unknown(let lhsValue), .__unknown(let rhsValue)): return lhsValue == rhsValue default: return false } @@ -4745,7 +4753,9 @@ public enum GraphAPI { .partialRefunds_2024, .notificationBannerUpdate_2024, .multipleShipfromLocations_2024, + .pledgeRedemptionUnifiedCreatorUi, .reactBackedProjects, + .copyAddons, ] } } diff --git a/Library/Extensions/UIView+Helper.swift b/Library/Extensions/UIView+Helper.swift new file mode 100644 index 0000000000..fa8aadaf7e --- /dev/null +++ b/Library/Extensions/UIView+Helper.swift @@ -0,0 +1,9 @@ +import UIKit + +extension UIView { + public func rounded(with cornerRadius: CGFloat = Styles.cornerRadius) { + self.clipsToBounds = true + self.layer.masksToBounds = true + self.layer.cornerRadius = cornerRadius + } +} diff --git a/Library/ViewModels/ConfirmDetailsViewModel.swift b/Library/ViewModels/ConfirmDetailsViewModel.swift index edd4f1c0ba..057d40fbfe 100644 --- a/Library/ViewModels/ConfirmDetailsViewModel.swift +++ b/Library/ViewModels/ConfirmDetailsViewModel.swift @@ -515,5 +515,5 @@ private func pledgeSummaryViewData( confirmationLabelHidden: Bool, pledgeHasNoReward: Bool ) -> PledgeSummaryViewData { - return (project, total, confirmationLabelHidden, pledgeHasNoReward) + return (project, total, confirmationLabelHidden, pledgeHasNoReward, nil) } diff --git a/Library/ViewModels/NoShippingPledgeViewModel.swift b/Library/ViewModels/NoShippingPledgeViewModel.swift index 599f4d0b09..852dba16e4 100644 --- a/Library/ViewModels/NoShippingPledgeViewModel.swift +++ b/Library/ViewModels/NoShippingPledgeViewModel.swift @@ -22,6 +22,7 @@ public protocol NoShippingPledgeViewModelInputs { paymentData: (displayName: String?, network: String?, transactionIdentifier: String) ) func paymentAuthorizationViewControllerDidFinish() + func paymentPlanSelected(_ paymentPlan: PledgePaymentPlansType) func pledgeAmountViewControllerDidUpdate(with data: PledgeAmountData) func pledgeDisclaimerViewDidTapLearnMore() func scaFlowCompleted(with result: StripePaymentHandlerActionStatusType, error: Error?) @@ -63,7 +64,7 @@ public protocol NoShippingPledgeViewModelOutputs { var showWebHelp: Signal { get } var title: Signal { get } var showPledgeOverTimeUI: Signal { get } - var pledgeOverTimeConfigData: Signal { get } + var pledgeOverTimeConfigData: Signal { get } } public protocol NoShippingPledgeViewModelType { @@ -93,20 +94,6 @@ public class NoShippingPledgeViewModel: NoShippingPledgeViewModelType, NoShippin let initialDataUnpacked = Signal.zip(project, baseReward, refTag, context) let backing = project.map { $0.personalization.backing }.skipNil() - self.showPledgeOverTimeUI = project.signal - .map { ($0.isPledgeOverTimeAllowed ?? false) && featurePledgeOverTimeEnabled() - } - - self.pledgeOverTimeConfigData = self.showPledgeOverTimeUI - .filter { showUI in showUI == true } - .combineLatest(with: project) - .map { _, project -> PledgePaymentPlansAndSelectionData in - PledgePaymentPlansAndSelectionData( - selectedPlan: .pledgeInFull, - increments: mockPledgePaymentIncrement(), - project: project - ) - } self.pledgeAmountViewHidden = context.map { $0.pledgeAmountViewHidden } self.pledgeAmountSummaryViewHidden = Signal.zip(baseReward, context).map { baseReward, context in @@ -285,30 +272,6 @@ public class NoShippingPledgeViewModel: NoShippingPledgeViewModelType, NoShippin shippingLocation.filter(isNil).mapConst(nil) ) - self.configurePledgeRewardsSummaryViewWithData = Signal.combineLatest( - initialData, - pledgeTotal, - additionalPledgeAmount, - shippingSummaryViewData, - rewards - ) - .compactMap { data, pledgeTotal, additionalPledgeAmount, shipping, rewards in - let rewardsData = PostCampaignRewardsSummaryViewData( - rewards: data.rewards, - selectedQuantities: data.selectedQuantities, - projectCountry: data.project.country, - omitCurrencyCode: data.project.stats.omitUSCurrencyCode, - shipping: shipping - ) - let pledgeData = PledgeSummaryViewData( - project: data.project, - total: pledgeTotal, - confirmationLabelHidden: false, - pledgeHasNoReward: pledgeHasNoRewards(rewards: rewards) - ) - return (rewardsData, additionalPledgeAmount, pledgeData) - } - self.configurePledgeAmountSummaryViewControllerWithData = Signal.combineLatest( projectAndReward, allRewardsTotal, @@ -998,6 +961,63 @@ public class NoShippingPledgeViewModel: NoShippingPledgeViewModelType, NoShippin refTag: refTag ) } + + // MARK: Pledge Over Time + + self.showPledgeOverTimeUI = project.signal + .map { ($0.isPledgeOverTimeAllowed ?? false) && featurePledgeOverTimeEnabled() } + + self.pledgeOverTimeConfigData = Signal.combineLatest( + self.showPledgeOverTimeUI, + project, + pledgeTotal, + self.paymentPlanSelectedSignal + ) + .map { showUI, project, pledgeTotal, planSelected -> PledgePaymentPlansAndSelectionData? in + guard showUI else { return nil } + // TODO: Temporary placeholder to simulate the ineligible state for plans. + // The `thresholdAmount` will be retrieved from the API in the future. + // See [MBL-1838](https://kickstarter.atlassian.net/browse/MBL-1838) for implementation details. + let thresholdAmount = 125.0 + let isIneligible = pledgeTotal < thresholdAmount + + return PledgePaymentPlansAndSelectionData( + selectedPlan: planSelected, + increments: mockPledgePaymentIncrement(), + ineligible: isIneligible, + project: project, + thresholdAmount: thresholdAmount + ) + } + + self.configurePledgeRewardsSummaryViewWithData = Signal.combineLatest( + initialData, + pledgeTotal, + additionalPledgeAmount, + shippingSummaryViewData, + rewards, + self.pledgeOverTimeConfigData + ) + .compactMap { data, pledgeTotal, additionalPledgeAmount, shipping, rewards, pledgeOverTimeData in + let rewardsData = PostCampaignRewardsSummaryViewData( + rewards: data.rewards, + selectedQuantities: data.selectedQuantities, + projectCountry: data.project.country, + omitCurrencyCode: data.project.stats.omitUSCurrencyCode, + shipping: shipping + ) + let pledgeData = PledgeSummaryViewData( + project: data.project, + total: pledgeTotal, + confirmationLabelHidden: false, + pledgeHasNoReward: pledgeHasNoRewards(rewards: rewards), + pledgeOverTimeData: pledgeOverTimeData + ) + return (rewardsData, additionalPledgeAmount, pledgeData) + } + + // Sending `.pledgeInFull` as default option + self.paymentPlanSelectedObserver.send(value: .pledgeInFull) } // MARK: - Inputs @@ -1037,6 +1057,12 @@ public class NoShippingPledgeViewModel: NoShippingPledgeViewModelType, NoShippin self.paymentAuthorizationDidFinishObserver.send(value: ()) } + private let (paymentPlanSelectedSignal, paymentPlanSelectedObserver) = Signal + .pipe() + public func paymentPlanSelected(_ paymentPlan: PledgePaymentPlansType) { + self.paymentPlanSelectedObserver.send(value: paymentPlan) + } + private let (goToLoginSignupSignal, goToLoginSignupObserver) = Signal.pipe() public func goToLoginSignupTapped() { self.goToLoginSignupObserver.send(value: ()) @@ -1122,7 +1148,7 @@ public class NoShippingPledgeViewModel: NoShippingPledgeViewModelType, NoShippin public let showWebHelp: Signal public let title: Signal public let showPledgeOverTimeUI: Signal - public var pledgeOverTimeConfigData: Signal + public var pledgeOverTimeConfigData: Signal public var inputs: NoShippingPledgeViewModelInputs { return self } public var outputs: NoShippingPledgeViewModelOutputs { return self } diff --git a/Library/ViewModels/NoShippingPledgeViewModelTests.swift b/Library/ViewModels/NoShippingPledgeViewModelTests.swift index 2acfc9471d..6f52460bd8 100644 --- a/Library/ViewModels/NoShippingPledgeViewModelTests.swift +++ b/Library/ViewModels/NoShippingPledgeViewModelTests.swift @@ -68,6 +68,8 @@ final class NoShippingPledgeViewModelTests: TestCase { private let showWebHelp = TestObserver() private let title = TestObserver() private let showPledgeOverTimeUI = TestObserver() + private let pledgeOverTimeData = TestObserver() + private let plotSelectedPlan = TestObserver() let shippingRule = ShippingRule.template |> ShippingRule.lens.location .~ (.template |> Location.lens.id .~ 55) @@ -149,6 +151,15 @@ final class NoShippingPledgeViewModelTests: TestCase { self.vm.outputs.title.observe(self.title.observer) self.vm.outputs.showPledgeOverTimeUI.observe(self.showPledgeOverTimeUI.observer) + self.vm.outputs + .pledgeOverTimeConfigData + .skipNil() + .observe(self.pledgeOverTimeData.observer) + + self.vm.outputs.pledgeOverTimeConfigData + .skipNil() + .map { $0.selectedPlan } + .observe(self.plotSelectedPlan.observer) } func testShowWebHelp() { @@ -5230,7 +5241,37 @@ final class NoShippingPledgeViewModelTests: TestCase { } } - func testShowPledgeOverTimeUI_True() { + func testShowPledgeOverTimeUI_False() { + let mockConfigClient = MockRemoteConfigClient() + mockConfigClient.features = [ + RemoteConfigFeature.pledgeOverTime.rawValue: false + ] + + withEnvironment(remoteConfigClient: mockConfigClient) { + let project = Project.template + |> Project.lens.isPledgeOverTimeAllowed .~ false + let reward = Reward.template + + let data = PledgeViewData( + project: project, + rewards: [reward], + selectedShippingRule: shippingRule, + selectedQuantities: [reward.id: 1], + selectedLocationId: nil, + refTag: .projectPage, + context: .pledge + ) + + self.vm.inputs.configure(with: data) + self.vm.inputs.viewDidLoad() + + self.showPledgeOverTimeUI.assertValues([false]) + self.pledgeOverTimeData.assertDidNotEmitValue() + self.plotSelectedPlan.assertDidNotEmitValue() + } + } + + func testPledgeOverTime_PledgeInFullSelected() { let mockConfigClient = MockRemoteConfigClient() mockConfigClient.features = [ RemoteConfigFeature.pledgeOverTime.rawValue: true @@ -5255,18 +5296,20 @@ final class NoShippingPledgeViewModelTests: TestCase { self.vm.inputs.viewDidLoad() self.showPledgeOverTimeUI.assertValues([true]) + self.pledgeOverTimeData.assertDidEmitValue() + self.plotSelectedPlan.assertValue(.pledgeInFull) } } - func testShowPledgeOverTimeUI_False() { + func testPledgeOverTime_PledgeOverTimeSelected() { let mockConfigClient = MockRemoteConfigClient() mockConfigClient.features = [ - RemoteConfigFeature.pledgeOverTime.rawValue: false + RemoteConfigFeature.pledgeOverTime.rawValue: true ] withEnvironment(remoteConfigClient: mockConfigClient) { let project = Project.template - |> Project.lens.isPledgeOverTimeAllowed .~ false + |> Project.lens.isPledgeOverTimeAllowed .~ true let reward = Reward.template let data = PledgeViewData( @@ -5281,8 +5324,11 @@ final class NoShippingPledgeViewModelTests: TestCase { self.vm.inputs.configure(with: data) self.vm.inputs.viewDidLoad() + self.vm.inputs.paymentPlanSelected(.pledgeOverTime) - self.showPledgeOverTimeUI.assertValues([false]) + self.showPledgeOverTimeUI.assertValues([true]) + self.pledgeOverTimeData.assertDidEmitValue() + self.plotSelectedPlan.assertValues([.pledgeInFull, .pledgeOverTime]) } } } diff --git a/Library/ViewModels/NoShippingPostCampaignCheckoutViewModel.swift b/Library/ViewModels/NoShippingPostCampaignCheckoutViewModel.swift index dec62850d4..e096c4dc56 100644 --- a/Library/ViewModels/NoShippingPostCampaignCheckoutViewModel.swift +++ b/Library/ViewModels/NoShippingPostCampaignCheckoutViewModel.swift @@ -238,7 +238,8 @@ public class NoShippingPostCampaignCheckoutViewModel: NoShippingPostCampaignChec project: data.project, total: pledgeTotal, confirmationLabelHidden: true, - pledgeHasNoReward: pledgeHasNoRewards(rewards: rewardsData.rewards) + pledgeHasNoReward: pledgeHasNoRewards(rewards: rewardsData.rewards), + pledgeOverTimeData: nil ) return (rewardsData, bonus, pledgeData) } diff --git a/Library/ViewModels/PledgePaymentPlansOptionViewModel.swift b/Library/ViewModels/PledgePaymentPlansOptionViewModel.swift index 8d978628ba..6b1a63ca38 100644 --- a/Library/ViewModels/PledgePaymentPlansOptionViewModel.swift +++ b/Library/ViewModels/PledgePaymentPlansOptionViewModel.swift @@ -3,22 +3,28 @@ import KsApi import ReactiveSwift public struct PledgePaymentPlanOptionData: Equatable { - public let type: PledgePaymentPlansType + public var ineligible: Bool = false + public var type: PledgePaymentPlansType public var selectedType: PledgePaymentPlansType // TODO: replece with API model in [MBL-1838](https://kickstarter.atlassian.net/browse/MBL-1838) - public let paymentIncrements: [PledgePaymentIncrement] - public let project: Project + public var paymentIncrements: [PledgePaymentIncrement] + public var project: Project + public var thresholdAmount: Double public init( + ineligible: Bool, type: PledgePaymentPlansType, selectedType: PledgePaymentPlansType, paymentIncrements: [PledgePaymentIncrement], - project: Project + project: Project, + thresholdAmount: Double ) { + self.ineligible = ineligible self.type = type self.selectedType = selectedType self.paymentIncrements = paymentIncrements self.project = project + self.thresholdAmount = thresholdAmount } } @@ -50,9 +56,12 @@ public protocol PledgePaymentPlansOptionViewModelOutputs { var subtitleText: Signal { get } var subtitleLabelHidden: Signal { get } var selectionIndicatorImageName: Signal { get } + var ineligibleBadgeHidden: Signal { get } + var ineligibleBadgeText: Signal { get } var notifyDelegatePaymentPlanOptionSelected: Signal { get } var notifyDelegateTermsOfUseTapped: Signal { get } var termsOfUseButtonHidden: Signal { get } + var optionViewEnabled: Signal { get } var paymentIncrementsHidden: Signal { get } var paymentIncrements: Signal<[PledgePaymentIncrementFormatted], Never> { get } } @@ -69,6 +78,8 @@ public final class PledgePaymentPlansOptionViewModel: public init() { let configData = self.configData.signal.skipNil() + let ineligible: Signal = configData.map { $0.type == .pledgeOverTime && $0.ineligible } + self.selectionIndicatorImageName = configData .map { $0.selectedType == $0.type ? @@ -78,14 +89,18 @@ public final class PledgePaymentPlansOptionViewModel: self.titleText = configData.map { getTitleText(by: $0.type) } self.subtitleText = configData .map { getSubtitleText(by: $0.type, isSelected: $0.selectedType == $0.type) } - self.subtitleLabelHidden = self.subtitleText.map { $0.isEmpty } + self.subtitleLabelHidden = self.subtitleText + .combineLatest(with: ineligible) + .map { subtitle, ineligible in + ineligible || subtitle.isEmpty + } self.notifyDelegatePaymentPlanOptionSelected = self.optionTappedProperty .signal .withLatest(from: configData) .map { $1.type } - let isPledgeOverTimeAndSelected = configData.map { + let isPledgeOverTimeAndSelected: Signal = configData.map { $0.type == .pledgeOverTime && $0.type == $0.selectedType } @@ -94,7 +109,7 @@ public final class PledgePaymentPlansOptionViewModel: self.paymentIncrementsHidden = isPledgeOverTimeAndSelected.negate() self.paymentIncrements = configData - .filter { $0.type == .pledgeOverTime && $0.selectedType == $0.type } + .filter { !$0.ineligible && $0.type == .pledgeOverTime && $0.selectedType == $0.type } .map { data in data.paymentIncrements .enumerated() @@ -105,7 +120,15 @@ public final class PledgePaymentPlansOptionViewModel: .filter { !$0.isEmpty } .take(first: 1) + self.ineligibleBadgeHidden = ineligible.negate() + + self.optionViewEnabled = self.ineligibleBadgeHidden + self.notifyDelegateTermsOfUseTapped = self.termsOfUseTappedProperty.signal.skipNil() + + self.ineligibleBadgeText = configData + .filterWhenLatestFrom(ineligible, satisfies: { $0 == true }) + .map { getIneligibleBadgeText(with: $0.project, thresholdAmount: $0.thresholdAmount) } } fileprivate let configData = MutableProperty(nil) @@ -131,9 +154,12 @@ public final class PledgePaymentPlansOptionViewModel: public var titleText: ReactiveSwift.Signal public var subtitleText: ReactiveSwift.Signal public var subtitleLabelHidden: Signal + public var ineligibleBadgeHidden: Signal + public var ineligibleBadgeText: Signal public var notifyDelegatePaymentPlanOptionSelected: Signal public var notifyDelegateTermsOfUseTapped: Signal public var termsOfUseButtonHidden: Signal + public var optionViewEnabled: Signal public var paymentIncrementsHidden: Signal public var paymentIncrements: Signal<[PledgePaymentIncrementFormatted], Never> @@ -178,6 +204,18 @@ private func getDateFormatted(_ timeStamp: TimeInterval) -> String { ) } +private func getIneligibleBadgeText(with project: Project, thresholdAmount: Double) -> String { + let projectCurrencyCountry = projectCountry(forCurrency: project.stats.currency) ?? project.country + let thresholdAmountFormatted = Format.currency( + thresholdAmount, + country: projectCurrencyCountry, + omitCurrencyCode: project.stats.omitUSCurrencyCode + ) + + // TODO: add strings translations [MBL-1860](https://kickstarter.atlassian.net/browse/MBL-1860) + return "Available for pledges over \(thresholdAmountFormatted)" +} + extension PledgePaymentIncrementFormatted { init(from increment: PledgePaymentIncrement, index: Int, project: Project) { let projectCurrencyCountry = projectCountry(forCurrency: project.stats.currency) ?? project.country diff --git a/Library/ViewModels/PledgePaymentPlansOptionViewModelTest.swift b/Library/ViewModels/PledgePaymentPlansOptionViewModelTest.swift index b069e5d189..eb78bcc693 100644 --- a/Library/ViewModels/PledgePaymentPlansOptionViewModelTest.swift +++ b/Library/ViewModels/PledgePaymentPlansOptionViewModelTest.swift @@ -18,9 +18,12 @@ final class PledgePaymentPlansOptionViewModelTest: TestCase { private var paymentIncrementsHidden = TestObserver() private var termsOfUseButtonHidden = TestObserver() private var paymentIncrements = TestObserver<[PledgePaymentIncrementFormatted], Never>() + private var ineligibleBadgeHidden = TestObserver() + private var ineligibleBadgeText = TestObserver() // MARK: Const + // TODO: add strings translations [MBL-1860](https://kickstarter.atlassian.net/browse/MBL-1860) private let pledgeInFullTitle = "Pledge in full" private let pledgeOverTimeTitle = "Pledge Over Time" private let pledgeOverTimeSubtitle = @@ -29,6 +32,7 @@ final class PledgePaymentPlansOptionViewModelTest: TestCase { "You will be charged for your pledge over four payments, at no extra cost.\n\nThe first charge will be 24 hours after the project ends successfully, then every 2 weeks until fully paid. When this option is selected no further edits can be made to your pledge." private let selectedImageName = "icon-payment-method-selected" private let unselectedImageName = "icon-payment-method-unselected" + private let thresholdAmount = 125.0 // MARK: Lifecycle @@ -44,16 +48,20 @@ final class PledgePaymentPlansOptionViewModelTest: TestCase { self.vm.outputs.paymentIncrementsHidden.observe(self.paymentIncrementsHidden.observer) self.vm.outputs.termsOfUseButtonHidden.observe(self.termsOfUseButtonHidden.observer) self.vm.outputs.paymentIncrements.observe(self.paymentIncrements.observer) + self.vm.outputs.ineligibleBadgeHidden.observe(self.ineligibleBadgeHidden.observer) + self.vm.outputs.ineligibleBadgeText.observe(self.ineligibleBadgeText.observer) } // MARK: Test cases func testPaymentPlanOption_PledgeinFull_Selected() { let data = PledgePaymentPlanOptionData( + ineligible: false, type: .pledgeInFull, selectedType: .pledgeInFull, paymentIncrements: mockPaymentIncrements(), - project: Project.template + project: Project.template, + thresholdAmount: self.thresholdAmount ) self.vm.inputs.configureWith(data: data) @@ -63,14 +71,18 @@ final class PledgePaymentPlansOptionViewModelTest: TestCase { self.paymentIncrementsHidden.assertValue(true) self.selectionIndicatorImageName.assertValue(self.selectedImageName) self.paymentIncrements.assertValues([]) + self.ineligibleBadgeHidden.assertValue(true) + self.ineligibleBadgeText.assertDidNotEmitValue() } func testPaymentPlanOption_PledgeinFull_Unselected() { let data = PledgePaymentPlanOptionData( + ineligible: false, type: .pledgeInFull, selectedType: .pledgeOverTime, paymentIncrements: mockPaymentIncrements(), - project: Project.template + project: Project.template, + thresholdAmount: self.thresholdAmount ) self.vm.inputs.configureWith(data: data) @@ -81,6 +93,8 @@ final class PledgePaymentPlansOptionViewModelTest: TestCase { self.paymentIncrementsHidden.assertValue(true) self.selectionIndicatorImageName.assertValue(self.unselectedImageName) self.paymentIncrements.assertValues([]) + self.ineligibleBadgeHidden.assertValue(true) + self.ineligibleBadgeText.assertDidNotEmitValue() } func testPaymentPlanOption_PledgeOverTime_Selected() { @@ -88,10 +102,12 @@ final class PledgePaymentPlansOptionViewModelTest: TestCase { let project = Project.template let incrementsFormatted = paymentIncrementsFormatted(from: increments, project: project) let data = PledgePaymentPlanOptionData( + ineligible: false, type: .pledgeOverTime, selectedType: .pledgeOverTime, paymentIncrements: increments, - project: project + project: project, + thresholdAmount: self.thresholdAmount ) self.vm.inputs.configureWith(data: data) @@ -104,14 +120,18 @@ final class PledgePaymentPlansOptionViewModelTest: TestCase { self.selectionIndicatorImageName.assertValue(self.selectedImageName) self.paymentIncrements.assertValues([incrementsFormatted]) + self.ineligibleBadgeHidden.assertValue(true) + self.ineligibleBadgeText.assertDidNotEmitValue() } func testPaymentPlanOption_PledgeOverTime_Unselected() { let data = PledgePaymentPlanOptionData( + ineligible: false, type: .pledgeOverTime, selectedType: .pledgeInFull, paymentIncrements: mockPaymentIncrements(), - project: Project.template + project: Project.template, + thresholdAmount: self.thresholdAmount ) self.vm.inputs.configureWith(data: data) @@ -122,22 +142,47 @@ final class PledgePaymentPlansOptionViewModelTest: TestCase { self.paymentIncrementsHidden.assertValue(true) self.selectionIndicatorImageName.assertValue(self.unselectedImageName) self.paymentIncrements.assertValues([]) + self.ineligibleBadgeHidden.assertValue(true) + self.ineligibleBadgeText.assertDidNotEmitValue() } func testPaymentPlanOption_OptionTapped() { let data = PledgePaymentPlanOptionData( + ineligible: false, type: .pledgeOverTime, selectedType: .pledgeInFull, paymentIncrements: mockPaymentIncrements(), - project: Project.template + project: Project.template, + thresholdAmount: self.thresholdAmount ) self.vm.inputs.configureWith(data: data) self.vm.inputs.optionTapped() self.notifyDelegatePaymentPlanOptionSelected.assertValue(.pledgeOverTime) } + + func testPaymentPlanOption_PledgeOverTime_Ineligible() { + let project = Project.template + // TODO: add strings translations [MBL-1860](https://kickstarter.atlassian.net/browse/MBL-1860) + let ineligibleText = "Available for pledges over $125" + + let data = PledgePaymentPlanOptionData( + ineligible: true, + type: .pledgeOverTime, + selectedType: .pledgeInFull, + paymentIncrements: [], + project: project, + thresholdAmount: self.thresholdAmount + ) + + self.vm.inputs.configureWith(data: data) + + self.titleText.assertValue(self.pledgeOverTimeTitle) + self.ineligibleBadgeHidden.assertValue(false) + self.ineligibleBadgeText.assertValue(ineligibleText) + } } -private func mockPaymentIncrements() -> [PledgePaymentIncrement] { +internal func mockPaymentIncrements() -> [PledgePaymentIncrement] { let amount = PledgePaymentIncrementAmount(amount: 250.0, currency: "USD") let scheduledCollection = TimeInterval(1_553_731_200) return [ diff --git a/Library/ViewModels/PledgePaymentPlansViewModel.swift b/Library/ViewModels/PledgePaymentPlansViewModel.swift index 91458c365b..7e9f840101 100644 --- a/Library/ViewModels/PledgePaymentPlansViewModel.swift +++ b/Library/ViewModels/PledgePaymentPlansViewModel.swift @@ -9,21 +9,28 @@ public enum PledgePaymentPlansType: Equatable { } public struct PledgePaymentPlansAndSelectionData { - public var selectedPlan: PledgePaymentPlansType + public var ineligible: Bool public var paymentIncrements: [PledgePaymentIncrement] public var project: Project - /* TODO: add the necesary properties for the next states (PLOT Selected and Ineligible) - - [MBL-1816](https://kickstarter.atlassian.net/browse/MBL-1816) - */ + public var selectedPlan: PledgePaymentPlansType + public var thresholdAmount: Double public init( selectedPlan: PledgePaymentPlansType, increments paymentIncrements: [PledgePaymentIncrement] = [], - project: Project + ineligible: Bool = false, + project: Project, + thresholdAmount: Double ) { - self.selectedPlan = selectedPlan + self.ineligible = ineligible self.paymentIncrements = paymentIncrements self.project = project + self.selectedPlan = selectedPlan + self.thresholdAmount = thresholdAmount + } + + public var isPledgeOverTime: Bool { + self.selectedPlan == .pledgeOverTime } } @@ -80,7 +87,9 @@ public final class PledgePaymentPlansViewModel: PledgePaymentPlansViewModelType, PledgePaymentPlansAndSelectionData( selectedPlan: selectedPlan, increments: data.paymentIncrements, - project: data.project + ineligible: data.ineligible, + project: data.project, + thresholdAmount: data.thresholdAmount ) } diff --git a/Library/ViewModels/PledgePaymentPlansViewModelTest.swift b/Library/ViewModels/PledgePaymentPlansViewModelTest.swift index 7159161754..f0a82367a4 100644 --- a/Library/ViewModels/PledgePaymentPlansViewModelTest.swift +++ b/Library/ViewModels/PledgePaymentPlansViewModelTest.swift @@ -16,7 +16,8 @@ final class PledgePaymentPlansViewModelTests: TestCase { private let selectionData = PledgePaymentPlansAndSelectionData( selectedPlan: .pledgeInFull, - project: Project.template + project: Project.template, + thresholdAmount: 125.0 ) // MARK: Lifecycle diff --git a/Library/ViewModels/PledgeSummaryViewModel.swift b/Library/ViewModels/PledgeSummaryViewModel.swift index 98758c942e..153ec214f8 100644 --- a/Library/ViewModels/PledgeSummaryViewModel.swift +++ b/Library/ViewModels/PledgeSummaryViewModel.swift @@ -3,11 +3,16 @@ import Prelude import ReactiveSwift import UIKit +private enum Constants { + public static let dateFormat = "MMMM d, yyyy" +} + public typealias PledgeSummaryViewData = ( project: Project, total: Double, confirmationLabelHidden: Bool, - pledgeHasNoReward: Bool? + pledgeHasNoReward: Bool?, + pledgeOverTimeData: PledgePaymentPlansAndSelectionData? ) public protocol PledgeSummaryViewModelInputs { @@ -21,6 +26,8 @@ public protocol PledgeSummaryViewModelOutputs { var confirmationLabelAttributedText: Signal { get } var confirmationLabelHidden: Signal { get } var notifyDelegateOpenHelpType: Signal { get } + var pledgeOverTimeStackViewHidden: Signal { get } + var pledgeOverTimeChargesText: Signal { get } var totalConversionLabelText: Signal { get } var titleLabelText: Signal { get } } @@ -40,10 +47,15 @@ public class PledgeSummaryViewModel: PledgeSummaryViewModelType, .map(first) let projectAndPledgeTotal = initialData - .map { project, total, _, _ in (project, total) } + .map { project, total, _, _, _ in (project, total) } let pledgeHasNoReward = initialData - .map { _, _, _, pledgeHasNoReward in pledgeHasNoReward } + .map { _, _, _, pledgeHasNoReward, _ in pledgeHasNoReward } + + let pledgeOverTimeData = initialData + .map { _, _, _, _, pledgeOverTimeData in + pledgeOverTimeData + } self.amountLabelAttributedText = projectAndPledgeTotal .map(attributedCurrency(with:total:)) @@ -85,13 +97,9 @@ public class PledgeSummaryViewModel: PledgeSummaryViewModelType, : Strings.Pledge_amount() } - self.confirmationLabelAttributedText = projectAndPledgeTotal - .map { project, pledgeTotal in - attributedConfirmationString( - with: project, - pledgeTotal: pledgeTotal - ) - } + self.confirmationLabelAttributedText = initialData.map { data in + attributedConfirmationString(with: data) + } let project = initialData.map(\.project) @@ -103,6 +111,12 @@ public class PledgeSummaryViewModel: PledgeSummaryViewModelType, return true } + + self.pledgeOverTimeStackViewHidden = pledgeOverTimeData.map { $0?.isPledgeOverTime ?? false }.negate() + + // TODO: add strings translations [MBL-1860](https://kickstarter.atlassian.net/browse/MBL-1860) + self.pledgeOverTimeChargesText = pledgeOverTimeData.skipNil() + .map { "charged as \($0.paymentIncrements.count) payments" } } private let configureWithDataProperty = MutableProperty(nil) @@ -124,6 +138,8 @@ public class PledgeSummaryViewModel: PledgeSummaryViewModelType, public let confirmationLabelAttributedText: Signal public let confirmationLabelHidden: Signal public let notifyDelegateOpenHelpType: Signal + public let pledgeOverTimeStackViewHidden: Signal + public let pledgeOverTimeChargesText: Signal public let totalConversionLabelText: Signal public let titleLabelText: Signal @@ -149,7 +165,7 @@ private func attributedConfirmationString(with project: Project, pledgeTotal: Do var date = "" if let deadline = project.dates.deadline { - date = Format.date(secondsInUTC: deadline, template: "MMMM d, yyyy") + date = Format.date(secondsInUTC: deadline, template: Constants.dateFormat) } let projectCurrencyCountry = projectCountry(forCurrency: project.stats.currency) ?? project.country @@ -167,3 +183,35 @@ private func attributedConfirmationString(with project: Project, pledgeTotal: Do with: font, foregroundColor: foregroundColor, attributes: [:], bolding: [pledgeTotal, date] ) } + +private func attributedConfirmationPledgeOverTimeString( + with project: Project, + increments: [PledgePaymentIncrement] +) -> NSAttributedString { + guard let firstIncrement = increments.first else { return NSAttributedString() } + + let date = Format.date(secondsInUTC: firstIncrement.scheduledCollection, template: Constants.dateFormat) + + let projectCurrencyCountry = projectCountry(forCurrency: project.stats.currency) ?? project.country + let chargeAmount = Format.currency(firstIncrement.amount.amount, country: projectCurrencyCountry) + + let font = UIFont.ksr_caption1() + let foregroundColor = UIColor.ksr_support_400 + + // TODO: add strings translations [MBL-1860](https://kickstarter.atlassian.net/browse/MBL-1860) + return "If the project reaches its funding goal, the first charge of \(chargeAmount) will be collected on \(date)." + .attributed( + with: font, foregroundColor: foregroundColor, attributes: [:], bolding: [chargeAmount, date] + ) +} + +private func attributedConfirmationString(with data: PledgeSummaryViewData) -> NSAttributedString { + if let plotData = data.pledgeOverTimeData, plotData.isPledgeOverTime { + return attributedConfirmationPledgeOverTimeString( + with: data.project, + increments: plotData.paymentIncrements + ) + } + + return attributedConfirmationString(with: data.project, pledgeTotal: data.total) +} diff --git a/Library/ViewModels/PledgeSummaryViewModelTests.swift b/Library/ViewModels/PledgeSummaryViewModelTests.swift index 468ddf3b72..4e7c6d32df 100644 --- a/Library/ViewModels/PledgeSummaryViewModelTests.swift +++ b/Library/ViewModels/PledgeSummaryViewModelTests.swift @@ -14,6 +14,8 @@ internal final class PledgeSummaryViewModelTests: TestCase { private let confirmationLabelText = TestObserver() private let confirmationLabelHidden = TestObserver() private let notifyDelegateOpenHelpType = TestObserver() + private var pledgeOverTimeStackViewHidden = TestObserver() + private var pledgeOverTimeChargesText = TestObserver() private let totalConversionLabelText = TestObserver() override func setUp() { @@ -27,6 +29,8 @@ internal final class PledgeSummaryViewModelTests: TestCase { self.vm.outputs.confirmationLabelAttributedText.map { $0.string } .observe(self.confirmationLabelText.observer) self.vm.outputs.confirmationLabelHidden.observe(self.confirmationLabelHidden.observer) + self.vm.outputs.pledgeOverTimeStackViewHidden.observe(self.pledgeOverTimeStackViewHidden.observer) + self.vm.outputs.pledgeOverTimeChargesText.observe(self.pledgeOverTimeChargesText.observer) self.vm.outputs.totalConversionLabelText.observe(self.totalConversionLabelText.observer) } @@ -46,7 +50,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { |> Project.lens.stats.currency .~ Project.Country.us.currencyCode |> Project.lens.country .~ Project.Country.us - self.vm.inputs.configure(with: (project, total: 30, false, false)) + self.vm.inputs.configure(with: (project, total: 30, false, false, nil)) self.vm.inputs.viewDidLoad() self.amountLabelText.assertValues(["$30.00"], "Total is added to reward minimum") @@ -57,7 +61,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { |> Project.lens.stats.currency .~ Project.Country.mx.currencyCode |> Project.lens.country .~ Project.Country.us - self.vm.inputs.configure(with: (project, total: 30, false, false)) + self.vm.inputs.configure(with: (project, total: 30, false, false, nil)) self.vm.inputs.viewDidLoad() self.amountLabelText.assertValues([" MX$ 30.00"], "Total is added to reward minimum") @@ -67,7 +71,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { let project = Project.template |> Project.lens.stats.currency .~ Project.Country.us.currencyCode |> Project.lens.country .~ Project.Country.us - self.vm.inputs.configure(with: (project, total: 10, false, true)) + self.vm.inputs.configure(with: (project, total: 10, false, true, nil)) self.vm.inputs.viewDidLoad() @@ -78,7 +82,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { let project = Project.template |> Project.lens.stats.currency .~ Project.Country.mx.currencyCode |> Project.lens.country .~ Project.Country.us - let pledgeSummaryViewData = PledgeSummaryViewData(project, total: 10, false, true) + let pledgeSummaryViewData = PledgeSummaryViewData(project, total: 10, false, true, nil) self.vm.inputs.configure(with: pledgeSummaryViewData) @@ -93,7 +97,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { |> Project.lens.stats.currentCurrency .~ Project.Country.gb.currencyCode |> Project.lens.stats.currentCurrencyRate .~ 2.0 - self.vm.inputs.configure(with: (project, total: 10, false, true)) + self.vm.inputs.configure(with: (project, total: 10, false, true, nil)) self.vm.inputs.viewDidLoad() self.totalConversionLabelText.assertValues(["About £20.00"]) @@ -105,7 +109,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { |> Project.lens.stats.currentCurrency .~ Project.Country.gb.currencyCode |> Project.lens.stats.currentCurrencyRate .~ 2.0 - self.vm.inputs.configure(with: (project, total: 20, false, false)) + self.vm.inputs.configure(with: (project, total: 20, false, false, nil)) self.vm.inputs.viewDidLoad() self.totalConversionLabelText.assertValues(["About £40.00"]) @@ -117,7 +121,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { |> Project.lens.stats.currentCurrency .~ nil |> Project.lens.stats.currentCurrencyRate .~ nil - self.vm.inputs.configure(with: (project, total: 10, false, false)) + self.vm.inputs.configure(with: (project, total: 10, false, false, nil)) self.vm.inputs.viewDidLoad() self.totalConversionLabelText.assertDidNotEmitValue() @@ -141,7 +145,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { |> Project.lens.stats.currentCurrency .~ Currency.USD.rawValue |> Project.lens.stats.currency .~ Currency.USD.rawValue - self.vm.inputs.configure(with: (project: project, total: 10, false, false)) + self.vm.inputs.configure(with: (project: project, total: 10, false, false, nil)) self.vm.inputs.viewDidLoad() self.confirmationLabelHidden.assertValues([false]) @@ -170,7 +174,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { |> Project.lens.stats.currentCurrency .~ Currency.USD.rawValue |> Project.lens.stats.currency .~ Currency.USD.rawValue - self.vm.inputs.configure(with: (project: project, total: 10, true, false)) + self.vm.inputs.configure(with: (project: project, total: 10, true, false, nil)) self.vm.inputs.viewDidLoad() self.confirmationLabelHidden.assertValues([true]) @@ -200,7 +204,7 @@ internal final class PledgeSummaryViewModelTests: TestCase { |> Project.lens.stats.currency .~ Currency.HKD.rawValue |> Project.lens.country .~ .us - self.vm.inputs.configure(with: (project: project, total: 10, false, false)) + self.vm.inputs.configure(with: (project: project, total: 10, false, false, nil)) self.vm.inputs.viewDidLoad() self.confirmationLabelHidden.assertValues([false]) @@ -210,4 +214,70 @@ internal final class PledgeSummaryViewModelTests: TestCase { ]) } } + + func testPledgeOverTime_PledgeInFull() { + let dateComponents = DateComponents() + |> \.month .~ 11 + |> \.day .~ 1 + |> \.year .~ 2_019 + |> \.timeZone .~ TimeZone.init(secondsFromGMT: 0) + + let calendar = Calendar(identifier: .gregorian) + |> \.timeZone .~ TimeZone.init(secondsFromGMT: 0)! + + withEnvironment(calendar: calendar, locale: Locale(identifier: "en")) { + let date = AppEnvironment.current.calendar.date(from: dateComponents) + + let project = Project.template + |> Project.lens.dates.deadline .~ date!.timeIntervalSince1970 + |> Project.lens.stats.currentCurrency .~ Currency.USD.rawValue + |> Project.lens.stats.currency .~ Currency.USD.rawValue + + let plotData = PledgePaymentPlansAndSelectionData( + selectedPlan: .pledgeInFull, + increments: mockPaymentIncrements(), + ineligible: false, + project: project, + thresholdAmount: 125.0 // The value is arbitrary and does not impact this test case logic. + ) + + self.vm.inputs.configure(with: (project: project, total: 10, false, false, plotData)) + self.vm.inputs.viewDidLoad() + + self.confirmationLabelHidden.assertValues([false]) + self.confirmationLabelAttributedText.assertValueCount(1) + self.pledgeOverTimeStackViewHidden.assertValue(true) + self.pledgeOverTimeChargesText.assertDidEmitValue() + self.confirmationLabelText.assertValues([ + "If the project reaches its funding goal, you will be charged $10 on November 1, 2019. You will receive a proof of pledge that will be redeemable if the project is funded and the creator is successful at completing the creative venture." + ]) + } + } + + func testPledgeOverTime_PledgeOverTime() { + withEnvironment(locale: Locale(identifier: "en")) { + let project = Project.template + |> Project.lens.stats.currentCurrency .~ Currency.USD.rawValue + |> Project.lens.stats.currency .~ Currency.USD.rawValue + + let plotData = PledgePaymentPlansAndSelectionData( + selectedPlan: .pledgeOverTime, + increments: mockPaymentIncrements(), + ineligible: false, + project: project, + thresholdAmount: 125.0 // The value is arbitrary and does not impact this test case logic. + ) + + self.vm.inputs.configure(with: (project: project, total: 10, false, false, plotData)) + self.vm.inputs.viewDidLoad() + + self.confirmationLabelHidden.assertValues([false]) + self.confirmationLabelAttributedText.assertValueCount(1) + self.pledgeOverTimeStackViewHidden.assertValue(false) + self.pledgeOverTimeChargesText.assertValue("charged as 2 payments") + self.confirmationLabelText.assertValues([ + "If the project reaches its funding goal, the first charge of $250 will be collected on March 28, 2019." + ]) + } + } } diff --git a/Library/ViewModels/PledgeViewModel.swift b/Library/ViewModels/PledgeViewModel.swift index 3656dc672a..182131500b 100644 --- a/Library/ViewModels/PledgeViewModel.swift +++ b/Library/ViewModels/PledgeViewModel.swift @@ -1324,7 +1324,7 @@ private func pledgeSummaryViewData( confirmationLabelHidden: Bool, pledgeHasNoReward: Bool ) -> PledgeSummaryViewData { - return (project, total, confirmationLabelHidden, pledgeHasNoReward) + return (project, total, confirmationLabelHidden, pledgeHasNoReward, nil) } private func pledgeAmountSummaryViewData( diff --git a/Library/ViewModels/PostCampaignCheckoutViewModel.swift b/Library/ViewModels/PostCampaignCheckoutViewModel.swift index c5a0760d8d..dc142e8c9c 100644 --- a/Library/ViewModels/PostCampaignCheckoutViewModel.swift +++ b/Library/ViewModels/PostCampaignCheckoutViewModel.swift @@ -119,7 +119,8 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType, project: data.project, total: data.total, confirmationLabelHidden: true, - pledgeHasNoReward: pledgeHasNoRewards(rewards: rewardsData.rewards) + pledgeHasNoReward: pledgeHasNoRewards(rewards: rewardsData.rewards), + pledgeOverTimeData: nil ) return (rewardsData, data.bonusAmount, pledgeData) } diff --git a/Library/ViewModels/PostCampaignPledgeRewardsSummaryViewModelTests.swift b/Library/ViewModels/PostCampaignPledgeRewardsSummaryViewModelTests.swift index 52bf17cf8c..b7517d0f18 100644 --- a/Library/ViewModels/PostCampaignPledgeRewardsSummaryViewModelTests.swift +++ b/Library/ViewModels/PostCampaignPledgeRewardsSummaryViewModelTests.swift @@ -55,7 +55,8 @@ final class PostCampaignPledgeRewardsSummaryViewModelTests: TestCase { project: Project.template, total: total, confirmationLabelHidden: true, - pledgeHasNoReward: false + pledgeHasNoReward: false, + pledgeOverTimeData: nil ) self.vm.inputs.configureWith(