Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MBL-1815] PLOT plan selector - selected state #2213

Merged
merged 11 commits into from
Dec 17, 2024

Conversation

jovaniks
Copy link
Contributor

@jovaniks jovaniks commented Dec 5, 2024

📲 What

This PR implements the "Pledge Over Time" (PLOT) selected state in the Payment Plan Selector. It allows backers to choose the PLOT option and view the corresponding payment increments, including charge dates and amounts.

🤔 Why

The PLOT payment option enables backers to split their pledge amount into four equal payments, improving affordability and flexibility. This change is needed to meet the requirements for the PLOT initiative and ensure proper functionality within the payment plan selector.

🛠 How

  • Added a selectable "Pledge Over Time" (PLOT) option in the Payment Plan Selector.
  • Implemented logic to display mock payment increment records:
    • Included charge dates (PledgePaymentIncrement).
    • Displayed amounts for each payment (PledgePaymentIncrement.amount).
  • Payment increments are displayed in the their own currency.
  • Adapted the UI to follow the provided design:
    • Added explanatory text below the PLOT option.
    • Included Terms of Use link.
  • Mock data was used as the API is not yet ready.

👀 See

Simulator Screen Recording - iPhone SE (3rd generation) - 2024-12-05 at 09 39 41

✅ Acceptance criteria

  • All tests pass
  • Confirm that payment increments (dates and amounts) are displayed correctly.
  • Verify the Terms of Use link functions as expected.
  • Verify the full description is displayed when selected.

⏰ TODO

  • Connect the component to the real API once it becomes available to replace mock data.

@jovaniks jovaniks self-assigned this Dec 5, 2024
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spacing issue shown here is due to the test locking the layout to a specific screen size, which might not fully represent the component's dynamic behavior. In actual implementation, this component adapts its size based on the content and available screen dimensions, so the extra space seen in the test is expected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you link the ticket you created here for reference? We should update how we use snapshot tests if we make a habit of testing individual views instead of the entire screen. (I'm a big fan of testing individual views, for the record; we just don't do that currently)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven’t created a ticket for this yet, but I can definitely do so to track this issue and the potential update to how we approach snapshot testing. Let me know if that works!

self.vm.inputs.viewDidLoad()

self.vm.inputs.configure(with: self.selectionData)
self.vm.inputs.didTapTermsOfUse(with: .terms)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scottkicks I checked the PLOT web version and they are showing the current Terms of use web page.

@jovaniks jovaniks requested a review from scottkicks December 9, 2024 20:57

if let firstDateLabel = dateLabels.first {
dateLabels.forEach { label in
label.widthAnchor.constraint(equalTo: firstDateLabel.widthAnchor).isActive = true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intervals dates/amount alignment issue fixe

Explaining the issue: when the increments table mixed dates with one-digit days (4 Jan 2025) and two-digit days (14 Feb 2025), the amountLabel was misaligned. To fix it, a width constraint (all equal width) was added to each dateLabel.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this as a code comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, added!

Copy link
Contributor

@scottkicks scottkicks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Just some clean up comments for you to consider.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there may be some extra spacing after "Charge 1". I know we were having issues without iPad snapshots for this previously. If it's the same issue here, that's fine for now. I'd like to fix that, though, if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, we need to address how we handle snapshot tests. I’ll create a ticket for this purpose. You can follow this thread for more details.

self.delegate?.pledgePaymentPlansViewController(self, didTapTermsOfUseWith: helpType)
}

self.termsOfUseButton.rac.hidden = self.viewModel.outputs.termsOfUseButtonHidden
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this output? When the option isn't selected the entire PledgePaymentPlanOptionView will be hidden anyway, right? I don't think there's a case where the terms button won't be shown when everything else is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we need it because the termsOfUseButton is part of the same stack view (optionDescriptorStackView) as the titleLabel, subtitleLabel, paymentIncrementsStackView, etc. Any recommendations or thoughts?

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I thought that there was a stackview just for the items that need to be hidden/shown at the correct time.

}

self.termsOfUseButton.rac.hidden = self.viewModel.outputs.termsOfUseButtonHidden
self.paymentIncrementsStackView.rac.hidden = self.viewModel.outputs.paymentIncrementsHidden
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually same question about this stack view as the one above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as with the termsOfUseButton, I could use the same output to manage all the controls that should be displayed when Pledge Over Time is selected. However, I prefer this solution as it provides control at the level of each individual UI element. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case where one element for PLOT would be shown and not another? If they're always grouped together, controlling them with one output makes more sense to me. But I'm not as familiar with the designs as you both are!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s no current case where one element would show and not another, except for the Ineligible state. In that case, we display the badge but hide the increments, description, and terms of use button. Given this, I think it makes sense to handle this cleanup as part of a tech-debt ticket. Let me know if that works for you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, works for me.

Comment on lines 206 to 208
let titleLabel = UILabel()
applyIncrementTitleLabelStyle(titleLabel)
titleLabel.text = increment.title
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we rename this to something like chargeNumberLabel or something that makes it a bit clearer?

