I am in the process of setting up a payment method using the Next.js library from Stripe. Here is the code snippet:
import React, { FunctionComponent } from 'react';
import type { DisplaySettingsProps } from '@company/frontoffice/types';
import { Row, Col, Input } from '@company/ui';
import { useIntl } from '@company/frontoffice/providers';
import messages from './payment.lang';
import { loadStripe } from '@stripe/stripe-js';
import {
Elements,
useStripe,
useElements,
CardElement,
} from '@stripe/react-stripe-js';
/* Make sure to call `loadStripe` outside of a component’s render to avoid. recreating the `Stripe` object on every render. */
const stripePromise = loadStripe(
'pk_test_51JzJxAGAsaImrqUbu1pKDHiCSaq9A0sZTgpV6Li3sd8yYrCCVvPhCPfZ3i9JJ8ySgY5Uhceu6iGxlgylx2vuXBj100SpHqClA8'
);
const PaymentForm: FunctionComponent = () => {
const stripe = useStripe();
const elements = useElements();
const f = useIntl();
const handleSubmit = async (e) => {
e.preventDefault();
console.log('inside handleSubmit');
if (!stripe || !elements) {
console.log('!stripe || !elements');
return;
}
const cardElement = elements.getElement(CardElement);
/*
Returns:
result.paymentMethod: a PaymentMethod was created successfully.
result.error: there was an error.
*/
const { paymentMethod, error: backendError } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name: 'Jenny Rosen',
},
});
if (backendError) {
console.error(backendError.message);
return;
}
};
return (
<Elements stripe={stripePromise}>
<form
onSubmit={(e) => {
console.log('handleSubmit');
handleSubmit(e);
}}
>
<Row mb={4}>
<Col col={12}>
<Input label={f(messages.name)} placeholder={f(messages.name)} required={true} />
</Col>
</Row>
<Row>
<Col>
<CardElement />
</Col>
</Row>
<Row>
<button>[submit /]</button>
</Row>
</form>
</Elements>
);
};
export type StripePaymentFormProps = DisplaySettingsProps & {
displaySettings: any /* TODO */;
};
const StripePaymentForm: FunctionComponent<StripePaymentFormProps> = () => {
return (
<Elements stripe={stripePromise}>
<PaymentForm />
</Elements>
);
};
export default StripePaymentForm;
The generated form can be viewed here:
https://i.stack.imgur.com/z6o5i.png
When I click the submit button, I encounter this error in the console:
v3:1 Uncaught (in promise) IntegrationError: Invalid value for createPaymentMethod: card should be an object or element. You specified: null.
Upon checking console.log(cardElement)
, it displays null. Any insights into why this may be happening?
-EDIT-
I faced the same issue when splitting the CardElement into CardNumber, CardExpiry, and CardCVC as needed:
/* global fetch */
import React, { FunctionComponent } from 'react';
import type { DisplaySettingsProps } from '@company/frontoffice/types';
import { Row, Col, Text, Image, Input } from '@company/ui';
import { StyledInput } from './styledPayment';
import StripeInput from './Stripeinput';
import messages from './payment.lang';
import { useIntl } from '@company/frontoffice/providers';
import { loadStripe } from '@stripe/stripe-js';
import {
Elements,
useStripe,
useElements,
CardNumberElement,
CardCvcElement,
CardExpiryElement,
} from '@stripe/react-stripe-js';
/* Make sure to call `loadStripe` outside of a component’s render to avoid. recreating the `Stripe` object on every render. */
const stripePromise = loadStripe(
'pk_test_51JzJxAGAsaImrqUbu1pKDHiCSaq9A0sZTgpV6Li3sd8yYrCCVvPhCPfZ3i9JJ8ySgY5Uhceu6iGxlgylx2vuXBj100SpHqClA8'
);
const PaymentForm: FunctionComponent = () => {
const stripe = useStripe();
const elements = useElements();
const f = useIntl();
const handleSubmit = async (e) => {
e.preventDefault();
console.log('inside handleSubmit');
if (!stripe || !elements) {
console.log('!stripe || !elements');
return;
}
// what we had
const cardNumber = elements.getElement(CardNumberElement);
const cardCVC = elements.getElement(CardCvcElement);
const cardExpiry = elements.getElement(CardExpiryElement);
console.log(cardNumber, cardCVC, cardExpiry);
const { paymentMethod, error: backendError } = await stripe.createPaymentMethod({
type: 'card',
card: cardNumber,
billing_details: {
name: 'Jenny Rosen',
},
});
/* Create payment intent on server */
const { error: backendError_, clientSecret } = await fetch('/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
paymentMethodType: 'card',
currency: 'eur',
}),
}).then((r) => r.json());
if (backendError) {
console.error(backendError.message);
return;
}
if (backendError_) {
console.error(backendError_.message);
return;
}
console.log(`clientSecret generated: ${clientSecret}`);
// Confirm de payment on the client
const { error: stripeError, paymentIntent, error } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: cardNumber,
},
}
);
if (stripeError) {
console.error(stripeError.message);
return;
}
console.log(`PaymentIntent ${paymentIntent.id} is ${paymentIntent.status}`);
// what we had
console.log(cardExpiry);
};
return (
<Elements stripe={stripePromise}>
<form
onSubmit={(e) => {
handleSubmit(e);
}}
>
<Row mb={4}>
<Col col={12}>
<Input label={f(messages.name)} placeholder={f(messages.name)} required={true} />
</Col>
</Row>
<Row mb={4}>
<Col col={12}>
<StyledInput
label={f(messages.number)}
placeholder={f(messages.number)}
required={true}
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardNumberElement,
},
}}
/>
</Col>
</Row>
<Row colGap={5} mb={4}>
<Col col={[12, 6]}>
<StyledInput
label={f(messages.expiry)}
placeholder={f(messages.expiry)}
required={true}
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardExpiryElement,
},
}}
/>
</Col>
<Col col={[10, 5]}>
<StyledInput
label={f(messages.security)}
placeholder="CVC"
required={true}
InputProps={{
inputComponent: StripeInput,
inputProps: {
component: CardCvcElement,
},
}}
/>
<Text as="p" fontSize="xs" color="text2" mt={2}>
{f(messages.securitySub)}
</Text>
</Col>
<Col col={[2, 1]}>
{/* Todo - replace the scr when we receive from design (already requested) */}
<Image
mt={[6, 6]}
display="block"
mx="auto"
width="size10"
maxWidth="sizeFull"
src="card-cvc.png"
alt="Credit card cvc icon"
/>
</Col>
</Row>
<Row>
<button>[submit /]</button>
</Row>
</form>
</Elements>
);
};
export type StripePaymentFormProps = DisplaySettingsProps & {
displaySettings: any /* TODO */;
};
const StripePaymentForm: FunctionComponent<StripePaymentFormProps> = () => {
return (
<Elements stripe={stripePromise}>
<PaymentForm />
</Elements>
);
};
export default StripePaymentForm;
You can view the resulting form here:
https://i.stack.imgur.com/Rtnod.png
All three console.logs show null
and the purpose of stripeinput is to style them:
import React, { FunctionComponent, useRef, useImperativeHandle } from 'react';
type StripeInputPrpps = {
component: any; // todo type this
inputRef: any; // todo type this
props: any; // todo type this
};
const StripeInput: FunctionComponent<StripeInputPrpps> = ({
component: Component,
inputRef,
...props
}) => {
const elementRef = useRef();
/* Todo - obtain this values from theme.props */
const color = 'rgb(19, 40, 72)';
const placeholderColor = 'rgb(137, 147, 164)';
const inputStyle = {
color: color,
'::placeholder': {
color: placeholderColor,
},
};
useImperativeHandle(inputRef, () => ({
focus: () => {
if (elementRef && elementRef.current) {
elementRef.current.focus;
}
},
}));
return (
<Component
onReady={(element) => (elementRef.current = element)}
options={{
style: {
base: inputStyle,
},
}}
{...props}
/>
);
};
export default StripeInput;