Skip to content
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
41 changes: 41 additions & 0 deletions components/StripeCard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.DS_Store
.fileStorage/
.vscode
.idea
.c9
.env*
!.env.example*
!.env.prod*
*.csv
*.dat
*.gz
*.log
*.out
*.pid
*.seed
*.sublime-project
*.sublime-workspace
browser.config.js

lib-cov
logs
node_modules
npm-debug.log
pids
results
allure-results
package-lock.json

.reaction/config.json

.next/*
src/.next/*
build
/reports

docker-compose.override.yml

# Yalc
.yalc/
yalc.lock
yalc-packages
216 changes: 216 additions & 0 deletions components/StripeCard/StripeCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import React, { forwardRef, Fragment, useCallback, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { CardCvcElement, CardExpiryElement, CardNumberElement, useElements, useStripe } from "@stripe/react-stripe-js";
import { Box, Grid, TextField } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import Alert from "@material-ui/lab/Alert";
import useStripePaymentIntent from "./hooks/useStripePaymentIntent";
import StripeInput from "./StripeInput";

const useStyles = makeStyles(() => ({
stripeForm: {
display: "flex",
flexDirection: "column"
}
}));

function SplitForm(
{
isSaving,
onSubmit,
onReadyForSaveChange,
stripeCardNumberInputLabel = "Card Number",
stripeCardExpirationDateInputLabel = "Exp.",
stripeCardCVCInputLabel = "CVC"
},
ref
) {
const classes = useStyles();
const stripe = useStripe();
const elements = useElements();
const options = useMemo(
() => ({
showIcon: true,
style: {
base: {
fontSize: "18px"
}
}
}),
[]
);
const [error, setError] = useState();

const [formCompletionState, setFormCompletionState] = useState({
cardNumber: false,
cardExpiry: false,
cardCvc: false
});

const [isConfirmationInFlight, setIsConfirmationInFlight] = useState(false);

const [createStripePaymentIntent] = useStripePaymentIntent();

const isReady = useMemo(() => {
const { cardNumber, cardExpiry, cardCvc } = formCompletionState;

if (!isSaving && !isConfirmationInFlight && cardNumber && cardExpiry && cardCvc) return true;

return false;
}, [formCompletionState, isSaving, isConfirmationInFlight]);

useEffect(() => {
onReadyForSaveChange(isReady);
}, [onReadyForSaveChange, isReady]);

const onInputChange = useCallback(
({ elementType, complete }) => {
if (formCompletionState[elementType] !== complete) {
setFormCompletionState({
...formCompletionState,
[elementType]: complete
});
}
},
[formCompletionState, setFormCompletionState]
);

const handleSubmit = useCallback(
async (event) => {
if (event) {
event.preventDefault();
}

if (!stripe || !elements || isSaving || isConfirmationInFlight) {
// Stripe.js has not loaded yet, saving is in progress or card payment confirmation is in-flight.
return;
}

setError();
setIsConfirmationInFlight(true);

// Await the server secret here
const { paymentIntentClientSecret } = await createStripePaymentIntent();

const result = await stripe.confirmCardPayment(paymentIntentClientSecret, {
// eslint-disable-next-line camelcase
payment_method: {
card: elements.getElement(CardNumberElement)
}
});

setIsConfirmationInFlight(false);

if (result.error) {
// Show error to your customer (e.g., insufficient funds)
console.error(result.error.message); // eslint-disable-line
setError(result.error.message);
} else if (result.paymentIntent.status === "succeeded" || result.paymentIntent.status === "requires_capture") {
// Show a success message to your customer
// There's a risk of the customer closing the window before callback
// execution. Set up a webhook or plugin to listen for the
// payment_intent.succeeded event that handles any business critical
// post-payment actions.
const { amount, id } = result.paymentIntent;
onSubmit({
amount: amount ? parseFloat(amount / 100) : null,
data: { stripePaymentIntentId: id },
displayName: "Stripe Payment"
});
} else {
console.error("Payment was not successful"); // eslint-disable-line
setError("Payment was not successful");
}
},
[createStripePaymentIntent, onSubmit, stripe, setError, isConfirmationInFlight, setIsConfirmationInFlight]
);

useImperativeHandle(ref, () => ({
submit() {
handleSubmit();
}
}));

return (
<Fragment>
<Box my={2}>
<Grid container spacing={2}>
{error && (
<Grid item xs={12}>
<Alert severity="error">{error}</Alert>
</Grid>
)}
<Grid item xs={12}>
<form onSubmit={handleSubmit} className={classes.stripeForm}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
label={stripeCardNumberInputLabel}
name="ccnumber"
variant="outlined"
fullWidth
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardNumberElement,
options
}
}}
InputLabelProps={{
shrink: true
}}
onChange={onInputChange}
required
/>
</Grid>

<Grid item xs={6}>
<TextField
label={stripeCardExpirationDateInputLabel}
name="ccexp"
variant="outlined"
fullWidth
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardExpiryElement,
options
}
}}
InputLabelProps={{
shrink: true
}}
onChange={onInputChange}
required
/>
</Grid>

<Grid item xs={6}>
<TextField
label={stripeCardCVCInputLabel}
name="cvc"
variant="outlined"
fullWidth
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardCvcElement,
options
}
}}
InputLabelProps={{
shrink: true
}}
onChange={onInputChange}
required
/>
</Grid>
</Grid>
</form>
</Grid>
</Grid>
</Box>
</Fragment>
);
}

export default forwardRef(SplitForm);
18 changes: 18 additions & 0 deletions components/StripeCard/StripeInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, { useImperativeHandle, useRef } from "react";

function StripeInput({ component: Component, inputRef, ...props }) {
const elementRef = useRef();
useImperativeHandle(inputRef, () => ({
focus: () => elementRef.current.focus
}));
return (
<Component
onReady={(element) => {
elementRef.current = element;
}}
{...props}
/>
);
}

export default StripeInput;
5 changes: 5 additions & 0 deletions components/StripeCard/hooks/createStripePaymentIntent.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation createStripePaymentIntent($input: CreateStripePaymentIntentInput!) {
createStripePaymentIntent(input: $input) {
paymentIntentClientSecret
}
}
28 changes: 28 additions & 0 deletions components/StripeCard/hooks/useStripePaymentIntent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useMutation } from "@apollo/client";
import useCartStore from "hooks/globalStores/useCartStore";
import useShop from "hooks/shop/useShop";

import createStripePaymentIntentMutation from "./createStripePaymentIntent.gql";

export default function useStripePaymentIntent() {
const shop = useShop();
const { accountCartId, anonymousCartId, anonymousCartToken } = useCartStore();

const [createStripePaymentIntentFunc, { loading }] = useMutation(createStripePaymentIntentMutation);

const createStripePaymentIntent = async () => {
const { data } = await createStripePaymentIntentFunc({
variables: {
input: {
cartId: anonymousCartId || accountCartId,
shopId: shop?._id,
cartToken: anonymousCartToken
}
}
});

return data?.createStripePaymentIntent;
};

return [createStripePaymentIntent, loading];
}
5 changes: 5 additions & 0 deletions components/StripeCard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import StripeWrapper from "./provider/StripeWrapper";

export { default } from "./StripeCard";

export { StripeWrapper };
15 changes: 15 additions & 0 deletions components/StripeCard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "reaction-stripe-sca-react",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Janus Reith",
"license": "Apache-2.0",
"dependencies": {
"@stripe/react-stripe-js": "^1.4.1",
"@stripe/stripe-js": "^1.15.0"
}
}
10 changes: 10 additions & 0 deletions components/StripeCard/provider/StripeWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";

const stripePromise = loadStripe(process.env.STRIPE_PUBLIC_API_KEY);

function StripeWrapper({ children }) {
return <Elements stripe={stripePromise}>{children}</Elements>;
}

export default StripeWrapper;
14 changes: 7 additions & 7 deletions custom/paymentMethods.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import ExampleIOUPaymentForm from "@reactioncommerce/components/ExampleIOUPaymentForm/v1";
import StripePaymentInput from "@reactioncommerce/components/StripePaymentInput/v1";
import StripeCard from "components/StripeCard";

const paymentMethods = [
{
displayName: "Credit Card",
InputComponent: StripePaymentInput,
name: "stripe_card",
shouldCollectBillingAddress: true
},
{
displayName: "IOU",
InputComponent: ExampleIOUPaymentForm,
name: "iou_example",
shouldCollectBillingAddress: true
},
{
displayName: "Credit Card (SCA)",
InputComponent: StripeCard,
name: "stripe_payment_intent",
shouldCollectBillingAddress: true
}
];

Expand Down
2 changes: 2 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@
"reacto-form": "~1.4.0",
"styled-components": "^5.3.0",
"subscriptions-transport-ws": "~0.9.15",
"swr": "^0.5.6"
"swr": "^0.5.6",
"@stripe/react-stripe-js": "^1.4.1",
"@stripe/stripe-js": "^1.16.0"
},
"devDependencies": {
"@commitlint/cli": "^11.0.0",
Expand Down
Loading