private func applyTermsOfUseStyle(_ button: UIButton) {
button.configuration = {
var config = UIButton.Configuration.borderless()
config.contentInsets = NSDirectionalEdgeInsets(top: 1.0, leading: 0, bottom: 1.0, trailing: 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we could throw these inset values, and any others if we have any, in a Constants enum. Similarly to EstimatedShippingCheckoutView.swift

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I’ll move these contentInsets values (and any similar ones like spacing things) to a Constants enum for consistency. Thanks for the suggestion!

@@ -94,14 +94,18 @@ public class NoShippingPledgeViewModel: NoShippingPledgeViewModelType, NoShippin

let backing = project.map { $0.personalization.backing }.skipNil()
self.showPledgeOverTimeUI = project.signal
.map { ($0.isPledgeOverTimeAllowed ?? false) && featurePledgeOverTimeEnabled()
}
.map { _ in featurePledgeOverTimeEnabled() }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can keep the project.isPledgeOverTimeAllowed check. Eventually, when this feature is fully rolled out and we clean up the feature flag we'll still only want to show PLOT UI if the project has isPledgeOverTimeAllowed set to true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ouch my bad, I removed a bit for test, I'll rollback this validation


self.pledgeOverTimeConfigData = self.showPledgeOverTimeUI
.map { showPledgeOverTimeUI -> PledgePaymentPlansAndSelectionData? in
.combineLatest(with: project)
.map { showPledgeOverTimeUI, project -> PledgePaymentPlansAndSelectionData? in
guard showPledgeOverTimeUI else { return nil }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this guard statement could be replaced with a .filter(showPledgeOverTimeUI). Then this output wouldn't need to return an optional PledgePaymentPlansAndSelectionData

import ReactiveSwift

public typealias PledgePaymentPlanOptionData = (
type: PledgePaymentPlansType,
selectedType: PledgePaymentPlansType
selectedType: PledgePaymentPlansType,
paymentIncrements: [PledgePaymentIncrement], // TODO: replece with API model
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
paymentIncrements: [PledgePaymentIncrement], // TODO: replece with API model
paymentIncrements: [PledgePaymentIncrement], // TODO: replece with API model in [mbl-1838](https://kickstarter.atlassian.net/browse/MBL-1838)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, thanks for adding the ticket ref

}
}

private func getPledgePaymentIncrementFormatted(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
private func getPledgePaymentIncrementFormatted(
private func formattedPledgePaymentIncrement(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed, good suggestion

public var selectedPlan: PledgePaymentPlansType
public var paymentIncrements: [PledgePaymentIncrement]
public var project: Project
/* TODO: add the necesary properties for the next states (PLOT Selected and Ineligible)
- [MBL-1815](https://kickstarter.atlassian.net/browse/MBL-1815)
Copy link
Contributor

@scottkicks scottkicks Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we be able to remove this comment now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, removed the line that is refer to the [MBL-1815]

@jovaniks jovaniks requested a review from scottkicks December 11, 2024 18:48
@nativeksr
Copy link
Collaborator

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

Copy link
Contributor

@scottkicks scottkicks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 just one last minor suggestion from me


self.pledgeOverTimeConfigData = self.showPledgeOverTimeUI
.filter { $0 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: personally, I'd prefer to make this more explicit like .filter { showUI in showUI == true } but that's a minor nit.

@scottkicks scottkicks mentioned this pull request Dec 11, 2024
4 tasks
Copy link
Contributor

@amy-at-kickstarter amy-at-kickstarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mostly looks good to me. Left some comments for clarity and a few suggestions. The two that are blocking for me are: using structs instead of tuples, and showing mock data in code merged to main. Can you address those two before merging this?

),
self.contentView.bottomAnchor.constraint(
equalTo: self.bottomAnchor,
constant: -Constants.defaultPaddingSpacing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that's slightly confusing - there's a very old, almost completely unused file KsApi/Constants.swift. Can you either rename this to something specific to PLOT (like PLOTConstants) or delete the old KsApi/Constants.swift?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Constants are scoped privately within this file, and they were actually suggested by Scott to keep things clean and localized. I’ll also check KsApi/Constants.swift and delete it if it’s not being used anywhere. Let me know if you think we should still move or rename these to something like PLOTConstants

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it looks like KsApi/Constants.swif is being used in KsApi/mutations/PostCommentMutation.swift

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha - so this is out of scope here. I'll clean up KsApi/Constants.swift in another PR for a quick win.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, thanks for taking care of that! Let me know if there's anything else needed from my side regarding this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, looks good to me.

}

self.termsOfUseButton.rac.hidden = self.viewModel.outputs.termsOfUseButtonHidden
self.paymentIncrementsStackView.rac.hidden = self.viewModel.outputs.paymentIncrementsHidden
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case where one element for PLOT would be shown and not another? If they're always grouped together, controlling them with one output makes more sense to me. But I'm not as familiar with the designs as you both are!

private func setupIncrementsStackView(with increments: [PledgePaymentIncrementFormatted]) {
var dateLabels: [UILabel] = []
increments.forEach { increment in
let incrementStackView = UIStackView()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be cleaner if it was all pulled out into its own subview, like

increments.forEach { increment in
 let incrementView = PledgePaymentIncrementView()
 incrementView.increment = increment

and then all the label and formatting logic could live there as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, and it would definitely help clean things up. I propose handling this as part of a tech-debt ticket so we can prioritize unblocking the API tasks for now. Let me know if that works for you!


if let firstDateLabel = dateLabels.first {
dateLabels.forEach { label in
label.widthAnchor.constraint(equalTo: firstDateLabel.widthAnchor).isActive = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this as a code comment?

@@ -1244,3 +1248,17 @@ private func pledgeAmountSummaryViewData(
rewardIsLocalPickup: rewardIsLocalPickup
)
}

private func mockPledgePaymentIncrement() -> [PledgePaymentIncrement] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging mock data into the main branch makes me nervous. It looks like it should be safe, since it's behind featurePledgeOverTimeEnabled(), but I'd worry about us making a mistake with the feature rollout and accidentally showing mock data to users on older versions of the app.

Can you put the mock code in a #if DEBUG macro block, or just comment it out when merging and return an empty array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, Amy. I think using #if DEBUG is the best approach here to ensure the mock data doesn’t accidentally appear in production. I’ll update the code accordingly and push the changes.

@@ -1,13 +1,35 @@
import Foundation
import KsApi
import ReactiveSwift

public typealias PledgePaymentPlanOptionData = (
Copy link
Contributor

@amy-at-kickstarter amy-at-kickstarter Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. If you need a model object here, it should be a struct and not a tuple. That way auto-complete and compile errors work significantly better.
  2. This will eventually come from the API, correct? Or will this object be created from the API object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve updated this to use a struct as suggested. In fact, some of these models have already been converted to struct in another PR. For point 2, only the paymentIncrements model will need to be updated to use the object from the API. The rest of the structure will remain as is for now.

self.subtitleLabelHidden = self.subtitleText.map { $0.isEmpty }

self.notifyDelegatePaymentPlanOptionSelected = self.optionTappedProperty
.signal
.withLatest(from: configData)
.map { $1.type }

let isPledgeOverTimeAndSelected = configData.map {
$0.type != .pledgeOverTime || $0.type != $0.selectedType
Copy link
Contributor

@amy-at-kickstarter amy-at-kickstarter Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This statement compared to the variable name is confusing, should it be $0.type == .pledgeOverTime && $0.type == $0.selectedType?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, Amy! I think this initially had other conditions, but now your suggestion makes perfect sense. I’ll apply your recommendation as is.

)
}

extension PledgePaymentIncrementFormatted {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it could live in its own file (and probably have a test or two)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I’ll suggest moving this into its own file and adding a tests as part of a tech-debt task, if that sounds good to you. Let me know if you'd prefer we tackle it sooner.

Copy link
Contributor Author

@jovaniks jovaniks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left replies addressing the feedback. For several points, I’ve suggested handling the proposed changes in tech-debt tickets to allow us to move forward with the API implementation work. Let me know if that approach works or if further adjustments are needed!

self.subtitleLabelHidden = self.subtitleText.map { $0.isEmpty }

self.notifyDelegatePaymentPlanOptionSelected = self.optionTappedProperty
.signal
.withLatest(from: configData)
.map { $1.type }

let isPledgeOverTimeAndSelected = configData.map {
$0.type != .pledgeOverTime || $0.type != $0.selectedType
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, Amy! I think this initially had other conditions, but now your suggestion makes perfect sense. I’ll apply your recommendation as is.

)
}

extension PledgePaymentIncrementFormatted {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I’ll suggest moving this into its own file and adding a tests as part of a tech-debt task, if that sounds good to you. Let me know if you'd prefer we tackle it sooner.

}

self.termsOfUseButton.rac.hidden = self.viewModel.outputs.termsOfUseButtonHidden
self.paymentIncrementsStackView.rac.hidden = self.viewModel.outputs.paymentIncrementsHidden
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s no current case where one element would show and not another, except for the Ineligible state. In that case, we display the badge but hide the increments, description, and terms of use button. Given this, I think it makes sense to handle this cleanup as part of a tech-debt ticket. Let me know if that works for you.

private func setupIncrementsStackView(with increments: [PledgePaymentIncrementFormatted]) {
var dateLabels: [UILabel] = []
increments.forEach { increment in
let incrementStackView = UIStackView()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, and it would definitely help clean things up. I propose handling this as part of a tech-debt ticket so we can prioritize unblocking the API tasks for now. Let me know if that works for you!

Copy link
Contributor

@amy-at-kickstarter amy-at-kickstarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates! LGTM. I filed a low-priority ticket for the other issues we discussed but aren't fixing now: https://kickstarter.atlassian.net/browse/MBL-1940

@jovaniks jovaniks merged commit 67c8b0e into main Dec 17, 2024
5 checks passed
@jovaniks jovaniks deleted the jluna/MBL-1815/plot-plan-selector-selected-state branch December 17, 2024 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants