Skip to content

Commit

Permalink
feat: Support authorize only and capture mode for Stripe #675
Browse files Browse the repository at this point in the history
  • Loading branch information
treoden committed Dec 18, 2024
1 parent 1603238 commit 5c40f5f
Show file tree
Hide file tree
Showing 21 changed files with 607 additions and 298 deletions.
59 changes: 33 additions & 26 deletions packages/evershop/src/components/common/context/checkout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useCheckoutSteps } from '@components/common/context/checkoutSteps';
import { useAppDispatch } from '@components/common/context/app';

const Checkout = React.createContext();

const CheckoutDispatch = React.createContext();
export function CheckoutProvider({
children,
cartId,
Expand All @@ -18,37 +18,16 @@ export function CheckoutProvider({
const [paymentMethods, setPaymentMethods] = useState([]);
const [orderPlaced, setOrderPlaced] = useState(false);
const [orderId, setOrderId] = useState();
const [, setError] = useState(null);
const [error, setError] = useState(null);

// Call api to current url when steps change
useEffect(() => {
const placeOrder = async () => {
// If order is placed, do nothing
if (orderPlaced) {
return;
}
// If there is a incompleted step, do nothing
if (
steps.length < 1 ||
steps.findIndex((s) => s.isCompleted === false) !== -1
) {
return;
}
const response = await axios.post(placeOrderAPI, { cart_id: cartId });
if (!response.data.error) {
setOrderPlaced(true);
setOrderId(response.data.data.uuid);
setError(null);
} else {
setError(response.data.error.message);
}
};
const reload = async () => {
const url = new URL(window.location.href, window.location.origin);
url.searchParams.append('ajax', true);
await AppContextDispatch.fetchPageData(url);
url.searchParams.delete('ajax');
await placeOrder();
// await placeOrder();
};
reload();
}, [steps]);
Expand All @@ -67,17 +46,44 @@ export function CheckoutProvider({
() => ({
steps,
cartId,
error,
orderPlaced,
orderId,
paymentMethods,
setPaymentMethods,
getPaymentMethods,
checkoutSuccessUrl
}),
[steps, cartId, orderPlaced, orderId, paymentMethods, checkoutSuccessUrl]
[
steps,
cartId,
error,
orderPlaced,
orderId,
paymentMethods,
checkoutSuccessUrl
]
);

return <Checkout.Provider value={contextValue}>{children}</Checkout.Provider>;
const placeOrder = async () => {
try {
setError(null);
const response = await axios.post(placeOrderAPI, { cart_id: cartId });
setOrderPlaced(true);
setOrderId(response.data.data.uuid);
return response.data.data;
} catch (e) {
setError(e.message);
return null;
}
};

const dispatchMethods = useMemo(() => ({ placeOrder, setError }), []);
return (
<CheckoutDispatch.Provider value={dispatchMethods}>
<Checkout.Provider value={contextValue}>{children}</Checkout.Provider>
</CheckoutDispatch.Provider>
);
}

CheckoutProvider.propTypes = {
Expand All @@ -92,3 +98,4 @@ CheckoutProvider.propTypes = {
};

export const useCheckout = () => React.useContext(Checkout);
export const useCheckoutDispatch = () => React.useContext(CheckoutDispatch);
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function StepContent({
}) {
const { completeStep } = useCheckoutStepsDispatch();
const [useShippingAddress, setUseShippingAddress] = useState(!billingAddress);
const { cartId, paymentMethods, getPaymentMethods } = useCheckout();
const { cartId, error, paymentMethods, getPaymentMethods } = useCheckout();
const [loading, setLoading] = useState(false);

const onSuccess = async (response) => {
Expand Down Expand Up @@ -72,13 +72,20 @@ export function StepContent({
getPaymentMethods();
}, []);

useEffect(() => {
if (error) {
setLoading(false);
toast.error(error);
}
}, [error]);

const [result] = useQuery({
query: QUERY,
variables: {
cartId
}
});
const { data, fetching, error } = result;
const { data, fetching, error: queryError } = result;

if (fetching) {
return (
Expand All @@ -87,7 +94,7 @@ export function StepContent({
</div>
);
}
if (error) {
if (queryError) {
return <div className="p-8 text-critical">{error.message}</div>;
}
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import {
useElements
} from '@stripe/react-stripe-js';
import { useQuery } from 'urql';
import { useCheckout } from '@components/common/context/checkout';
import {
useCheckout,
useCheckoutDispatch
} from '@components/common/context/checkout';
import './CheckoutForm.scss';
import { Field } from '@components/common/form/Field';
import RenderIfTrue from '@components/common/RenderIfTrue';
import Spinner from '@components/common/Spinner';
import { toast } from 'react-toastify';
import { _ } from '@evershop/evershop/src/lib/locale/translate';
import TestCards from './TestCards';

const cartQuery = `
Expand Down Expand Up @@ -57,16 +61,15 @@ const cartQuery = `

export default function CheckoutForm({
stripePublishableKey,
clientSecret,
createPaymentIntentApi,
returnUrl
}) {
const [cardComleted, setCardCompleted] = useState(false);
const [error, setError] = useState(null);
const [, setDisabled] = useState(true);
const [clientSecret, setClientSecret] = React.useState(null);
const [showTestCard, setShowTestCard] = useState('success');
const stripe = useStripe();
const elements = useElements();
const { cartId, orderId, orderPlaced, paymentMethods } = useCheckout();
const { steps, cartId, orderId, orderPlaced, paymentMethods } = useCheckout();
const { placeOrder, setError } = useCheckoutDispatch();

const [result] = useQuery({
query: cartQuery,
Expand All @@ -78,15 +81,45 @@ export default function CheckoutForm({

useEffect(() => {
const pay = async () => {
const billingAddress =
result.data.cart.billingAddress || result.data.cart.shippingAddress;

const submit = await elements.submit();
if (submit.error) {
// Show error to your customer
setError(submit.error.message);
return;
}
// Place the order
await placeOrder();
};
// If all steps are completed, submit the payment
if (steps.every((step) => step.isCompleted)) {
pay();
}
}, [steps]);

useEffect(() => {
if (orderId && orderPlaced) {
window
.fetch(createPaymentIntentApi, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ cart_id: cartId, order_id: orderId })
})
.then((res) => res.json())
.then((data) => {
if (data.error) {
toast.error(_('Some error occurred. Please try again later.'));
} else {
setClientSecret(data.data.clientSecret);
}
});
}
}, [orderId]);

useEffect(() => {
const confirmPayment = async () => {
const billingAddress =
result.data.cart.billingAddress || result.data.cart.shippingAddress;
const payload = await stripe.confirmPayment({
clientSecret,
elements,
Expand Down Expand Up @@ -116,20 +149,10 @@ export default function CheckoutForm({
window.location.href = `${returnUrl}?order_id=${orderId}&payment_intent=${paymentIntent.id}`;
}
};

if (orderId && clientSecret) {
pay();
if (orderPlaced && clientSecret) {
confirmPayment();
}
}, [orderId, clientSecret, result]);

const handleChange = (event) => {
// Listen for changes in the CardElement
// and display any errors as the customer types their card details
setDisabled(event.empty);
if (event.complete === true && !event.error) {
setCardCompleted(true);
}
};
}, [orderPlaced, clientSecret]);

const testSuccess = () => {
setShowTestCard('success');
Expand All @@ -142,20 +165,21 @@ export default function CheckoutForm({
if (result.error) {
return (
<div className="flex p-8 justify-center items-center text-critical">
{error.message}
{result.error.message}
</div>
);
}
// Check if the selected payment method is Stripe
const stripePaymentMethod = paymentMethods.find(
(method) => method.code === 'stripe' && method.selected === true
);
if (!stripePaymentMethod) return null;

if (!stripePaymentMethod) {
return null;
}
return (
// eslint-disable-next-line react/jsx-filename-extension
<>
<RenderIfTrue condition={stripe && elements}>
<RenderIfTrue condition={!!(stripe && elements)}>
<div>
<div className="stripe-form">
{stripePublishableKey &&
Expand All @@ -166,28 +190,11 @@ export default function CheckoutForm({
testFailure={testFailure}
/>
)}
<PaymentElement id="payment-element" onChange={handleChange} />
<PaymentElement id="payment-element" />
</div>
{/* Show any error that happens when processing the payment */}
{error && (
<div className="card-error text-critical mb-8" role="alert">
{error}
</div>
)}
<Field
type="hidden"
name="stripeCartComplete"
value={cardComleted ? 1 : ''}
validationRules={[
{
rule: 'notEmpty',
message: 'Please complete the card information'
}
]}
/>
</div>
</RenderIfTrue>
<RenderIfTrue condition={!stripe || !elements}>
<RenderIfTrue condition={!!(!stripe || !elements)}>
<div className="flex justify-center p-5">
<Spinner width={20} height={20} />
</div>
Expand All @@ -198,6 +205,6 @@ export default function CheckoutForm({

CheckoutForm.propTypes = {
stripePublishableKey: PropTypes.string.isRequired,
clientSecret: PropTypes.string.isRequired,
returnUrl: PropTypes.string.isRequired
returnUrl: PropTypes.string.isRequired,
createPaymentIntentApi: PropTypes.string.isRequired
};
5 changes: 1 addition & 4 deletions packages/evershop/src/lib/util/hookable.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,18 @@ function hookable(originalFunction, context) {
const beforeHookFunctions = beforeHooks.get(funcName) || [];
const afterHookFunctions = afterHooks.get(funcName) || [];

// Clone the argumentsList to avoid mutation

for (let index = 0; index < beforeHookFunctions.length; index += 1) {
const callbackFunc = beforeHookFunctions[index].callback;
// Call the callback function with the cloned arguments
await callbackFunc.call(context, ...argumentsList);
}

const result = await Reflect.apply(target, thisArg, argumentsList);

for (let index = 0; index < afterHookFunctions.length; index += 1) {
const callbackFunc = afterHookFunctions[index].callback;
await callbackFunc.call(context, result, ...argumentsList);
}

return result;
}
: function (target, thisArg, argumentsList) {
Expand All @@ -80,7 +78,6 @@ function hookable(originalFunction, context) {
afterHookFunctions.forEach((hook) => {
hook.callback.call(context, result, ...argumentsList);
});

return result;
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module.exports = async (request, response, delegate, next) => {
return;
}

const orderId = await createOrder(cart);
const { uuid: orderId } = await createOrder(cart);

// Load created order
const order = await select()
Expand Down
Loading

0 comments on commit 5c40f5f

Please sign in to comment.