-
Notifications
You must be signed in to change notification settings - Fork 679
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
Handling invalid braintree nonce error while placing order. #2405
Handling invalid braintree nonce error while placing order. #2405
Conversation
|
} = talonProps; | ||
|
||
const [, { addToast }] = useToasts(); | ||
|
||
useEffect(() => { | ||
if (hasError) { | ||
const message = | ||
error && error.message | ||
? error.message |
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 we display the same error we receive from the GraphQl server? At times, it can contain text like GQL Error: *******
which might not be useful for the user.
Performance Test Results The following fails have been reported by WebpageTest. These numbers indicates a possible performance issue with the PR which requires further manual testing to validate. https://pr-2405.pwa-venia.com : LH Performance Expected 0.85 Actual 0.58, LH Best Practices Expected 1 Actual 0.92 |
@@ -105,6 +113,19 @@ export const usePaymentInformation = props => { | |||
[onSave] | |||
); | |||
|
|||
const clearPaymentDetails = useCallback(() => { |
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 will make sure even if the user refreshes the page, the summary won't be shown because hasData
will be false.
@@ -115,6 +136,27 @@ export const usePaymentInformation = props => { | |||
onCompleted: onPaymentDetailsQueryCompleted | |||
}); | |||
|
|||
const handleExiredPaymentError = useCallback(() => { |
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 have tried to reset the selected_payment_method
on the cart as well, but the GQL server does not allow us to do so. Once set, the selected_payment_method
can not be changed to ""
which is the default value when a cart is created.
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.
Yea I remember this being a thing. Fortunately we just display "done" state based on the completion of the child method and not based on selected_payment_method
.
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.
const handleExiredPaymentError = useCallback(() => { | |
const handleExpiredPaymentError = useCallback(() => { |
Make sure to update all references to this constant.
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.
Here's some initial pre-ux review feedback :) This is pretty close to what I had imagined the solution being. Basically just need to reset to step 3 (payment info) and display the edit form (wipe nonce/setDoneEditing(false).
* the step to PAYMENT, we will make the user re-enter the | ||
* payment information. | ||
*/ | ||
setCheckoutStep(CHECKOUT_STEP.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.
Instead of threading a callback to the child you can just do this in a try/catch inside handlePlaceOrder
.
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 am not sure how will throwing an error in the parent will propagate to the child.
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.
placeOrderError
is still that same error that is thrown but my point is that logic specific to checkout page should be done within the checkout page callback and logic specific to error handling for payment information such as removing the nonce should be done down the line. You've got a combination of these two things, I'm just asking that you split them.
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.
Commented below in another thread about my 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.
#2394 had logic in the place order catch block for setting review order button to false and setting checkout step to payment. If we're going to delegate the errors to the children for handling then we don't need the catch logic anymore (though we still should log the error for development purposes). So I would remove lines 139 and 140 in this file and perhaps replace them with a comment about how the child components handle submission errors.
Additionally, we also started passing setCheckoutStep
and resetShouldSubmit
to payment information.
Instead of writing the error handler here in checkout, could you define it in the usePaymentInformation
talon? We don't need to define it here and pass it down as nothing else uses it.
* This is temp stuff, need to have a better check. | ||
*/ | ||
return this.error.graphQLErrors.map(({ message }) => | ||
message.includes(paymentErrorMessage) |
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.
Maybe it doesn't matter that the payment is specifically expired. I'm not sure of all the ways that the specific error could be thrown but perhaps we don't even need a custom error class. With GQL mutations we can handle errors as they occur which means we won't ever be using this class for handling something like a submitShippingMethodError
since that error will be handled by the mutation invoker, useShippingMethod.js
.
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 are different errors that can happen during the place order mutation and some of them are not related to payment information. Check this out: https://devdocs.magento.com/guides/v2.3/graphql/mutations/place-order.html#errors
Now that we have an error class, we can pass that error to different child components and let them deal with it instead of adding this in the useCheckoutPage
's placeOrder
function.
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 nice find on the doc! May as well add a link in a comment at the top of this file.
Also funny enough, the transaction was declined
error isn't on that page 😆
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.
We may have to refine/add new error checks to extend this error class to other checkout page steps.
clearPaymentDetails({ variables: { cartId } }); | ||
resetReviewOrderButtonClicked(); | ||
setHasData(false); | ||
onError(); |
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.
Use a try-catch in the checkout page so you don't have to thread the onError
down to the child nor do you need to reset the review order button.
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 I have the try-catch and the throw statement in the useCheckoutPage
talon, how will the children catch it? It will only propagate upwards, right?
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 commented this in multiple places, but for posterity: placeOrderError
can be passed down (or some combination of the error).
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.
You are right, I can have the try-catch and send the rest of the props down the PaymentInformation
component/talon. But I feel, sending down a function to call when an error occurs is better than having some logic in try-catch in the parent and some in the child even though the function sent down to the child executes in the parent context.
That said, the function in question, revertPaymentInformationDone
sent as onError
is called as the last function in handleExiredPaymentError
. If instead of sending it as a prop, we call it in the try-catch, it will be called first. The child component won't have control over when to call the function.
TLDR, I feel comfortable sending it as a prop and having the child components handle the error and the onError handler. In this case, it is PaymentInformation
, but once we expand this to check for errors in ShippingInformation
and ShippingMethods
they can call the handlers themselves when they think is the right time.
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.
OK I get. it! In the future we may get other errors during placeOrder
that are related to other checkout components besides payment info. So this pattern would delegate the responsibility for handling the error to the component that caused it.
I'm on board with this but I do think we can at least move resetReviewOrderButtonClicked
to useCheckoutPage
handler as that's a global concern we will want to do every time we get an error.
packages/peregrine/lib/talons/CheckoutPage/PaymentInformation/usePaymentInformation.js
Outdated
Show resolved
Hide resolved
cart: { | ||
__typename: 'Cart', | ||
id: cartId, | ||
paymentNonce: null |
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 don't think that usePaymentInformation
should know about the nonce as it is specific to braintree/credit card.
That said, after working on payment information I understand the component heirarchy a bit more. CreditCard
is not mounted if you are in a "done editing" view which means that there's no way to pass the error to that component and have it wipe CC specific data like nonce or billing address.
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.
Exactly. Also, it is not a rule but it can be an understanding that any payment method which has to save something related to payment on the cart in the cache, should place it in the key called paymentNonce
. That way, we can handle clearing off of the cache for any payment method in the payment information component.
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.
any payment method which has to save something related to payment on the cart in the cache, should place it in the key called
paymentNonce
.
I like the idea here but I don't think paymentNonce
is a good name for it 😄Probably worth a small refactor here to rename this property. Perhaps selectedPaymentDetails
?
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.
@sirugh should we still change the prop? If so, I would like to handle it as part of a diff PR. I would not like to expand the scope of this PR. What do you think?
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 paymentNonce
should be fine as a name. I'd also be concerned about tearing that name out of every place we have it currently.
packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentInformation.gql.js
Show resolved
Hide resolved
class CheckoutError extends Error { | ||
constructor(gqlError, ...params) { | ||
super(params); | ||
this.name = 'CheckoutError'; | ||
this.message = gqlError.message; | ||
this.message = removeGQLTag(gqlError.message); |
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 don't think we have to explicitly strip out the gql tag. There is a property on the graphql errors that is just the message after the colon. Probably something like graphqlErrors[0].message
. I'd have to debug it to see the specific props but I know it's there.
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.
gqlError.message
is the generic error from the server. Since graphqlErrors
is an array and each one of them has a meesage
field, how would we know which one to display? Either we can display all the graphqlErrors.message
's or just show the generic message for the whole error object which is gqlError.message
.
I am fine with either. Let me know what you think.
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 wonder if error.message
always equivalent to the message from the first element in graphQLErrors
with GraphQL Error
.
In all other cases that we reference graphqlErrors
in this codebase we either iterate over each element in the array and handle it separately or we treat the first element in the array as the one we will "handle" either by displaying a relevant message or logging it.
Replacing the prefixed text seems fine though, especially for the root message of the error class.
export const GET_PAYMENT_NONCE = gql` | ||
query getPaymentNonce($cartId: String!) { | ||
cart(cart_id: $cartId) @connection(key: "Cart") { | ||
id |
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.
Remove id
and wrap it in eslint-disable for required fields. If you include id
it will make a network request :(
* the step to PAYMENT, we will make the user re-enter the | ||
* payment information. | ||
*/ | ||
setCheckoutStep(CHECKOUT_STEP.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.
#2394 had logic in the place order catch block for setting review order button to false and setting checkout step to payment. If we're going to delegate the errors to the children for handling then we don't need the catch logic anymore (though we still should log the error for development purposes). So I would remove lines 139 and 140 in this file and perhaps replace them with a comment about how the child components handle submission errors.
Additionally, we also started passing setCheckoutStep
and resetShouldSubmit
to payment information.
Instead of writing the error handler here in checkout, could you define it in the usePaymentInformation
talon? We don't need to define it here and pass it down as nothing else uses it.
import { CHECKOUT_STEP } from '../useCheckoutPage'; | ||
|
||
/** | ||
* | ||
* @param {Function} props.onSave callback to be called when user clicks review order button | ||
* @param {Object} props.checkoutError an instance of the `CheckoutError` error that has been generated using the error from the place order mutation | ||
* @param {DocumentNode} props.queries.getPaymentDetailsQuery query to fetch selected payment method and payment nonce details |
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.
Replace this with the nonce query doc. the other query is already included in the docs on line 16.
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.
Aside from the comment about the extra doc line added this is good to go for QA.
QA Pass, good to merge after approval. |
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.
New commits look 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.
Looks good. Some minor cleanup to do but I think this'll be good once that's done. 👍
@@ -115,6 +138,27 @@ export const usePaymentInformation = props => { | |||
onCompleted: onPaymentDetailsQueryCompleted | |||
}); | |||
|
|||
const handleExiredPaymentError = useCallback(() => { |
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.
const handleExiredPaymentError = useCallback(() => { | |
const handleExpiredPaymentError = useCallback(() => { |
* https://devdocs.magento.com/guides/v2.3/graphql/mutations/place-order.html#errors | ||
*/ | ||
|
||
const paymentErrorMessage = |
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.
Maybe leave a comment explaining that this a heuristic, rather than something to be rendered?
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 may be the correct use case for an enum containing all of the constants from the documentation linked elsewhere. Perhaps import this one?
cart: { | ||
__typename: 'Cart', | ||
id: cartId, | ||
paymentNonce: null |
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 paymentNonce
should be fine as a name. I'd also be concerned about tearing that name out of every place we have it currently.
@@ -115,6 +136,27 @@ export const usePaymentInformation = props => { | |||
onCompleted: onPaymentDetailsQueryCompleted | |||
}); | |||
|
|||
const handleExiredPaymentError = useCallback(() => { |
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.
const handleExiredPaymentError = useCallback(() => { | |
const handleExpiredPaymentError = useCallback(() => { |
Make sure to update all references to this constant.
|
||
useEffect(() => { | ||
if ( | ||
checkoutError && |
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.
Maybe remove this, if it's redundant with the next line.
Description
This PR handles the use case of an invalid nonce being submitted to the backend during the place order mutation. This can happen if a user has created a payment nonce and has not checkout for a while, enough for the nonce to expire.
The UI would never know to refresh the payment information until the backend fails with the invalid payment nonce error when the user clicks on the place order button.
I have handled payment information errors in this PR, but also made it possible for other developers to add other error handlers in Shipping Information and Shipping Method.
This PR introduces a new error class called
CheckoutError
which will be passed from theuseCheckoutPage
talon to Shipping Information, Shipping Method, and Payment Information. Each of those components should have handlers to handle this kind of an error. In this PR, I have added a handler for theCheckoutError
in theusePaymentInformation
talon.Related Issue
Closes PWA-556
Verification Stakeholders
@jimbo
@sirugh
Verification Steps
Here is the mutation:
and the query params:
Screenshots / Screen Captures (if appropriate)
Demo LIink - The error message in the demo has been changed. I have stripped off
GraphQL error:
string from the error message. Below is the updated screen shot for that.Error screenshot:
Checklist