├── next-env.d.ts
├── public
├── logo.png
├── social_card.png
├── checkout_demo.gif
├── elements_demo.gif
├── shopping_cart_demo.gif
├── use-shopping-cart.png
├── elements-card-payment.svg
└── checkout-one-time-payments.svg
├── .gitignore
├── pages
├── _app.tsx
├── donate-with-checkout.tsx
├── donate-with-elements.tsx
├── use-shopping-cart.tsx
├── api
│ ├── checkout_sessions
│ │ ├── [id].ts
│ │ ├── index.ts
│ │ └── cart.ts
│ ├── payment_intents
│ │ └── index.ts
│ └── webhooks
│ │ └── index.ts
├── index.tsx
└── result.tsx
├── .env.local.example
├── config
└── index.ts
├── components
├── ClearCart.tsx
├── PrintObject.tsx
├── Cart.tsx
├── StripeTestCards.tsx
├── CustomDonationInput.tsx
├── Products.tsx
├── CartSummary.tsx
├── Layout.tsx
├── CheckoutForm.tsx
└── ElementsForm.tsx
├── utils
├── get-stripejs.ts
├── stripe-helpers.ts
└── api-helpers.ts
├── tsconfig.json
├── data
└── products.json
├── package.json
├── LICENSE
├── styles.css
└── README.md
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-archive/nextjs-typescript-react-stripe-js/HEAD/public/logo.png
--------------------------------------------------------------------------------
/public/social_card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-archive/nextjs-typescript-react-stripe-js/HEAD/public/social_card.png
--------------------------------------------------------------------------------
/public/checkout_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-archive/nextjs-typescript-react-stripe-js/HEAD/public/checkout_demo.gif
--------------------------------------------------------------------------------
/public/elements_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-archive/nextjs-typescript-react-stripe-js/HEAD/public/elements_demo.gif
--------------------------------------------------------------------------------
/public/shopping_cart_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-archive/nextjs-typescript-react-stripe-js/HEAD/public/shopping_cart_demo.gif
--------------------------------------------------------------------------------
/public/use-shopping-cart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stripe-archive/nextjs-typescript-react-stripe-js/HEAD/public/use-shopping-cart.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 |
4 | # Node files
5 | node_modules/
6 |
7 | # Typescript
8 | dist
9 |
10 | # Next.js
11 | .next
12 | .vercel
13 | .env*.local
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from 'next/app'
2 |
3 | import '../styles.css'
4 |
5 | function MyApp({ Component, pageProps }: AppProps) {
6 | return
7 | }
8 |
9 | export default MyApp
10 |
--------------------------------------------------------------------------------
/.env.local.example:
--------------------------------------------------------------------------------
1 | # Stripe keys
2 | # https://dashboard.stripe.com/apikeys
3 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_12345
4 | STRIPE_SECRET_KEY=sk_12345
5 | # https://stripe.com/docs/webhooks/signatures
6 | STRIPE_WEBHOOK_SECRET=whsec_1234
7 |
--------------------------------------------------------------------------------
/config/index.ts:
--------------------------------------------------------------------------------
1 | export const CURRENCY = 'usd'
2 | // Set your amount limits: Use float for decimal currencies and
3 | // Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal.
4 | export const MIN_AMOUNT = 10.0
5 | export const MAX_AMOUNT = 5000.0
6 | export const AMOUNT_STEP = 5.0
7 |
--------------------------------------------------------------------------------
/components/ClearCart.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useShoppingCart } from 'use-shopping-cart';
3 |
4 | export default function ClearCart() {
5 | const { clearCart } = useShoppingCart();
6 |
7 | useEffect(() => clearCart(), [clearCart]);
8 |
9 | return
Cart cleared.
;
10 | }
11 |
--------------------------------------------------------------------------------
/components/PrintObject.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = {
4 | content: object;
5 | };
6 |
7 | const PrintObject = ({ content }: Props) => {
8 | const formattedContent: string = JSON.stringify(content, null, 2);
9 | return {formattedContent};
10 | };
11 |
12 | export default PrintObject;
13 |
--------------------------------------------------------------------------------
/utils/get-stripejs.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This is a singleton to ensure we only instantiate Stripe once.
3 | */
4 | import { Stripe, loadStripe } from '@stripe/stripe-js';
5 |
6 | let stripePromise: Promise;
7 | const getStripe = () => {
8 | if (!stripePromise) {
9 | stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
10 | }
11 | return stripePromise;
12 | };
13 |
14 | export default getStripe;
15 |
--------------------------------------------------------------------------------
/components/Cart.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import { CartProvider } from 'use-shopping-cart';
3 | import getStripe from '../utils/get-stripejs';
4 | import * as config from '../config';
5 |
6 | const Cart = ({ children }: { children: ReactNode }) => (
7 |
12 | <>{children}>
13 |
14 | );
15 |
16 | export default Cart;
17 |
--------------------------------------------------------------------------------
/pages/donate-with-checkout.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import Layout from '../components/Layout'
3 |
4 | import CheckoutForm from '../components/CheckoutForm'
5 |
6 | const DonatePage: NextPage = () => {
7 | return (
8 |
9 |
10 |
Donate with Checkout
11 |
Donate to our project 💖
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default DonatePage
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve"
16 | },
17 | "exclude": ["node_modules"],
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
19 | }
20 |
--------------------------------------------------------------------------------
/components/StripeTestCards.tsx:
--------------------------------------------------------------------------------
1 | const StripeTestCards = () => {
2 | return (
3 |
4 | Use any of the{' '}
5 |
10 | Stripe test cards
11 | {' '}
12 | for this demo, e.g.{' '}
13 |
14 | 4242424242424242
15 |
16 | .
17 |
18 | );
19 | };
20 |
21 | export default StripeTestCards;
22 |
--------------------------------------------------------------------------------
/pages/donate-with-elements.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 |
3 | import { Elements } from '@stripe/react-stripe-js';
4 | import getStripe from '../utils/get-stripejs';
5 |
6 | import Layout from '../components/Layout';
7 | import ElementsForm from '../components/ElementsForm';
8 |
9 | const DonatePage: NextPage = () => {
10 | return (
11 |
12 |
13 |
Donate with Elements
14 |
Donate to our project 💖
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default DonatePage;
24 |
--------------------------------------------------------------------------------
/data/products.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Bananas",
4 | "description": "Yummy yellow fruit",
5 | "sku": "sku_GBJ2Ep8246qeeT",
6 | "price": 400,
7 | "image": "https://images.unsplash.com/photo-1574226516831-e1dff420e562?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=225&q=80",
8 | "attribution": "Photo by Priscilla Du Preez on Unsplash",
9 | "currency": "USD"
10 | },
11 | {
12 | "name": "Tangerines",
13 | "sku": "sku_GBJ2WWfMaGNC2Z",
14 | "price": 100,
15 | "image": "https://images.unsplash.com/photo-1482012792084-a0c3725f289f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=225&q=80",
16 | "attribution": "Photo by Jonathan Pielmayer on Unsplash",
17 | "currency": "USD"
18 | }
19 | ]
20 |
--------------------------------------------------------------------------------
/pages/use-shopping-cart.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import Layout from '../components/Layout';
3 |
4 | import Cart from '../components/Cart';
5 | import CartSummary from '../components/CartSummary';
6 | import Products from '../components/Products';
7 |
8 | const DonatePage: NextPage = () => {
9 | return (
10 |
11 |
12 |
Shopping Cart
13 |
14 | Powered by the{' '}
15 | use-shopping-cart{' '}
16 | React hooks library.
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default DonatePage;
28 |
--------------------------------------------------------------------------------
/pages/api/checkout_sessions/[id].ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 |
3 | import Stripe from 'stripe';
4 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
5 | // https://github.com/stripe/stripe-node#configuration
6 | apiVersion: '2020-03-02',
7 | });
8 |
9 | export default async function handler(
10 | req: NextApiRequest,
11 | res: NextApiResponse
12 | ) {
13 | const id: string = req.query.id as string;
14 | try {
15 | if (!id.startsWith('cs_')) {
16 | throw Error('Incorrect CheckoutSession ID.');
17 | }
18 | const checkout_session: Stripe.Checkout.Session = await stripe.checkout.sessions.retrieve(
19 | id,
20 | { expand: ['payment_intent'] }
21 | );
22 |
23 | res.status(200).json(checkout_session);
24 | } catch (err) {
25 | res.status(500).json({ statusCode: 500, message: err.message });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/utils/stripe-helpers.ts:
--------------------------------------------------------------------------------
1 | export function formatAmountForDisplay(
2 | amount: number,
3 | currency: string
4 | ): string {
5 | let numberFormat = new Intl.NumberFormat(['en-US'], {
6 | style: 'currency',
7 | currency: currency,
8 | currencyDisplay: 'symbol',
9 | })
10 | return numberFormat.format(amount)
11 | }
12 |
13 | export function formatAmountForStripe(
14 | amount: number,
15 | currency: string
16 | ): number {
17 | let numberFormat = new Intl.NumberFormat(['en-US'], {
18 | style: 'currency',
19 | currency: currency,
20 | currencyDisplay: 'symbol',
21 | })
22 | const parts = numberFormat.formatToParts(amount)
23 | let zeroDecimalCurrency: boolean = true
24 | for (let part of parts) {
25 | if (part.type === 'decimal') {
26 | zeroDecimalCurrency = false
27 | }
28 | }
29 | return zeroDecimalCurrency ? amount : Math.round(amount * 100)
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stripe-sample-nextjs-typescript-react-stripe-js",
3 | "version": "2.0.0",
4 | "description": "Full-stack TypeScript example using Next.js, react-stripe-js, and stripe-node.",
5 | "scripts": {
6 | "dev": "next",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "license": "ISC",
11 | "dependencies": {
12 | "@stripe/react-stripe-js": "1.1.2",
13 | "@stripe/stripe-js": "1.5.0",
14 | "dotenv": "latest",
15 | "micro": "^9.3.4",
16 | "micro-cors": "^0.1.1",
17 | "next": "latest",
18 | "react": "^16.12.0",
19 | "react-dom": "^16.12.0",
20 | "stripe": "8.56.0",
21 | "swr": "^0.1.16",
22 | "use-shopping-cart": "2.1.0"
23 | },
24 | "devDependencies": {
25 | "@types/micro": "^7.3.3",
26 | "@types/micro-cors": "^0.1.0",
27 | "@types/node": "^13.1.2",
28 | "@types/react": "^16.9.17",
29 | "typescript": "^3.7.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Stripe, Inc. (https://stripe.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/components/CustomDonationInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { formatAmountForDisplay } from '../utils/stripe-helpers';
3 |
4 | type Props = {
5 | name: string;
6 | value: number;
7 | min: number;
8 | max: number;
9 | currency: string;
10 | step: number;
11 | onChange: (e: React.ChangeEvent) => void;
12 | className?: string;
13 | };
14 |
15 | const CustomDonationInput = ({
16 | name,
17 | value,
18 | min,
19 | max,
20 | currency,
21 | step,
22 | onChange,
23 | className,
24 | }: Props) => (
25 |
48 | );
49 |
50 | export default CustomDonationInput;
51 |
--------------------------------------------------------------------------------
/components/Products.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import products from '../data/products.json';
4 | import { useShoppingCart, formatCurrencyString } from 'use-shopping-cart';
5 |
6 | const Products = () => {
7 | const { addItem, removeItem } = useShoppingCart();
8 |
9 | return (
10 |
11 | {products.map((product) => (
12 |
13 |

14 |
{product.name}
15 |
16 | {formatCurrencyString({
17 | value: product.price,
18 | currency: product.currency,
19 | })}
20 |
21 |
27 |
33 |
34 | ))}
35 |
36 | );
37 | };
38 |
39 | export default Products;
40 |
--------------------------------------------------------------------------------
/utils/api-helpers.ts:
--------------------------------------------------------------------------------
1 | export async function fetchGetJSON(url: string) {
2 | try {
3 | const data = await fetch(url).then((res) => res.json());
4 | return data;
5 | } catch (err) {
6 | throw new Error(err.message);
7 | }
8 | }
9 |
10 | export async function fetchPostJSON(url: string, data?: {}) {
11 | try {
12 | // Default options are marked with *
13 | const response = await fetch(url, {
14 | method: 'POST', // *GET, POST, PUT, DELETE, etc.
15 | mode: 'cors', // no-cors, *cors, same-origin
16 | cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
17 | credentials: 'same-origin', // include, *same-origin, omit
18 | headers: {
19 | 'Content-Type': 'application/json',
20 | // 'Content-Type': 'application/x-www-form-urlencoded',
21 | },
22 | redirect: 'follow', // manual, *follow, error
23 | referrerPolicy: 'no-referrer', // no-referrer, *client
24 | body: JSON.stringify(data || {}), // body data type must match "Content-Type" header
25 | });
26 | return await response.json(); // parses JSON response into native JavaScript objects
27 | } catch (err) {
28 | throw new Error(err.message);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import Link from 'next/link';
3 | import Layout from '../components/Layout';
4 |
5 | const IndexPage: NextPage = () => {
6 | return (
7 |
8 |
34 |
35 | );
36 | };
37 |
38 | export default IndexPage;
39 |
--------------------------------------------------------------------------------
/pages/result.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import { useRouter } from 'next/router';
3 |
4 | import Layout from '../components/Layout';
5 | import PrintObject from '../components/PrintObject';
6 | import Cart from '../components/Cart';
7 | import ClearCart from '../components/ClearCart';
8 |
9 | import { fetchGetJSON } from '../utils/api-helpers';
10 | import useSWR from 'swr';
11 |
12 | const ResultPage: NextPage = () => {
13 | const router = useRouter();
14 |
15 | // Fetch CheckoutSession from static page via
16 | // https://nextjs.org/docs/basic-features/data-fetching#static-generation
17 | const { data, error } = useSWR(
18 | router.query.session_id
19 | ? `/api/checkout_sessions/${router.query.session_id}`
20 | : null,
21 | fetchGetJSON
22 | );
23 |
24 | if (error) return failed to load
;
25 |
26 | return (
27 |
28 |
29 |
Checkout Payment Result
30 |
Status: {data?.payment_intent?.status ?? 'loading...'}
31 |
CheckoutSession response:
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default ResultPage;
42 |
--------------------------------------------------------------------------------
/pages/api/payment_intents/index.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 |
3 | import { CURRENCY, MIN_AMOUNT, MAX_AMOUNT } from '../../../config';
4 | import { formatAmountForStripe } from '../../../utils/stripe-helpers';
5 |
6 | import Stripe from 'stripe';
7 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
8 | // https://github.com/stripe/stripe-node#configuration
9 | apiVersion: '2020-03-02',
10 | });
11 |
12 | export default async function handler(
13 | req: NextApiRequest,
14 | res: NextApiResponse
15 | ) {
16 | if (req.method === 'POST') {
17 | const { amount }: { amount: number } = req.body;
18 | try {
19 | // Validate the amount that was passed from the client.
20 | if (!(amount >= MIN_AMOUNT && amount <= MAX_AMOUNT)) {
21 | throw new Error('Invalid amount.');
22 | }
23 | // Create PaymentIntent from body params.
24 | const params: Stripe.PaymentIntentCreateParams = {
25 | payment_method_types: ['card'],
26 | amount: formatAmountForStripe(amount, CURRENCY),
27 | currency: CURRENCY,
28 | };
29 | const payment_intent: Stripe.PaymentIntent = await stripe.paymentIntents.create(
30 | params
31 | );
32 |
33 | res.status(200).json(payment_intent);
34 | } catch (err) {
35 | res.status(500).json({ statusCode: 500, message: err.message });
36 | }
37 | } else {
38 | res.setHeader('Allow', 'POST');
39 | res.status(405).end('Method Not Allowed');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/pages/api/checkout_sessions/index.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 |
3 | import { CURRENCY, MIN_AMOUNT, MAX_AMOUNT } from '../../../config';
4 | import { formatAmountForStripe } from '../../../utils/stripe-helpers';
5 |
6 | import Stripe from 'stripe';
7 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
8 | // https://github.com/stripe/stripe-node#configuration
9 | apiVersion: '2020-03-02',
10 | });
11 |
12 | export default async function handler(
13 | req: NextApiRequest,
14 | res: NextApiResponse
15 | ) {
16 | if (req.method === 'POST') {
17 | const amount: number = req.body.amount;
18 | try {
19 | // Validate the amount that was passed from the client.
20 | if (!(amount >= MIN_AMOUNT && amount <= MAX_AMOUNT)) {
21 | throw new Error('Invalid amount.');
22 | }
23 | // Create Checkout Sessions from body params.
24 | const params: Stripe.Checkout.SessionCreateParams = {
25 | submit_type: 'donate',
26 | payment_method_types: ['card'],
27 | line_items: [
28 | {
29 | name: 'Custom amount donation',
30 | amount: formatAmountForStripe(amount, CURRENCY),
31 | currency: CURRENCY,
32 | quantity: 1,
33 | },
34 | ],
35 | success_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`,
36 | cancel_url: `${req.headers.origin}/donate-with-checkout`,
37 | };
38 | const checkoutSession: Stripe.Checkout.Session = await stripe.checkout.sessions.create(
39 | params
40 | );
41 |
42 | res.status(200).json(checkoutSession);
43 | } catch (err) {
44 | res.status(500).json({ statusCode: 500, message: err.message });
45 | }
46 | } else {
47 | res.setHeader('Allow', 'POST');
48 | res.status(405).end('Method Not Allowed');
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/components/CartSummary.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import StripeTestCards from '../components/StripeTestCards';
4 |
5 | import { useShoppingCart } from 'use-shopping-cart';
6 | import { fetchPostJSON } from '../utils/api-helpers';
7 |
8 | const CartSummary = () => {
9 | const [loading, setLoading] = useState(false);
10 | const [cartEmpty, setCartEmpty] = useState(true);
11 | const {
12 | formattedTotalPrice,
13 | cartCount,
14 | clearCart,
15 | cartDetails,
16 | redirectToCheckout,
17 | } = useShoppingCart();
18 |
19 | useEffect(() => setCartEmpty(!cartCount), [cartCount]);
20 |
21 | const handleCheckout: React.FormEventHandler = async (
22 | event
23 | ) => {
24 | event.preventDefault();
25 | setLoading(true);
26 |
27 | const response = await fetchPostJSON(
28 | '/api/checkout_sessions/cart',
29 | cartDetails
30 | );
31 |
32 | if (response.statusCode === 500) {
33 | console.error(response.message);
34 | return;
35 | }
36 |
37 | redirectToCheckout({ sessionId: response.id });
38 | };
39 |
40 | return (
41 |
68 | );
69 | };
70 |
71 | export default CartSummary;
72 |
--------------------------------------------------------------------------------
/pages/api/checkout_sessions/cart.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 |
3 | /*
4 | * Product data can be loaded from anywhere. In this case, we’re loading it from
5 | * a local JSON file, but this could also come from an async call to your
6 | * inventory management service, a database query, or some other API call.
7 | *
8 | * The important thing is that the product info is loaded from somewhere trusted
9 | * so you know the pricing information is accurate.
10 | */
11 | import { validateCartItems } from 'use-shopping-cart/src/serverUtil';
12 | import inventory from '../../../data/products.json';
13 |
14 | import Stripe from 'stripe';
15 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
16 | // https://github.com/stripe/stripe-node#configuration
17 | apiVersion: '2020-03-02',
18 | });
19 |
20 | export default async function handler(
21 | req: NextApiRequest,
22 | res: NextApiResponse
23 | ) {
24 | if (req.method === 'POST') {
25 | try {
26 | // Validate the cart details that were sent from the client.
27 | const cartItems = req.body;
28 | const line_items = validateCartItems(inventory, cartItems);
29 | // Create Checkout Sessions from body params.
30 | const params: Stripe.Checkout.SessionCreateParams = {
31 | submit_type: 'pay',
32 | payment_method_types: ['card'],
33 | billing_address_collection: 'auto',
34 | shipping_address_collection: {
35 | allowed_countries: ['US', 'CA'],
36 | },
37 | line_items,
38 | success_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`,
39 | cancel_url: `${req.headers.origin}/use-shopping-cart`,
40 | };
41 | const checkoutSession: Stripe.Checkout.Session = await stripe.checkout.sessions.create(
42 | params
43 | );
44 |
45 | res.status(200).json(checkoutSession);
46 | } catch (err) {
47 | res.status(500).json({ statusCode: 500, message: err.message });
48 | }
49 | } else {
50 | res.setHeader('Allow', 'POST');
51 | res.status(405).end('Method Not Allowed');
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import Head from 'next/head';
3 | import Link from 'next/link';
4 |
5 | type Props = {
6 | children: ReactNode;
7 | title?: string;
8 | };
9 |
10 | const Layout = ({
11 | children,
12 | title = 'TypeScript Next.js Stripe Example',
13 | }: Props) => (
14 | <>
15 |
16 | {title}
17 |
18 |
19 |
20 |
21 |
22 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Stripe Sample
41 |
42 | Next.js, TypeScript, and Stripe 🔒💸
43 |
44 |
45 |
46 | {children}
47 |
48 |
69 | >
70 | );
71 |
72 | export default Layout;
73 |
--------------------------------------------------------------------------------
/public/elements-card-payment.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/api/webhooks/index.ts:
--------------------------------------------------------------------------------
1 | import { buffer } from 'micro';
2 | import Cors from 'micro-cors';
3 | import { NextApiRequest, NextApiResponse } from 'next';
4 |
5 | import Stripe from 'stripe';
6 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
7 | // https://github.com/stripe/stripe-node#configuration
8 | apiVersion: '2020-03-02',
9 | });
10 |
11 | const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET!;
12 |
13 | // Stripe requires the raw body to construct the event.
14 | export const config = {
15 | api: {
16 | bodyParser: false,
17 | },
18 | };
19 |
20 | const cors = Cors({
21 | allowMethods: ['POST', 'HEAD'],
22 | });
23 |
24 | const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
25 | if (req.method === 'POST') {
26 | const buf = await buffer(req);
27 | const sig = req.headers['stripe-signature']!;
28 |
29 | let event: Stripe.Event;
30 |
31 | try {
32 | event = stripe.webhooks.constructEvent(
33 | buf.toString(),
34 | sig,
35 | webhookSecret
36 | );
37 | } catch (err) {
38 | // On error, log and return the error message.
39 | console.log(`❌ Error message: ${err.message}`);
40 | res.status(400).send(`Webhook Error: ${err.message}`);
41 | return;
42 | }
43 |
44 | // Successfully constructed event.
45 | console.log('✅ Success:', event.id);
46 |
47 | // Cast event data to Stripe object.
48 | if (event.type === 'payment_intent.succeeded') {
49 | const paymentIntent = event.data.object as Stripe.PaymentIntent;
50 | console.log(`💰 PaymentIntent status: ${paymentIntent.status}`);
51 | } else if (event.type === 'payment_intent.payment_failed') {
52 | const paymentIntent = event.data.object as Stripe.PaymentIntent;
53 | console.log(
54 | `❌ Payment failed: ${paymentIntent.last_payment_error?.message}`
55 | );
56 | } else if (event.type === 'charge.succeeded') {
57 | const charge = event.data.object as Stripe.Charge;
58 | console.log(`💵 Charge id: ${charge.id}`);
59 | } else {
60 | console.warn(`🤷♀️ Unhandled event type: ${event.type}`);
61 | }
62 |
63 | // Return a response to acknowledge receipt of the event.
64 | res.json({ received: true });
65 | } else {
66 | res.setHeader('Allow', 'POST');
67 | res.status(405).end('Method Not Allowed');
68 | }
69 | };
70 |
71 | export default cors(webhookHandler as any);
72 |
--------------------------------------------------------------------------------
/components/CheckoutForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import CustomDonationInput from '../components/CustomDonationInput';
4 | import StripeTestCards from '../components/StripeTestCards';
5 |
6 | import getStripe from '../utils/get-stripejs';
7 | import { fetchPostJSON } from '../utils/api-helpers';
8 | import { formatAmountForDisplay } from '../utils/stripe-helpers';
9 | import * as config from '../config';
10 |
11 | const CheckoutForm = () => {
12 | const [loading, setLoading] = useState(false);
13 | const [input, setInput] = useState({
14 | customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP),
15 | });
16 |
17 | const handleInputChange: React.ChangeEventHandler = (e) =>
18 | setInput({
19 | ...input,
20 | [e.currentTarget.name]: e.currentTarget.value,
21 | });
22 |
23 | const handleSubmit: React.FormEventHandler = async (e) => {
24 | e.preventDefault();
25 | setLoading(true);
26 | // Create a Checkout Session.
27 | const response = await fetchPostJSON('/api/checkout_sessions', {
28 | amount: input.customDonation,
29 | });
30 |
31 | if (response.statusCode === 500) {
32 | console.error(response.message);
33 | return;
34 | }
35 |
36 | // Redirect to Checkout.
37 | const stripe = await getStripe();
38 | const { error } = await stripe!.redirectToCheckout({
39 | // Make the id field from the Checkout Session creation API response
40 | // available to this file, so you can provide it as parameter here
41 | // instead of the {{CHECKOUT_SESSION_ID}} placeholder.
42 | sessionId: response.id,
43 | });
44 | // If `redirectToCheckout` fails due to a browser or network
45 | // error, display the localized error message to your customer
46 | // using `error.message`.
47 | console.warn(error.message);
48 | setLoading(false);
49 | };
50 |
51 | return (
52 |
72 | );
73 | };
74 |
75 | export default CheckoutForm;
76 |
--------------------------------------------------------------------------------
/components/ElementsForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import CustomDonationInput from '../components/CustomDonationInput';
4 | import StripeTestCards from '../components/StripeTestCards';
5 | import PrintObject from '../components/PrintObject';
6 |
7 | import { fetchPostJSON } from '../utils/api-helpers';
8 | import { formatAmountForDisplay } from '../utils/stripe-helpers';
9 | import * as config from '../config';
10 |
11 | import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
12 |
13 | const CARD_OPTIONS = {
14 | iconStyle: 'solid' as const,
15 | style: {
16 | base: {
17 | iconColor: '#6772e5',
18 | color: '#6772e5',
19 | fontWeight: '500',
20 | fontFamily: 'Roboto, Open Sans, Segoe UI, sans-serif',
21 | fontSize: '16px',
22 | fontSmoothing: 'antialiased',
23 | ':-webkit-autofill': {
24 | color: '#fce883',
25 | },
26 | '::placeholder': {
27 | color: '#6772e5',
28 | },
29 | },
30 | invalid: {
31 | iconColor: '#ef2961',
32 | color: '#ef2961',
33 | },
34 | },
35 | };
36 |
37 | const ElementsForm = () => {
38 | const [input, setInput] = useState({
39 | customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP),
40 | cardholderName: '',
41 | });
42 | const [payment, setPayment] = useState({ status: 'initial' });
43 | const [errorMessage, setErrorMessage] = useState('');
44 | const stripe = useStripe();
45 | const elements = useElements();
46 |
47 | const PaymentStatus = ({ status }: { status: string }) => {
48 | switch (status) {
49 | case 'processing':
50 | case 'requires_payment_method':
51 | case 'requires_confirmation':
52 | return Processing...
;
53 |
54 | case 'requires_action':
55 | return Authenticating...
;
56 |
57 | case 'succeeded':
58 | return Payment Succeeded 🥳
;
59 |
60 | case 'error':
61 | return (
62 | <>
63 | Error 😭
64 | {errorMessage}
65 | >
66 | );
67 |
68 | default:
69 | return null;
70 | }
71 | };
72 |
73 | const handleInputChange: React.ChangeEventHandler = (e) =>
74 | setInput({
75 | ...input,
76 | [e.currentTarget.name]: e.currentTarget.value,
77 | });
78 |
79 | const handleSubmit: React.FormEventHandler = async (e) => {
80 | e.preventDefault();
81 | // Abort if form isn't valid
82 | if (!e.currentTarget.reportValidity()) return;
83 | setPayment({ status: 'processing' });
84 |
85 | // Create a PaymentIntent with the specified amount.
86 | const response = await fetchPostJSON('/api/payment_intents', {
87 | amount: input.customDonation,
88 | });
89 | setPayment(response);
90 |
91 | if (response.statusCode === 500) {
92 | setPayment({ status: 'error' });
93 | setErrorMessage(response.message);
94 | return;
95 | }
96 |
97 | // Get a reference to a mounted CardElement. Elements knows how
98 | // to find your CardElement because there can only ever be one of
99 | // each type of element.
100 | const cardElement = elements!.getElement(CardElement);
101 |
102 | // Use your card Element with other Stripe.js APIs
103 | const { error, paymentIntent } = await stripe!.confirmCardPayment(
104 | response.client_secret,
105 | {
106 | payment_method: {
107 | card: cardElement!,
108 | billing_details: { name: input.cardholderName },
109 | },
110 | }
111 | );
112 |
113 | if (error) {
114 | setPayment({ status: 'error' });
115 | setErrorMessage(error.message ?? 'An unknown error occured');
116 | } else if (paymentIntent) {
117 | setPayment(paymentIntent);
118 | }
119 | };
120 |
121 | return (
122 | <>
123 |
170 |
171 |
172 | >
173 | );
174 | };
175 |
176 | export default ElementsForm;
177 |
--------------------------------------------------------------------------------
/public/checkout-one-time-payments.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | /* Variables */
2 | :root {
3 | --body-color: #fcfdfe;
4 | --checkout-color: #8f6ed5;
5 | --elements-color: #6772e5;
6 | --body-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
7 | sans-serif;
8 | --h1-color: #1a1f36;
9 | --h2-color: #7b818a;
10 | --h3-color: #a3acb9;
11 | --radius: 6px;
12 | --container-width-max: 1280px;
13 | --page-width-max: 600px;
14 | --transition-duration: 2s;
15 | }
16 |
17 | body {
18 | margin: 0;
19 | padding: 0;
20 | background: var(--body-color);
21 | overflow-y: scroll;
22 | }
23 |
24 | * {
25 | box-sizing: border-box;
26 | font-family: var(--body-font-family);
27 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
28 | }
29 |
30 | #__next {
31 | display: flex;
32 | justify-content: center;
33 | }
34 |
35 | .container {
36 | max-width: var(--container-width-max);
37 | padding: 45px 25px;
38 | display: flex;
39 | flex-direction: row;
40 | }
41 |
42 | .page-container {
43 | padding-bottom: 60px;
44 | max-width: var(--page-width-max);
45 | }
46 |
47 | h1 {
48 | font-weight: 600;
49 | color: var(--h1-color);
50 | margin: 6px 0 12px;
51 | font-size: 27px;
52 | line-height: 32px;
53 | }
54 |
55 | h1 span.light {
56 | color: var(--h3-color);
57 | }
58 |
59 | h2 {
60 | color: var(--h2-color);
61 | margin: 8px 0;
62 | }
63 |
64 | h3 {
65 | font-size: 17px;
66 | color: var(--h3-color);
67 | margin: 8px 0;
68 | }
69 |
70 | a {
71 | color: var(--checkout-color);
72 | text-decoration: none;
73 | }
74 |
75 | header {
76 | position: relative;
77 | flex: 0 0 250px;
78 | padding-right: 48px;
79 | }
80 |
81 | .header-content {
82 | position: sticky;
83 | top: 45px;
84 | }
85 |
86 | .logo img {
87 | height: 20px;
88 | margin-bottom: 52px;
89 | }
90 |
91 | ul,
92 | li {
93 | list-style: none;
94 | padding: 0;
95 | margin: 0;
96 | }
97 |
98 | .card-list {
99 | display: flex;
100 | flex-wrap: wrap;
101 | align-content: flex-start;
102 | padding-top: 64px;
103 | }
104 |
105 | .card {
106 | display: block;
107 | border-radius: 10px;
108 | position: relative;
109 | padding: 12px;
110 | height: 320px;
111 | flex: 0 0 33%;
112 | min-width: 304px;
113 | width: 33%;
114 | margin: 0 20px 20px 0;
115 | text-decoration: none;
116 | box-shadow: -20px 20px 60px #abacad, 20px -20px 60px #ffffff;
117 | }
118 | .card h2 {
119 | color: #fff;
120 | }
121 | .card h2.bottom {
122 | position: absolute;
123 | bottom: 10px;
124 | }
125 |
126 | .card img {
127 | width: 80%;
128 | position: absolute;
129 | top: 50%;
130 | left: 50%;
131 | transform: translate(-50%, -50%);
132 | }
133 |
134 | .error-message {
135 | color: #ef2961;
136 | }
137 |
138 | .FormRow,
139 | fieldset,
140 | input[type='number'],
141 | input[type='text'] {
142 | border-radius: var(--radius);
143 | padding: 5px 12px;
144 | width: 100%;
145 | background: #fff;
146 | appearance: none;
147 | font-size: 16px;
148 | margin-top: 10px;
149 | }
150 |
151 | input[type='range'] {
152 | margin: 5px 0;
153 | width: 100%;
154 | }
155 |
156 | button {
157 | border-radius: var(--radius);
158 | color: white;
159 | font-size: larger;
160 | border: 0;
161 | padding: 12px 16px;
162 | margin-top: 10px;
163 | font-weight: 600;
164 | cursor: pointer;
165 | transition: all 0.2s ease;
166 | display: block;
167 | width: 100%;
168 | }
169 | button:disabled {
170 | opacity: 0.5;
171 | cursor: not-allowed;
172 | }
173 |
174 | .elements-style {
175 | color: var(--elements-color);
176 | border: 1px solid var(--elements-color);
177 | }
178 | .elements-style-background {
179 | background: var(--elements-color);
180 | transition: box-shadow var(--transition-duration);
181 | }
182 | .card.elements-style-background:hover {
183 | box-shadow: 20px 20px 60px #464e9c, -20px -20px 60px #8896ff;
184 | }
185 | .checkout-style {
186 | color: var(--checkout-color);
187 | border: 1px solid var(--checkout-color);
188 | }
189 | .checkout-style-background {
190 | background: var(--checkout-color);
191 | transition: box-shadow var(--transition-duration);
192 | }
193 | .card.checkout-style-background:hover {
194 | box-shadow: 20px 20px 60px #614b91, -20px -20px 60px #bd91ff;
195 | }
196 | .cart-style-background {
197 | background: teal;
198 | transition: box-shadow var(--transition-duration);
199 | }
200 | .card.cart-style-background:hover {
201 | box-shadow: 20px 20px 60px teal, -20px -20px 60px teal;
202 | }
203 |
204 | /* Products */
205 | .products {
206 | display: grid;
207 | gap: 2rem;
208 | grid-template-columns: repeat(2, 1fr);
209 | margin-top: 3rem;
210 | }
211 |
212 | .product img {
213 | max-width: 100%;
214 | }
215 |
216 | /* Test card number */
217 | .test-card-notice {
218 | display: block;
219 | margin-block-start: 1em;
220 | margin-block-end: 1em;
221 | margin-inline-start: 0px;
222 | margin-inline-end: 0px;
223 | }
224 | .card-number {
225 | display: inline;
226 | white-space: nowrap;
227 | font-family: Menlo, Consolas, monospace;
228 | color: #3c4257;
229 | font-weight: 500;
230 | }
231 | .card-number span {
232 | display: inline-block;
233 | width: 4px;
234 | }
235 |
236 | /* Code block */
237 | code,
238 | pre {
239 | font-family: 'SF Mono', 'IBM Plex Mono', 'Menlo', monospace;
240 | font-size: 12px;
241 | background: rgba(0, 0, 0, 0.03);
242 | padding: 12px;
243 | border-radius: var(--radius);
244 | max-height: 500px;
245 | width: var(--page-width-max);
246 | overflow: auto;
247 | }
248 |
249 | .banner {
250 | max-width: 825px;
251 | margin: 0 auto;
252 | font-size: 14px;
253 | background: var(--body-color);
254 | color: #6a7c94;
255 | border-radius: 50px;
256 | box-shadow: -20px 20px 60px #abacad, 20px -20px 60px #ffffff;
257 | display: flex;
258 | align-items: center;
259 | box-sizing: border-box;
260 | padding: 15px;
261 | line-height: 1.15;
262 | position: fixed;
263 | bottom: 2vh;
264 | left: 0;
265 | right: 0;
266 | text-align: center;
267 | justify-content: center;
268 | }
269 |
270 | @media only screen and (max-width: 980px) {
271 | .container {
272 | flex-direction: column;
273 | }
274 |
275 | .header-content {
276 | max-width: 280px;
277 | position: relative;
278 | top: 0;
279 | }
280 |
281 | .card {
282 | margin: 0 20px 20px 0;
283 | box-shadow: none;
284 | }
285 |
286 | .card-list {
287 | padding-top: 0;
288 | }
289 |
290 | .banner {
291 | box-shadow: none;
292 | bottom: 0;
293 | }
294 | }
295 |
296 | @media only screen and (max-width: 600px) {
297 | .container {
298 | flex-direction: column;
299 | }
300 |
301 | .card {
302 | display: block;
303 | border-radius: 8px;
304 | flex: 1 0 100%;
305 | max-width: 100%;
306 | padding-left: 0;
307 | padding-right: 0;
308 | margin: 0 0 20px 0;
309 | box-shadow: none;
310 | }
311 |
312 | .card-list {
313 | padding-top: 0;
314 | }
315 |
316 | code,
317 | pre,
318 | h3 {
319 | display: none;
320 | }
321 |
322 | .banner {
323 | box-shadow: none;
324 | bottom: 0;
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | >
2 | >
3 | > This project is deprecated and is no longer being actively maintained.
4 | >
5 | > See [vercel/next.js/examples/with-stripe-typescript](https://github.com/vercel/next.js/tree/canary/examples/with-stripe-typescript) for an alternative sample.
6 |
7 | # Sample using Next.js, TypeScript, and react-stripe-js 🔒💸
8 |
9 | This is a full-stack TypeScript example using:
10 |
11 | - Frontend:
12 | - Next.js and [SWR](https://github.com/zeit/swr)
13 | - [react-stripe-js](https://github.com/stripe/react-stripe-js) for [Checkout](https://stripe.com/checkout) and [Elements](https://stripe.com/elements)
14 | - Backend
15 | - Next.js [API routes](https://nextjs.org/docs/api-routes/introduction)
16 | - [stripe-node with TypeScript](https://github.com/stripe/stripe-node#usage-with-typescript)
17 |
18 | ## Demo
19 |
20 | - Live demo: https://nextjs-typescript-react-stripe-js.now.sh/
21 | - CodeSandbox: https://codesandbox.io/s/github/stripe-samples/nextjs-typescript-react-stripe-js
22 | - Tutorial: https://dev.to/thorwebdev/type-safe-payments-with-next-js-typescript-and-stripe-4jo7
23 |
24 | The demo is running in test mode -- use `4242424242424242` as a test card number with any CVC + future expiration date.
25 |
26 | Use the `4000000000003220` test card number to trigger a 3D Secure challenge flow.
27 |
28 | Read more about testing on Stripe at https://stripe.com/docs/testing.
29 |
30 | Shopping Cart Checkout Demo
31 |
32 |
33 |
34 | Checkout Donations Demo
35 |
36 |
37 |
38 | Elements Donations Demo
39 |
40 |
41 |
42 | ## Deploy your own
43 |
44 | Once you have access to [the environment variables you'll need](#required-configuration) from the [Stripe Dashboard](https://dashboard.stripe.com/apikeys), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
45 |
46 | [](https://vercel.com/import/select-scope?c=1&s=https://github.com/vercel/next.js/tree/canary/examples/with-stripe-typescript&id=70107786&env=NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,STRIPE_SECRET_KEY&envDescription=Enter%20your%20Stripe%20Keys&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-stripe-typescript%23required-configuration)
47 |
48 | ## Included functionality
49 |
50 | - [Global CSS styles](https://nextjs.org/blog/next-9-2#built-in-css-support-for-global-stylesheets)
51 | - Implementation of a Layout component that loads and sets up Stripe.js and Elements for usage with SSR via `loadStripe` helper: [components/Layout.tsx](components/Layout.tsx).
52 | - Stripe Checkout
53 | - Custom Amount Donation with redirect to Stripe Checkout:
54 | - Frontend: [pages/donate-with-checkout.tsx](pages/donate-with-checkout.tsx)
55 | - Backend: [pages/api/checkout_sessions/](pages/api/checkout_sessions/)
56 | - Checkout payment result page that uses [SWR](https://github.com/zeit/swr) hooks to fetch the CheckoutSession status from the API route: [pages/result.tsx](pages/result.tsx).
57 | - Stripe Elements
58 | - Custom Amount Donation with Stripe Elements & PaymentIntents (no redirect):
59 | - Frontend: [pages/donate-with-elements.tsx](pages/donate-with-checkout.tsx)
60 | - Backend: [pages/api/payment_intents/](pages/api/payment_intents/)
61 | - Webhook handling for [post-payment events](https://stripe.com/docs/payments/accept-a-payment#web-fulfillment)
62 | - By default Next.js API routes are same-origin only. To allow Stripe webhook event requests to reach our API route, we need to add `micro-cors` and [verify the webhook signature](https://stripe.com/docs/webhooks/signatures) of the event. All of this happens in [pages/api/webhooks/index.ts](pages/api/webhooks/index.ts).
63 | - Helpers
64 | - [utils/api-helpers.ts](utils/api-helpers.ts)
65 | - helpers for GET and POST requests.
66 | - [utils/stripe-helpers.ts](utils/stripe-helpers.ts)
67 | - Format amount strings properly using `Intl.NumberFormat`.
68 | - Format amount for usage with Stripe, including zero decimal currency detection.
69 |
70 | ## How to use
71 |
72 | ### Using `create-next-app`
73 |
74 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
75 |
76 | ```bash
77 | npx create-next-app --example with-stripe-typescript with-stripe-typescript-app
78 | # or
79 | yarn create next-app --example with-stripe-typescript with-stripe-typescript-app
80 | ```
81 |
82 | ### Download manually
83 |
84 | Download the example:
85 |
86 | ```bash
87 | curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-stripe-typescript
88 | cd with-stripe-typescript
89 | ```
90 |
91 | ### Required configuration
92 |
93 | Copy the `.env.local.example` file into a file named `.env.local` in the root directory of this project:
94 |
95 | ```bash
96 | cp .env.local.example .env.local
97 | ```
98 |
99 | You will need a Stripe account ([register](https://dashboard.stripe.com/register)) to run this sample. Go to the Stripe [developer dashboard](https://stripe.com/docs/development/quickstart#api-keys) to find your API keys and replace them in the `.env.local` file.
100 |
101 | ```bash
102 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
103 | STRIPE_SECRET_KEY=
104 | ```
105 |
106 | Now install the dependencies and start the development server.
107 |
108 | ```bash
109 | npm install
110 | npm run dev
111 | # or
112 | yarn
113 | yarn dev
114 | ```
115 |
116 | ### Forward webhooks to your local dev server
117 |
118 | First [install the CLI](https://stripe.com/docs/stripe-cli) and [link your Stripe account](https://stripe.com/docs/stripe-cli#link-account).
119 |
120 | Next, start the webhook forwarding:
121 |
122 | ```bash
123 | stripe listen --forward-to localhost:3000/api/webhooks
124 | ```
125 |
126 | The CLI will print a webhook secret key to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your `.env.local` file.
127 |
128 | ### Setting up a live webhook endpoint
129 |
130 | After deploying, copy the deployment URL with the webhook path (`https://your-url.now.sh/api/webhooks`) and create a live webhook endpoint [in your Stripe dashboard](https://stripe.com/docs/webhooks/setup#configure-webhook-settings).
131 |
132 | Once created, you can click to reveal your webhook's signing secret. Copy the webhook secret (`whsec_***`) and add it as a new environment variable in your [Vercel Dashboard](https://vercel.com/dashboard):
133 |
134 | - Select your newly created project.
135 | - Navigate to the Settings tab.
136 | - In the general settings scroll to the "Environment Variables" section.
137 |
138 | After adding an environment variable you will need to rebuild your project for it to become within your code. Within your project Dashboard, navigate to the "Deployments" tab, select the most recent deployment, click the overflow menu button (next to the "Visit" button) and select "Redeploy".
139 |
140 | ### Deploy on Vercel
141 |
142 | You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
143 |
144 | #### Deploy Your Local Project
145 |
146 | To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/import/git?utm_source=github&utm_medium=readme&utm_campaign=next-example).
147 |
148 | **Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file.
149 |
150 | #### Deploy from Our Template
151 |
152 | Alternatively, you can deploy using our template by clicking on the Deploy button below.
153 |
154 | [](https://vercel.com/import/select-scope?c=1&s=https://github.com/vercel/next.js/tree/canary/examples/with-stripe-typescript&id=70107786&env=NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,STRIPE_SECRET_KEY&envDescription=Enter%20your%20Stripe%20Keys&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-stripe-typescript%23required-configuration)
155 |
156 | ## Get support
157 | If you found a bug or want to suggest a new [feature/use case/sample], please [file an issue](../../issues).
158 |
159 | If you have questions, comments, or need help with code, we're here to help:
160 | - on [Discord](https://stripe.com/go/developer-chat)
161 | - on Twitter at [@StripeDev](https://twitter.com/StripeDev)
162 | - on Stack Overflow at the [stripe-payments](https://stackoverflow.com/tags/stripe-payments/info) tag
163 | - by [email](mailto:support+github@stripe.com)
164 |
165 | Sign up to [stay updated with developer news](https://go.stripe.global/dev-digest).
166 |
167 | ## Authors
168 |
169 | - [@thorsten-stripe](https://twitter.com/thorwebdev)
170 | - [@lfades](https://twitter.com/luis_fades)
171 |
--------------------------------------------------------------------------------