-
Notifications
You must be signed in to change notification settings - Fork 288
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add Stripe Payments Intents API SCA-compliant Payment Component
Signed-off-by: Janus Reith <mail@janusreith.de>
- Loading branch information
1 parent
b1c1ae0
commit 30fa912
Showing
10 changed files
with
366 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
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 { | ||
// The payment has been processed! | ||
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
mutation createStripePaymentIntent($input: CreateStripePaymentIntentInput!) { | ||
createStripePaymentIntent(input: $input) { | ||
paymentIntentClientSecret | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import StripeWrapper from "./provider/StripeWrapper"; | ||
export { default } from "./StripeCard"; | ||
|
||
export { StripeWrapper }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.