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

Fix checkout process #1486

Merged
merged 5 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ way to update this template, but currently, we follow a pattern:

## Upcoming version 2021-XX-XX

- [fix] Fix bugs in checkout process:

- Submit button was enabled prematurely for onetime payments
- Toggling between default card and onetime payment flows was not working correctly in case of
error (e.g. network error).
- Calling Stripe.confirmCardPayment when status is requires_capture is unnecessary.

[#1486](https://github.com/sharetribe/ftw-daily/pull/1486)

- [change] Update many dependencies. See full list in the package.json changes in the PR.
[#1483](https://github.com/sharetribe/ftw-daily/pull/1483)
- [fix] Double click issue. Show dedicated message, when current user doesn't have a pending email
Expand Down
7 changes: 3 additions & 4 deletions src/containers/CheckoutPage/CheckoutPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,8 @@ export class CheckoutPageComponent extends Component {
const { stripe, card, billingDetails, paymentIntent } = handlePaymentParams;
const stripeElementMaybe = selectedPaymentFlow !== USE_SAVED_CARD ? { card } : {};

// Note: payment_method could be set here for USE_SAVED_CARD flow.
// { payment_method: stripePaymentMethodId }
// However, we have set it already on API side, when PaymentIntent was created.
// Note: For basic USE_SAVED_CARD scenario, we have set it already on API side, when PaymentIntent was created.
// However, the payment_method is save here for USE_SAVED_CARD flow if customer first attempted onetime payment
const paymentParams =
selectedPaymentFlow !== USE_SAVED_CARD
? {
Expand All @@ -293,7 +292,7 @@ export class CheckoutPageComponent extends Component {
card: card,
},
}
: {};
: { payment_method: stripePaymentMethodId };

const params = {
stripePaymentIntentClientSecret,
Expand Down
23 changes: 21 additions & 2 deletions src/ducks/stripe.duck.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { storableError } from '../util/errors';
import * as log from '../util/log';

// https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status
const STRIPE_PI_HAS_PASSED_CONFIRM = ['processing', 'requires_capture', 'canceled', 'succeeded'];

// ================ Action types ================ //

export const STRIPE_ACCOUNT_CLEAR_ERROR = 'app/stripe/STRIPE_ACCOUNT_CLEAR_ERROR';
Expand Down Expand Up @@ -255,14 +258,30 @@ export const confirmCardPayment = params => dispatch => {
? [stripePaymentIntentClientSecret, paymentParams]
: [stripePaymentIntentClientSecret];

const doConfirmCardPayment = () =>
stripe.confirmCardPayment(...args).then(response => {
if (response.error) {
return Promise.reject(response);
} else {
dispatch(confirmCardPaymentSuccess(response));
return { ...response, transactionId };
}
});

// First, check if the payment intent has already been confirmed and it just requires capture.
return stripe
.confirmCardPayment(...args)
.retrievePaymentIntent(stripePaymentIntentClientSecret)
.then(response => {
// Handle response.error or response.paymentIntent
if (response.error) {
return Promise.reject(response);
} else {
} else if (STRIPE_PI_HAS_PASSED_CONFIRM.includes(response?.paymentIntent?.status)) {
// Payment Intent has been confirmed already, move forward.
dispatch(confirmCardPaymentSuccess(response));
return { ...response, transactionId };
} else {
// If payment intent has not been confirmed yet, confirm it.
return doConfirmCardPayment();
}
})
.catch(err => {
Expand Down
58 changes: 45 additions & 13 deletions src/forms/StripePaymentForm/StripePaymentForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,30 @@ const getPaymentMethod = (selectedPaymentMethod, hasDefaultPaymentMethod) => {
: selectedPaymentMethod;
};

// Should we show onetime payment fields and does StripeElements card need attention
const checkOnetimePaymentFields = (
cardValueValid,
selectedPaymentMethod,
hasDefaultPaymentMethod,
hasHandledCardPayment
) => {
const useDefaultPaymentMethod =
selectedPaymentMethod === 'defaultCard' && hasDefaultPaymentMethod;
// Billing details are known if we have already handled card payment or existing default payment method is used.
const billingDetailsKnown = hasHandledCardPayment || useDefaultPaymentMethod;

// If onetime payment is used, check that the StripeElements card has valid value.
const oneTimePaymentMethods = ['onetimeCardPayment', 'replaceCard'];
const useOnetimePaymentMethod = oneTimePaymentMethods.includes(selectedPaymentMethod);
const onetimePaymentNeedsAttention =
!billingDetailsKnown && !(useOnetimePaymentMethod && cardValueValid);

return {
onetimePaymentNeedsAttention,
showOnetimePaymentFields: useOnetimePaymentMethod,
};
};

const initialState = {
error: null,
cardValueValid: false,
Expand Down Expand Up @@ -255,6 +279,7 @@ class StripePaymentForm extends Component {
this.card.removeEventListener('change', this.handleCardValueChange);
this.card.unmount();
this.card = null;
this.setState({ cardValueValid: false });
}
this.setState({ paymentMethod: changedTo });
}
Expand Down Expand Up @@ -292,8 +317,14 @@ class StripePaymentForm extends Component {
} = this.props;
const { initialMessage } = values;
const { cardValueValid, paymentMethod } = this.state;
const billingDetailsKnown = hasHandledCardPayment || defaultPaymentMethod;
const onetimePaymentNeedsAttention = !billingDetailsKnown && !cardValueValid;
const hasDefaultPaymentMethod = defaultPaymentMethod?.id;
const selectedPaymentMethod = getPaymentMethod(paymentMethod, hasDefaultPaymentMethod);
const { onetimePaymentNeedsAttention } = checkOnetimePaymentFields(
cardValueValid,
selectedPaymentMethod,
hasDefaultPaymentMethod,
hasHandledCardPayment
);

if (inProgress || onetimePaymentNeedsAttention) {
// Already submitting or card value incomplete/invalid
Expand Down Expand Up @@ -338,8 +369,17 @@ class StripePaymentForm extends Component {

const ensuredDefaultPaymentMethod = ensurePaymentMethodCard(defaultPaymentMethod);
const billingDetailsNeeded = !(hasHandledCardPayment || confirmPaymentError);
const billingDetailsKnown = hasHandledCardPayment || ensuredDefaultPaymentMethod;
const onetimePaymentNeedsAttention = !billingDetailsKnown && !this.state.cardValueValid;

const { cardValueValid, paymentMethod } = this.state;
const hasDefaultPaymentMethod = ensuredDefaultPaymentMethod.id;
const selectedPaymentMethod = getPaymentMethod(paymentMethod, hasDefaultPaymentMethod);
const { onetimePaymentNeedsAttention, showOnetimePaymentFields } = checkOnetimePaymentFields(
cardValueValid,
selectedPaymentMethod,
hasDefaultPaymentMethod,
hasHandledCardPayment
);

const submitDisabled = invalid || onetimePaymentNeedsAttention || submitInProgress;
const hasCardError = this.state.error && !submitInProgress;
const hasPaymentErrors = confirmCardPaymentError || confirmPaymentError;
Expand Down Expand Up @@ -391,19 +431,11 @@ class StripePaymentForm extends Component {
);

const hasStripeKey = config.stripe.publishableKey;
const showPaymentMethodSelector = ensuredDefaultPaymentMethod.id;
const selectedPaymentMethod = getPaymentMethod(
this.state.paymentMethod,
showPaymentMethodSelector
);
const showOnetimePaymentFields = ['onetimeCardPayment', 'replaceCard'].includes(
selectedPaymentMethod
);
return hasStripeKey ? (
<Form className={classes} onSubmit={handleSubmit} enforcePagePreloadFor="OrderDetailsPage">
{billingDetailsNeeded && !loadingData ? (
<React.Fragment>
{showPaymentMethodSelector ? (
{hasDefaultPaymentMethod ? (
<PaymentMethodSelector
cardClasses={cardClasses}
formId={formId}
Expand Down