-
Notifications
You must be signed in to change notification settings - Fork 111
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
[Woo POS][Cash & Receipts] Render cash button and navigate to cash view #14724
[Woo POS][Cash & Receipts] Render cash button and navigate to cash view #14724
Conversation
👋 there's no rush to review this as targets 21.4, it can wait to next week. |
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 it supposed to be modal, or a push navigation? It looks and feels like it's supposed to be a fullscreen cover modal, but it's not totally clear because of the back button. Certainly fullscreen cover would be a closer match to the bottom sheet shown in Wagner's walkthrough.
A couple of areas to look at:
- The transition to and from cash payments is weird, and I think it's because of the view being presented from the dashboard.
- Payment state – as a general principle, don't mess with the source of truth, it'll cause bugs and make everything complicated!
Transition weirdness
I don't think presenting cash payments from the dashboard is ideal – perhaps try a fullscreenCover
over the Totals view if it should be modal. If you do this, use a close button not the back button.
Note the way the underlying totals views navigate in and out. Fixing this is going to be difficult with the way you're presenting it at the moment, I'd advise against it to keep things simple! A fullscreenCover
presentation should just avoid all these issues.
Transitions.for.cash.payment.mp4
Payment state
The aggregate model exposes the source of truth for (card) payments, but it comes from the card payment service – you can't just change it. If you do, the card payment service will keep doing its thing and that makes the cash payment go away.
When the merchant taps Cash payment
, they are implicitly saying "This is not a card payment". So we should cancel the card payment preparation, otherwise the reader will just be sitting there waiting to be tapped. The simulator shows this really well because it insta-taps.
Fortunately we already support cancellations and re-preparation well: we do it when you go back to the item list and forward to the checkout again. Those functions should be all you need to handle the state management for cash payments.
card.payment.interfering.with.cash.mp4
Various other comments inline. I know the UI's not done yet, but just a note – that back arrow is very Androidy, and perhaps these designs are Android-first so need some translation. I think we should use a chevron.backward
where we need similar for iOS, but you could raise it with Wagner to check. This feels like somewhere platform convention comes before the design system.
@@ -61,6 +61,7 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt | |||
|
|||
private var startPaymentOnCardReaderConnection: AnyCancellable? | |||
private var cardReaderDisconnection: AnyCancellable? | |||
private var latestPaymentState: PointOfSalePaymentState? = 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.
To avoid issues around keeping state up to date, it would be better to avoid this duplication. Can we get what we need from the paymentState
?
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.
In this case you mean to keep track of "latest state before it switches to new state" internally within PointOfSalePaymentState
, and then access that? It definitely feels better, yes. For the moment I've reverted the changes to payment state with the changes to navigation, but I'll take that into account going forward.
func collectCashPayment() { | ||
// Capture the latest payment state before switching to cash collection | ||
// so we can return if the collection is cancelled | ||
latestPaymentState = paymentState | ||
paymentState = .acceptingCash | ||
} | ||
|
||
func cancelCashPayment() { | ||
if let latestPaymentState = latestPaymentState { | ||
paymentState = latestPaymentState | ||
} | ||
} |
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'm not sure that this is particularly safe. Setting the state back to something it used to be risks it being out-of-sync with the card payment service, and what happens then?
It would probably be better to cancel the reader preparation when they tap cash payment, then trigger it again if they cancel. I believe that's what's being done on Android too, so keeping platform consistency would be good.
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, thanks for the pointer. I've reverted these changes for the time being.
It would probably be better to cancel the reader preparation when they tap cash payment, then trigger it again if they cancel. I believe that's what's being done on Android too, so keeping platform consistency would be good.
👍 I've added a note to #14682 for when implementing collecting and cancelling
.cardPaymentSuccessful, | ||
.acceptingCash, | ||
.cashPaymentSuccessful: |
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 the distinction between card
and cash
needed on the success state? If so, perhaps they should be distinguished using an associated value?
Should acceptingCash
actually be a state here? Probably, but it's arguable.
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 question. Some of the presentation logic for the rest of the collection flow checks if .cardPaymentSuccessful
, which won't make much sense as state when the order is paid successfully via cash. Since moving the cash view presentation to not rely on payment state I think right now we can get rid of .acceptingCash
, but we still need to do something with .cardPaymentSuccessful
.
I'll check both into using a new state, or as you mention making this a .paymentSuccessful
state with associated value for cash or card 👍
private var orderTotal: String? { | ||
if case .loaded(let totals) = posModel.orderState { | ||
return totals.orderTotal | ||
} | ||
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.
Perhaps you can simplify this view by eliminating the potential for nil here?
If you required that totals
gets passed in, then only allow this view to be presented when the orderState is loaded, you'd avoid it entirely.
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 definitely, updated here: 714dfe5
Button(action: { | ||
Task { @MainActor in | ||
await markComplete() | ||
} | ||
}, label: { | ||
HStack(spacing: Constants.buttonSpacing) { | ||
if isLoading { | ||
ProgressView() | ||
.progressViewStyle(CircularProgressViewStyle()) | ||
.tint(Color.posPrimaryTextInverted) | ||
} else { | ||
Text(Localization.markPaymentCompletedButtonTitle) | ||
.font(Constants.buttonFont) | ||
} | ||
} | ||
.frame(maxWidth: .infinity) | ||
}) |
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.
Perhaps another place where an AsyncButton would be good? It'd need a binding so that the text field's onSubmit
also triggered the progress view, but that seems like a useful option.
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.
Agree! I'll make the change with #14602
.padding(Constants.buttonPadding) | ||
.frame(maxWidth: .infinity) | ||
.foregroundColor(Color.posPrimaryTextInverted) | ||
.background(Color.posOverlayFillInverted) | ||
.cornerRadius(Constants.buttonCornerRadius) | ||
.contentShape(Rectangle()) |
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'm guessing some of these are shared with other buttons? It'd be good to put them in a button style for sharing and to make the view easier to read.
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.
Yes, we have an existing issue to make the async button a reusable component, I've logged it along: #14626
comment: "Title of the cash payment navigation back button" | ||
) | ||
static let markPaymentCompletedButtonTitle = NSLocalizedString( | ||
"pointOfSale.cashview.back.navigation.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.
This key is wrong 😬 copy-pasta from 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.
Oops! Fixed: 714dfe5
) | ||
static let markPaymentCompletedButtonTitle = NSLocalizedString( | ||
"pointOfSale.cashview.back.navigation.title", | ||
value: "Mark payment as complete", |
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 is a weird way to word it... I can see it's in the designs, but maybe it's worth discussing some more? It feels very programmery, I think the merchant action is more like Accept payment
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.
Yeah, I'll raise it with Wagner along the navigation icons for each platform
switch posModel.paymentState { | ||
case .acceptingCash: | ||
PointOfSaleCollectCashView() | ||
default: | ||
TotalsView() | ||
.accessibilitySortPriority(2) | ||
.transition(.move(edge: .trailing)) | ||
} |
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 feels like the wrong place for the views to be switched. The button to collect cash is on the TotalsView, it seems like that should also do the presentation, rather than bounce out to the dashboard.
I know TotalsView is big, but collecting cash still seems like part of its responsibilities, not the dashboard's.
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, I've moved this to be a modal presentation from Totals: 6eb26ef
Along with the usage of a chevron for the cash view when navigating back, we also update the receipt view with the same icon for consistency
Thanks for the review and pointers @joshheald , very much appreciated.
That's a good question, I think there's a bit of mix here, it looks like push navigation (from the Android demo this also seems to be pushed from the side) but then the design demo shows a modal. I've make the changes to present it from the totals view as a full screen modal and definitely feels much better and less prone to error.
I wasn't feeling very good about this, and I guess the gut feeling was right 😅 I've removed the navigation interaction with the payment state for the time being. I've also updated the conditions when the button should be rendered so it only happens when the payment state is .idle for now. I'll need to test this further when playing with reader connection/cancellation: Screen.Recording.2024-12-20.at.16.18.13.mov
Yes, I've updated the arrow to a chevron (both in cash and receipt views) for consistency, I'll bring this up with design in any case, along with the string recommendations. Changes: ✅ Present the view as a full-screen modal from totals |
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 will finish the review with a physical card reader in a few hours, posting the initial review first.
In dark mode, iPad 10th gen iOS 18.0 simulator, I noticed some unexpected behavior from the cart view only when the feature flag is enabled as in the screencast below:
- When navigating to the totals screen, the top bar in the cart view has a different background color
- After dismissing the cash payment view, the cart view also gets shifted vertically briefly
This cart view behavior does not take place when the feature flag is disabled or in trunk.
Simulator.Screen.Recording.-.iPad.10th.generation.-.2024-12-23.at.10.55.53.mp4
Not sure if it's from some animation or some part of presentation of the cash payment view?
.fullScreenCover(isPresented: $shouldShowCollectCashPayment) { | ||
if case .loaded(let total) = posModel.orderState { | ||
PointOfSaleCollectCashView(isVisible: $shouldShowCollectCashPayment, |
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 isVisible
is only used for dismissing the PointOfSaleCollectCashView
fullscreen cover, can the view be dismissed with the @Environment(\.dismiss) private var dismiss
? If it's really needed, just a dismiss closure would be more clear than isVisible
parameter since fullScreenCover(isPresented: $shouldShowCollectCashPayment)
already uses the binding to present the view.
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 good catch, that was my initial approach but didn’t work correctly as we were pushing navigation into the stack, dismissing the whole POS, but since we’ve moved to a modal presentation instead it's much simpler to use dismiss. Changed on 8d723fa
case .idle, .validatingOrderError, .acceptingCard: | ||
case .idle, | ||
.validatingOrderError, | ||
.acceptingCard: |
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: it doesn't look like there are any changes, coulud be reverted.
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.
Reverted! e656b3b
@@ -291,6 +316,8 @@ private extension TotalsView { | |||
enum Constants { | |||
static let pricesIdealWidth: CGFloat = 382 | |||
static let verticalSpacing: CGFloat = 56 | |||
static let buttonHeight: CGFloat = 56 |
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: fixed button height might result in text being clipped with a large dynamic font size, and/or multiline text. If a fixed height is needed, @ScaledMetric
can be considered.
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.
@State private var isLoading: Bool = false | ||
@State private var errorMessage: String? | ||
|
||
@Binding var isVisible: Bool |
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: does this need to be public? (If this binding is still needed, from another comment in TotalsView
)
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.
No need! Removed since we're using dismiss 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.
Just tested on a physical tablet and this step didn't work on my end:
Connect the reader and observe the button still renders. Tapping the button and navigating back and forth retains the payment state, so the reader should remain connected:
After connecting the reader, the Cash payment CTA is not shown anymore:
RPReplay_Final1734936422.MP4
This doesn't seem expected from the screencast for this step in the PR description?
When dismissing the cash view, the animation for cart seems to jump vertically. It seems to be resolved by adding the cash view to matched geometry
Thanks for the review and testing @jaclync !
I'm unable to replicate this, now in iOS17 nor 18 🤔 do you see it in the physical device as well?
I see the same, the cart definitely seems to jump when dismissing. I fixed it on c8a00b2 by using the matching geometry. That said I'm seeing some oddness in the simulator that does not happen in physical device, I dropped a comment in slack here: p1734945310025859-C025A8VV728
Good catch, yes and no. My fault as I didn't update the instructions: We don't want to retain the state anymore, but to cancel the payment intent if the operator decides to use cash (this will be implemented later), but you're right that the cash payment button should appear when we're accepting card but haven't tapped yet. I've addressed this here: 57a66b6 |
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 cash payment CTA is shown when the reader is ready now 👍
It's interesting I don't see the cart view header background color change in iOS 17.5.1 iPad device, only in one iOS 18.0 iPad 10th gen simulator. In another iOS 18.0 iPad Pro 11in simulator, the background color only changed briefly when the vertical move happened after dismissing the cash view 😅
Since this doesn't happen when the feature flag is disabled, we can try testing on an iOS 18 physical tablet to confirm outside of this PR. I can upgrade my iPad to iOS 18 if needed.
iOS 17.5.1 device | iOS 18.0 simulator |
---|---|
Thanks for the screenshots! Let's upgrade either yours or mine so we still keep one in iOS17 for testing. I'll drop a follow-up in Slack 👍 |
@iamgabrielma as you reported internally, p1734920797831869-slack-CC7L49W13, the prototype build fails. Buried. inthe logs, I found these compilation errors
|
…into task/14603-add-cash-option-to-totals-view
📲 You can test the changes from this Pull Request in WooCommerce iOS by scanning the QR code below to install the corresponding build.
|
Closes: #14603
Closes: #14599
Description
This PR adds the
cash payment
button to the POS dashboard, and the initial navigation back and forth between both screens.Please disregard the UI details on the cash view, we'll address the design when implementing the cash functionality and error handling.
Changes
.acceptingCash
and.cashPaymentSuccessful
PointOfSaleCollectCashView
Testing
.acceptCashForPointOfSale
cash payment
buttons appears and it's tappable below the reader not connected error:RPReplay_Final1734600371.MP4
Screenshots
RELEASE-NOTES.txt
if necessary.Reviewer (or Author, in the case of optional code reviews):
Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement: