-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[MBL-1815] PLOT plan selector - selected state #2213
Conversation
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
|
||
if let firstDateLabel = dateLabels.first { | ||
dateLabels.forEach { label in | ||
label.widthAnchor.constraint(equalTo: firstDateLabel.widthAnchor).isActive = true |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, added!
There was a problem hiding this 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, works for me.
let titleLabel = UILabel() | ||
applyIncrementTitleLabelStyle(titleLabel) | ||
titleLabel.text = increment.title |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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() } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
paymentIncrements: [PledgePaymentIncrement], // TODO: replece with API model | |
paymentIncrements: [PledgePaymentIncrement], // TODO: replece with API model in [mbl-1838](https://kickstarter.atlassian.net/browse/MBL-1838) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
private func getPledgePaymentIncrementFormatted( | |
private func formattedPledgePaymentIncrement( |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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]
Generated by 🚫 Danger |
There was a problem hiding this 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 } |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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] { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 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. - This will eventually come from the API, correct? Or will this object be created from the API object?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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!
There was a problem hiding this 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
📲 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
👀 See
✅ Acceptance criteria
⏰ TODO