From c6ee9765c89459664b77d223320d858ba2b46123 Mon Sep 17 00:00:00 2001 From: Thorsten Schaeff Date: Mon, 10 Feb 2020 19:29:40 +0800 Subject: [PATCH 1/8] Initial commit. --- examples/with-stripe-typescript/.env.example | 4 + examples/with-stripe-typescript/.gitignore | 13 ++ examples/with-stripe-typescript/README.md | 110 +++++++++++++ .../components/CheckoutForm.tsx | 64 ++++++++ .../components/CustomDonationInput.tsx | 38 +++++ .../components/ElementsForm.tsx | 150 ++++++++++++++++++ .../components/Layout.tsx | 64 ++++++++ .../components/PrintObject.tsx | 12 ++ .../with-stripe-typescript/config/index.ts | 6 + examples/with-stripe-typescript/next-env.d.ts | 2 + .../with-stripe-typescript/next.config.js | 9 ++ examples/with-stripe-typescript/now.json | 9 ++ examples/with-stripe-typescript/package.json | 31 ++++ .../pages/api/checkout_sessions/[id].ts | 27 ++++ .../pages/api/checkout_sessions/index.ts | 50 ++++++ .../pages/api/payment_intents/index.ts | 41 +++++ .../pages/api/webhooks/index.ts | 72 +++++++++ .../pages/donate-with-checkout.tsx | 22 +++ .../pages/donate-with-elements.tsx | 22 +++ .../with-stripe-typescript/pages/index.tsx | 23 +++ .../with-stripe-typescript/pages/result.tsx | 42 +++++ examples/with-stripe-typescript/tsconfig.json | 19 +++ .../utils/api-helpers.ts | 32 ++++ .../utils/stripe-helpers.ts | 30 ++++ 24 files changed, 892 insertions(+) create mode 100644 examples/with-stripe-typescript/.env.example create mode 100644 examples/with-stripe-typescript/.gitignore create mode 100644 examples/with-stripe-typescript/README.md create mode 100644 examples/with-stripe-typescript/components/CheckoutForm.tsx create mode 100644 examples/with-stripe-typescript/components/CustomDonationInput.tsx create mode 100644 examples/with-stripe-typescript/components/ElementsForm.tsx create mode 100644 examples/with-stripe-typescript/components/Layout.tsx create mode 100644 examples/with-stripe-typescript/components/PrintObject.tsx create mode 100644 examples/with-stripe-typescript/config/index.ts create mode 100644 examples/with-stripe-typescript/next-env.d.ts create mode 100644 examples/with-stripe-typescript/next.config.js create mode 100644 examples/with-stripe-typescript/now.json create mode 100644 examples/with-stripe-typescript/package.json create mode 100644 examples/with-stripe-typescript/pages/api/checkout_sessions/[id].ts create mode 100644 examples/with-stripe-typescript/pages/api/checkout_sessions/index.ts create mode 100644 examples/with-stripe-typescript/pages/api/payment_intents/index.ts create mode 100644 examples/with-stripe-typescript/pages/api/webhooks/index.ts create mode 100644 examples/with-stripe-typescript/pages/donate-with-checkout.tsx create mode 100644 examples/with-stripe-typescript/pages/donate-with-elements.tsx create mode 100644 examples/with-stripe-typescript/pages/index.tsx create mode 100644 examples/with-stripe-typescript/pages/result.tsx create mode 100644 examples/with-stripe-typescript/tsconfig.json create mode 100644 examples/with-stripe-typescript/utils/api-helpers.ts create mode 100644 examples/with-stripe-typescript/utils/stripe-helpers.ts diff --git a/examples/with-stripe-typescript/.env.example b/examples/with-stripe-typescript/.env.example new file mode 100644 index 0000000000000..577acb0942408 --- /dev/null +++ b/examples/with-stripe-typescript/.env.example @@ -0,0 +1,4 @@ +# Stripe keys +STRIPE_PUBLISHABLE_KEY=pk_12345 +STRIPE_SECRET_KEY=sk_12345 +STRIPE_WEBHOOK_SECRET=whsec_1234 diff --git a/examples/with-stripe-typescript/.gitignore b/examples/with-stripe-typescript/.gitignore new file mode 100644 index 0000000000000..e623a2ddab55f --- /dev/null +++ b/examples/with-stripe-typescript/.gitignore @@ -0,0 +1,13 @@ +.env +.DS_Store +.vscode + +# Node files +node_modules/ + +# Typescript +dist + +# Next.js +.next +.now \ No newline at end of file diff --git a/examples/with-stripe-typescript/README.md b/examples/with-stripe-typescript/README.md new file mode 100644 index 0000000000000..37d684076b27f --- /dev/null +++ b/examples/with-stripe-typescript/README.md @@ -0,0 +1,110 @@ +# Example using Stripe with TypeScript and react-stripe-js 🔒💸 + +- Demo: https://nextjs-typescript-react-stripe-js.now.sh/ +- CodeSandbox: https://codesandbox.io/s/nextjs-typescript-react-stripe-js-ix23n +- Tutorial: https://dev.to/thorwebdev/type-safe-payments-with-next-js-typescript-and-stripe-2a1o + +This is a full-stack TypeScript example using: + +- Frontend: + - [Next.js 9](https://nextjs.org/blog/next-9) for [SSR](https://nextjs.org/features/server-side-rendering) + - [react-stripe-js](https://github.com/stripe/react-stripe-js) for [Checkout](https://stripe.com/checkout) and [Elements](https://stripe.com/elements) +- Backend + - Next.js 9 [API routes](https://nextjs.org/blog/next-9#api-routes) + - [stripe-node with TypeScript](https://github.com/stripe/stripe-node#usage-with-typescript) + +## Included functionality + +- Making `.env` variables available to next: [next.config.js](next.config.js) + - **_NOTE_**: when deploying with Now you need to [add your secrets](https://zeit.co/docs/v2/serverless-functions/env-and-secrets) and specify a [now.json](/now.json) file. +- 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). +- Stripe Checkout + - Custom Amount Donation with redirect to Stripe Checkout: + - Frontend: [pages/donate-with-checkout.tsx](pages/donate-with-checkout.tsx) + - Backend: [pages/api/checkout_sessions/](pages/api/checkout_sessions/) + - 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). +- Stripe Elements + - Custom Amount Donation with Stripe Elements & PaymentIntents (no redirect): + - Frontend: [pages/donate-with-elements.tsx](pages/donate-with-checkout.tsx) + - Backend: [pages/api/payment_intents/](pages/api/payment_intents/) +- Webhook handling for [post-payment events](https://stripe.com/docs/payments/accept-a-payment#web-fulfillment) + - 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). +- Helpers + - [utils/api-helpers.ts](utils/api-helpers.ts) + - `isomorphic-unfetch` helpers for GET and POST requests. + - [utils/stripe-helpers.ts](utils/stripe-helpers.ts) + - Format amount strings properly using `Intl.NumberFormat`. + - Format amount for usage with Stripe, including zero decimal currency detection. + +## How to use + +### Using `create-next-app` + +Execute [`create-next-app`](https://github.com/zeit/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: + +```bash +npm init next-app --example with-stripe-typescript with-stripe-typescript-app +# or +yarn create next-app --example with-stripe-typescript with-stripe-typescript-app +``` + +### Download manually + +Download the example: + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-stripe-typescript +cd with-stripe-typescript +``` + +### Required configuration + +Copy the `.env.example` file into a file named `.env` in the root directory of this project: + + cp .env.example .env + +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#api-keys) to find your API keys and replace them in the `.env` file. + + STRIPE_PUBLISHABLE_KEY= + STRIPE_SECRET_KEY= + +Now install the dependencies and start the development server. + + npm install + npm run dev + # or + yarn + yarn dev + +### Forward webhooks to your local dev server + +First [install the CLI](https://stripe.com/docs/stripe-cli) and [link your Stripe account](https://stripe.com/docs/stripe-cli#link-account). + +Next, start the webhook forwarding: + + stripe listen --forward-to localhost:3000/api/webhooks + +The CLI will print a webhook secret key to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your `.env` file. + +### Deploy it to the cloud with Zeit Now + +Install [Now](https://zeit.co/now) ([download](https://zeit.co/download)) + +Add your Stripe [secrets to Now](https://zeit.co/docs/v2/serverless-functions/env-and-secrets): + + now secrets add stripe_publishable_key pk_*** + now secrets add stripe_secret_key sk_*** + now secrets add stripe_webhook_secret whsec_*** + +To start the deploy, run: + + now + +After the successful deploy, Now will show you the URL for your site. Copy that URL (`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). + +**_Note_** that you're live webhook will have a different secret. To update it in your deployed application you will need to first rm the existing secret and then add the new secret: + + now secrets rm stripe_webhook_secret + now secrets add stripe_webhook_secret whsec_*** + +As the secrets are pulled into the application at deploy time, we will need to redeploy our app after we made changes to the secrets. Run `now` again to redeploy with the new secret value. diff --git a/examples/with-stripe-typescript/components/CheckoutForm.tsx b/examples/with-stripe-typescript/components/CheckoutForm.tsx new file mode 100644 index 0000000000000..4e7de2c956bb0 --- /dev/null +++ b/examples/with-stripe-typescript/components/CheckoutForm.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react' + +import CustomDonationInput from '../components/CustomDonationInput' + +import { fetchPostJSON } from '../utils/api-helpers' +import { formatAmountForDisplay } from '../utils/stripe-helpers' +import * as config from '../config' + +import { useStripe } from '@stripe/react-stripe-js' + +const CheckoutForm: React.FunctionComponent = () => { + const [input, setInput] = useState({ customDonation: config.MIN_AMOUNT }) + const stripe = useStripe() + + const handleInputChange: React.ChangeEventHandler = e => + setInput({ + ...input, + [e.currentTarget.name]: e.currentTarget.value, + }) + + const handleSubmit: React.FormEventHandler = async e => { + e.preventDefault() + // Create a Checkout Session. + const response = await fetchPostJSON('/api/checkout_sessions', { + amount: input.customDonation, + }) + + if (response.statusCode === 500) { + console.error(response.message) + return + } + + // Redirect to Checkout. + const { error } = await stripe!.redirectToCheckout({ + // Make the id field from the Checkout Session creation API response + // available to this file, so you can provide it as parameter here + // instead of the {{CHECKOUT_SESSION_ID}} placeholder. + sessionId: response.id, + }) + // If `redirectToCheckout` fails due to a browser or network + // error, display the localized error message to your customer + // using `error.message`. + console.warn(error.message) + } + + return ( +
+ + + + ) +} + +export default CheckoutForm diff --git a/examples/with-stripe-typescript/components/CustomDonationInput.tsx b/examples/with-stripe-typescript/components/CustomDonationInput.tsx new file mode 100644 index 0000000000000..67e1d7a117530 --- /dev/null +++ b/examples/with-stripe-typescript/components/CustomDonationInput.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { formatAmountForDisplay } from '../utils/stripe-helpers' + +type Props = { + name: string + value: number + min: number + max: number + currency: string + step: number + onChange: (e: React.ChangeEvent) => void +} + +const CustomDonationInput: React.FunctionComponent = ({ + name, + value, + min, + max, + currency, + step, + onChange, +}) => ( + +) + +export default CustomDonationInput diff --git a/examples/with-stripe-typescript/components/ElementsForm.tsx b/examples/with-stripe-typescript/components/ElementsForm.tsx new file mode 100644 index 0000000000000..6686e1fab73fa --- /dev/null +++ b/examples/with-stripe-typescript/components/ElementsForm.tsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react' + +import CustomDonationInput from '../components/CustomDonationInput' +import PrintObject from '../components/PrintObject' + +import { fetchPostJSON } from '../utils/api-helpers' +import { formatAmountForDisplay } from '../utils/stripe-helpers' +import * as config from '../config' + +import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js' + +const ElementsForm: React.FunctionComponent = () => { + const [input, setInput] = useState({ + customDonation: config.MIN_AMOUNT, + cardholderName: '', + }) + const [payment, setPayment] = useState({ status: 'initial' }) + const [errorMessage, setErrorMessage] = useState('') + const stripe = useStripe() + const elements = useElements() + + const PaymentStatus = ({ status }: { status: string }) => { + switch (status) { + case 'processing': + case 'requires_payment_method': + case 'requires_confirmation': + return

Processing...

+ + case 'requires_action': + return

Authenticating...

+ + case 'succeeded': + return

Payment Succeeded 🥳

+ + case 'error': + return ( + <> +

Error 😭

+

{errorMessage}

+ + ) + + default: + return null + } + } + + const handleInputChange: React.ChangeEventHandler = e => + setInput({ + ...input, + [e.currentTarget.name]: e.currentTarget.value, + }) + + const handleSubmit: React.FormEventHandler = async e => { + e.preventDefault() + setPayment({ status: 'processing' }) + + // Create a PaymentIntent with the specified amount. + const response = await fetchPostJSON('/api/payment_intents', { + amount: input.customDonation, + }) + setPayment(response) + + if (response.statusCode === 500) { + setPayment({ status: 'error' }) + setErrorMessage(response.message) + return + } + + // Get a reference to a mounted CardElement. Elements knows how + // to find your CardElement because there can only ever be one of + // each type of element. + const cardElement = elements!.getElement(CardElement) + + // Use your card Element with other Stripe.js APIs + const { error, paymentIntent } = await stripe!.confirmCardPayment( + response.client_secret, + { + payment_method: { + card: cardElement!, + billing_details: { name: input.cardholderName }, + }, + } + ) + + if (error) { + setPayment({ status: 'error' }) + setErrorMessage(error.message ?? 'An unknown error occured') + } else if (paymentIntent) { + setPayment(paymentIntent) + } + } + + return ( + <> +
+ +
+ Your payment details: + + +
+ + + + + + ) +} + +export default ElementsForm diff --git a/examples/with-stripe-typescript/components/Layout.tsx b/examples/with-stripe-typescript/components/Layout.tsx new file mode 100644 index 0000000000000..572d21db586ab --- /dev/null +++ b/examples/with-stripe-typescript/components/Layout.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import Link from 'next/link' +import Head from 'next/head' +import { Elements } from '@stripe/react-stripe-js' +import { loadStripe } from '@stripe/stripe-js' + +type Props = { + title?: string +} + +const stripePromise = loadStripe(process.env.STRIPE_PUBLISHABLE_KEY!) + +const Layout: React.FunctionComponent = ({ + children, + title = 'TypeScript Next.js Stripe Example', +}) => ( + + + {title} + + + +
+ +
+ {children} + +
+) + +export default Layout diff --git a/examples/with-stripe-typescript/components/PrintObject.tsx b/examples/with-stripe-typescript/components/PrintObject.tsx new file mode 100644 index 0000000000000..8b36b678c9f96 --- /dev/null +++ b/examples/with-stripe-typescript/components/PrintObject.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +type Props = { + content: object +} + +const PrintObject: React.FunctionComponent = ({ content }) => { + const formattedContent: string = JSON.stringify(content, null, 2) + return
{formattedContent}
+} + +export default PrintObject diff --git a/examples/with-stripe-typescript/config/index.ts b/examples/with-stripe-typescript/config/index.ts new file mode 100644 index 0000000000000..b36aaf9098211 --- /dev/null +++ b/examples/with-stripe-typescript/config/index.ts @@ -0,0 +1,6 @@ +export const CURRENCY = 'usd' +// Set your amount limits: Use float for decimal currencies and +// Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal. +export const MIN_AMOUNT = 10.0 +export const MAX_AMOUNT = 5000.0 +export const AMOUNT_STEP = 5.0 diff --git a/examples/with-stripe-typescript/next-env.d.ts b/examples/with-stripe-typescript/next-env.d.ts new file mode 100644 index 0000000000000..0ed4fc8ed494f --- /dev/null +++ b/examples/with-stripe-typescript/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/with-stripe-typescript/next.config.js b/examples/with-stripe-typescript/next.config.js new file mode 100644 index 0000000000000..c1fd14db22e84 --- /dev/null +++ b/examples/with-stripe-typescript/next.config.js @@ -0,0 +1,9 @@ +require('dotenv').config() + +module.exports = { + env: { + STRIPE_PUBLISHABLE_KEY: process.env.STRIPE_PUBLISHABLE_KEY, + STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, + STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, + }, +} diff --git a/examples/with-stripe-typescript/now.json b/examples/with-stripe-typescript/now.json new file mode 100644 index 0000000000000..19119c02f98cc --- /dev/null +++ b/examples/with-stripe-typescript/now.json @@ -0,0 +1,9 @@ +{ + "build": { + "env": { + "STRIPE_PUBLISHABLE_KEY": "@stripe_publishable_key", + "STRIPE_SECRET_KEY": "@stripe_secret_key", + "STRIPE_WEBHOOK_SECRET": "@stripe_webhook_secret" + } + } +} diff --git a/examples/with-stripe-typescript/package.json b/examples/with-stripe-typescript/package.json new file mode 100644 index 0000000000000..2ceac385459b0 --- /dev/null +++ b/examples/with-stripe-typescript/package.json @@ -0,0 +1,31 @@ +{ + "name": "with-stripe-typescript", + "version": "1.0.0", + "description": "", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "license": "ISC", + "dependencies": { + "@stripe/react-stripe-js": "^1.0.0-beta.5", + "@stripe/stripe-js": "^1.0.0-beta.5", + "dotenv": "latest", + "isomorphic-unfetch": "^3.0.0", + "micro": "^9.3.4", + "micro-cors": "^0.1.1", + "next": "latest", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "stripe": "^8.8.1", + "swr": "^0.1.16" + }, + "devDependencies": { + "@types/micro": "^7.3.3", + "@types/micro-cors": "^0.1.0", + "@types/node": "^13.1.2", + "@types/react": "^16.9.17", + "typescript": "^3.7.4" + } +} diff --git a/examples/with-stripe-typescript/pages/api/checkout_sessions/[id].ts b/examples/with-stripe-typescript/pages/api/checkout_sessions/[id].ts new file mode 100644 index 0000000000000..5c7bc3a3c6e12 --- /dev/null +++ b/examples/with-stripe-typescript/pages/api/checkout_sessions/[id].ts @@ -0,0 +1,27 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +import Stripe from 'stripe' +const stripeSecretKey: string = process.env.STRIPE_SECRET_KEY! +const stripe = new Stripe(stripeSecretKey, { + // https://github.com/stripe/stripe-node#configuration + apiVersion: '2019-12-03', + typescript: true, + telemetry: true, +}) + +export default async (req: NextApiRequest, res: NextApiResponse) => { + const id: string = req.query.id as string + try { + if (!id.startsWith('cs_')) { + throw Error('Incorrect CheckoutSession ID.') + } + const checkout_session: Stripe.Checkout.Session = await stripe.checkout.sessions.retrieve( + id, + { expand: ['payment_intent'] } + ) + + res.status(200).json(checkout_session) + } catch (err) { + res.status(500).json({ statusCode: 500, message: err.message }) + } +} diff --git a/examples/with-stripe-typescript/pages/api/checkout_sessions/index.ts b/examples/with-stripe-typescript/pages/api/checkout_sessions/index.ts new file mode 100644 index 0000000000000..469c700d1135d --- /dev/null +++ b/examples/with-stripe-typescript/pages/api/checkout_sessions/index.ts @@ -0,0 +1,50 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +import { CURRENCY, MIN_AMOUNT, MAX_AMOUNT } from '../../../config' +import { formatAmountForStripe } from '../../../utils/stripe-helpers' + +import Stripe from 'stripe' +const stripeSecretKey: string = process.env.STRIPE_SECRET_KEY! +const stripe = new Stripe(stripeSecretKey, { + // https://github.com/stripe/stripe-node#configuration + apiVersion: '2019-12-03', + typescript: true, + telemetry: true, +}) + +export default async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'POST') { + const amount: number = req.body.amount + try { + // Validate the amount that was passed from the client. + if (!(amount >= MIN_AMOUNT && amount <= MAX_AMOUNT)) { + throw new Error('Invalid amount.') + } + // Create Checkout Sessions from body params. + const params: Stripe.Checkout.SessionCreateParams = { + submit_type: 'donate', + payment_method_types: ['card'], + line_items: [ + { + name: 'Custom amount donation', + amount: formatAmountForStripe(amount, CURRENCY), + currency: CURRENCY, + quantity: 1, + }, + ], + success_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`, + } + const checkoutSession: Stripe.Checkout.Session = await stripe.checkout.sessions.create( + params + ) + + res.status(200).json(checkoutSession) + } catch (err) { + res.status(500).json({ statusCode: 500, message: err.message }) + } + } else { + res.setHeader('Allow', 'POST') + res.status(405).end('Method Not Allowed') + } +} diff --git a/examples/with-stripe-typescript/pages/api/payment_intents/index.ts b/examples/with-stripe-typescript/pages/api/payment_intents/index.ts new file mode 100644 index 0000000000000..7e6cc9d56d2f1 --- /dev/null +++ b/examples/with-stripe-typescript/pages/api/payment_intents/index.ts @@ -0,0 +1,41 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +import { CURRENCY, MIN_AMOUNT, MAX_AMOUNT } from '../../../config' +import { formatAmountForStripe } from '../../../utils/stripe-helpers' + +import Stripe from 'stripe' +const stripeSecretKey: string = process.env.STRIPE_SECRET_KEY! +const stripe = new Stripe(stripeSecretKey, { + // https://github.com/stripe/stripe-node#configuration + apiVersion: '2019-12-03', + typescript: true, + telemetry: true, +}) + +export default async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'POST') { + const { amount }: { amount: number } = req.body + try { + // Validate the amount that was passed from the client. + if (!(amount >= MIN_AMOUNT && amount <= MAX_AMOUNT)) { + throw new Error('Invalid amount.') + } + // Create PaymentIntent from body params. + const params: Stripe.PaymentIntentCreateParams = { + payment_method_types: ['card'], + amount: formatAmountForStripe(amount, CURRENCY), + currency: CURRENCY, + } + const payment_intent: Stripe.PaymentIntent = await stripe.paymentIntents.create( + params + ) + + res.status(200).json(payment_intent) + } catch (err) { + res.status(500).json({ statusCode: 500, message: err.message }) + } + } else { + res.setHeader('Allow', 'POST') + res.status(405).end('Method Not Allowed') + } +} diff --git a/examples/with-stripe-typescript/pages/api/webhooks/index.ts b/examples/with-stripe-typescript/pages/api/webhooks/index.ts new file mode 100644 index 0000000000000..700d43594cab2 --- /dev/null +++ b/examples/with-stripe-typescript/pages/api/webhooks/index.ts @@ -0,0 +1,72 @@ +import { buffer } from 'micro' +import Cors from 'micro-cors' +import { NextApiRequest, NextApiResponse } from 'next' + +import Stripe from 'stripe' +const stripeSecretKey: string = process.env.STRIPE_SECRET_KEY! +const stripe = new Stripe(stripeSecretKey, { + // https://github.com/stripe/stripe-node#configuration + apiVersion: '2019-12-03', + typescript: true, + telemetry: true, +}) + +const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET! + +// Stripe requires the raw body to construct the event. +export const config = { + api: { + bodyParser: false, + }, +} + +const cors = Cors({ + allowMethods: ['POST', 'HEAD'], +}) + +const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'POST') { + const buf = await buffer(req) + const sig = req.headers['stripe-signature']! + + let event: Stripe.Event + + try { + event = stripe.webhooks.constructEvent(buf.toString(), sig, webhookSecret) + } catch (err) { + // On error, log and return the error message. + console.log(`❌ Error message: ${err.message}`) + res.status(400).send(`Webhook Error: ${err.message}`) + return + } + + // Successfully constructed event. + console.log('✅ Success:', event.id) + + // Cast event data to Stripe object. + if (event.type === 'payment_intent.succeeded') { + const stripeObject: Stripe.PaymentIntent = event.data + .object as Stripe.PaymentIntent + console.log(`💰 PaymentIntent status: ${stripeObject.status}`) + } else if (event.type === 'charge.succeeded') { + const stripeObject = event.data.object as Stripe.Charge + console.log(`💵 Charge id: ${stripeObject.id}`) + } else if (event.type === 'payment_intent.payment_failed') { + const stripeObject: Stripe.PaymentIntent = event.data + .object as Stripe.PaymentIntent + console.log( + `❌ Payment failed: ${stripeObject.last_payment_error?.message}` + ) + } else { + console.warn(`🤷‍♀️ Unhandled event type: ${event.type}`) + } + + // Return a response to acknowledge receipt of the event. + res.json({ received: true }) + } else { + res.setHeader('Allow', 'POST') + res.status(405).end('Method Not Allowed') + } +} + +export default cors(webhookHandler as any) diff --git a/examples/with-stripe-typescript/pages/donate-with-checkout.tsx b/examples/with-stripe-typescript/pages/donate-with-checkout.tsx new file mode 100644 index 0000000000000..ed74503f00467 --- /dev/null +++ b/examples/with-stripe-typescript/pages/donate-with-checkout.tsx @@ -0,0 +1,22 @@ +import { NextPage } from 'next' +import Link from 'next/link' +import Layout from '../components/Layout' + +import CheckoutForm from '../components/CheckoutForm' + +const DonatePage: NextPage = () => { + return ( + +

Donate with Checkout

+

Donate to our project 💖

+ +

+ + Go home + +

+
+ ) +} + +export default DonatePage diff --git a/examples/with-stripe-typescript/pages/donate-with-elements.tsx b/examples/with-stripe-typescript/pages/donate-with-elements.tsx new file mode 100644 index 0000000000000..f5c3a4326b05a --- /dev/null +++ b/examples/with-stripe-typescript/pages/donate-with-elements.tsx @@ -0,0 +1,22 @@ +import { NextPage } from 'next' +import Link from 'next/link' +import Layout from '../components/Layout' + +import ElementsForm from '../components/ElementsForm' + +const DonatePage: NextPage = () => { + return ( + +

Donate with Elements

+

Donate to our project 💖

+ +

+ + Go home + +

+
+ ) +} + +export default DonatePage diff --git a/examples/with-stripe-typescript/pages/index.tsx b/examples/with-stripe-typescript/pages/index.tsx new file mode 100644 index 0000000000000..ad255e3019aad --- /dev/null +++ b/examples/with-stripe-typescript/pages/index.tsx @@ -0,0 +1,23 @@ +import { NextPage } from 'next' +import Link from 'next/link' +import Layout from '../components/Layout' + +const IndexPage: NextPage = () => { + return ( + +

Type-safe Payments with Next.js, TypeScript, and Stripe 🔒💸

+

+ + Donate with Checkout + +

+

+ + Donate with Elements + +

+
+ ) +} + +export default IndexPage diff --git a/examples/with-stripe-typescript/pages/result.tsx b/examples/with-stripe-typescript/pages/result.tsx new file mode 100644 index 0000000000000..26a9e39c5ae39 --- /dev/null +++ b/examples/with-stripe-typescript/pages/result.tsx @@ -0,0 +1,42 @@ +import { NextPage } from 'next' +import { useRouter } from 'next/router' +import Link from 'next/link' +import Layout from '../components/Layout' +import PrintObject from '../components/PrintObject' + +import { fetchGetJSON } from '../utils/api-helpers' +import useSWR from 'swr' + +const ResultPage: NextPage = () => { + const router = useRouter() + + // Fetch CheckoutSession from static page via + // https://nextjs.org/docs/basic-features/data-fetching#static-generation + const { data, error } = useSWR( + router.query.session_id + ? `/api/checkout_sessions/${router.query.session_id}` + : null, + fetchGetJSON + ) + + if (error) return
failed to load
+ + return ( + +

Checkout Payment Result

+

Status: {data?.payment_intent?.status ?? 'loading...'}

+

+ Your Checkout Session ID:{' '} + {router.query.session_id ?? 'loading...'} +

+ +

+ + Go home + +

+
+ ) +} + +export default ResultPage diff --git a/examples/with-stripe-typescript/tsconfig.json b/examples/with-stripe-typescript/tsconfig.json new file mode 100644 index 0000000000000..193d144c0987a --- /dev/null +++ b/examples/with-stripe-typescript/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "exclude": ["node_modules"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] +} diff --git a/examples/with-stripe-typescript/utils/api-helpers.ts b/examples/with-stripe-typescript/utils/api-helpers.ts new file mode 100644 index 0000000000000..c4e918c8da182 --- /dev/null +++ b/examples/with-stripe-typescript/utils/api-helpers.ts @@ -0,0 +1,32 @@ +import fetch from 'isomorphic-unfetch' + +export async function fetchGetJSON(url: string) { + try { + const data = await fetch(url).then(res => res.json()) + return data + } catch (err) { + throw new Error(err.message) + } +} + +export async function fetchPostJSON(url: string, data?: {}) { + try { + // Default options are marked with * + const response = await fetch(url, { + method: 'POST', // *GET, POST, PUT, DELETE, etc. + mode: 'cors', // no-cors, *cors, same-origin + cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached + credentials: 'same-origin', // include, *same-origin, omit + headers: { + 'Content-Type': 'application/json', + // 'Content-Type': 'application/x-www-form-urlencoded', + }, + redirect: 'follow', // manual, *follow, error + referrerPolicy: 'no-referrer', // no-referrer, *client + body: JSON.stringify(data || {}), // body data type must match "Content-Type" header + }) + return await response.json() // parses JSON response into native JavaScript objects + } catch (err) { + throw new Error(err.message) + } +} diff --git a/examples/with-stripe-typescript/utils/stripe-helpers.ts b/examples/with-stripe-typescript/utils/stripe-helpers.ts new file mode 100644 index 0000000000000..06c46e6974be0 --- /dev/null +++ b/examples/with-stripe-typescript/utils/stripe-helpers.ts @@ -0,0 +1,30 @@ +export function formatAmountForDisplay( + amount: number, + currency: string +): string { + let numberFormat = new Intl.NumberFormat(['en-US'], { + style: 'currency', + currency: currency, + currencyDisplay: 'symbol', + }) + return numberFormat.format(amount) +} + +export function formatAmountForStripe( + amount: number, + currency: string +): number { + let numberFormat = new Intl.NumberFormat(['en-US'], { + style: 'currency', + currency: currency, + currencyDisplay: 'symbol', + }) + const parts = numberFormat.formatToParts(amount) + let zeroDecimalCurrency: boolean = true + for (let part of parts) { + if (part.type === 'decimal') { + zeroDecimalCurrency = false + } + } + return zeroDecimalCurrency ? amount : Math.round(amount * 100) +} From 71de55714e8210356118130ff7913384c441c81f Mon Sep 17 00:00:00 2001 From: Thorsten Schaeff Date: Tue, 11 Feb 2020 00:48:10 +0800 Subject: [PATCH 2/8] Update stripe-js --- examples/with-stripe-typescript/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-stripe-typescript/package.json b/examples/with-stripe-typescript/package.json index 2ceac385459b0..f89d7ce03a328 100644 --- a/examples/with-stripe-typescript/package.json +++ b/examples/with-stripe-typescript/package.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@stripe/react-stripe-js": "^1.0.0-beta.5", - "@stripe/stripe-js": "^1.0.0-beta.5", + "@stripe/stripe-js": "^1.0.0-beta.6", "dotenv": "latest", "isomorphic-unfetch": "^3.0.0", "micro": "^9.3.4", From ea855683add534e92e40fbf4a95abcf804d14739 Mon Sep 17 00:00:00 2001 From: Thorsten Schaeff Date: Tue, 11 Feb 2020 01:15:30 +0800 Subject: [PATCH 3/8] Update links. --- examples/with-stripe-typescript/README.md | 2 +- examples/with-stripe-typescript/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/with-stripe-typescript/README.md b/examples/with-stripe-typescript/README.md index 37d684076b27f..e459e068641f8 100644 --- a/examples/with-stripe-typescript/README.md +++ b/examples/with-stripe-typescript/README.md @@ -1,7 +1,7 @@ # Example using Stripe with TypeScript and react-stripe-js 🔒💸 - Demo: https://nextjs-typescript-react-stripe-js.now.sh/ -- CodeSandbox: https://codesandbox.io/s/nextjs-typescript-react-stripe-js-ix23n +- CodeSandbox: https://codesandbox.io/s/nextjs-typescript-react-stripe-js-rqrss - Tutorial: https://dev.to/thorwebdev/type-safe-payments-with-next-js-typescript-and-stripe-2a1o This is a full-stack TypeScript example using: diff --git a/examples/with-stripe-typescript/package.json b/examples/with-stripe-typescript/package.json index f89d7ce03a328..b519ad5073b27 100644 --- a/examples/with-stripe-typescript/package.json +++ b/examples/with-stripe-typescript/package.json @@ -1,7 +1,7 @@ { "name": "with-stripe-typescript", "version": "1.0.0", - "description": "", + "description": "Full-stack TypeScript sample using Next.js, react-stripe-js, and stripe-node.", "scripts": { "dev": "next", "build": "next build", From fc81283543d68e71ee188ecd2409654afd485a2a Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 10 Feb 2020 19:29:57 -0500 Subject: [PATCH 4/8] Remove isomorphic-unfetch --- examples/with-stripe-typescript/package.json | 1 - examples/with-stripe-typescript/utils/api-helpers.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/examples/with-stripe-typescript/package.json b/examples/with-stripe-typescript/package.json index b519ad5073b27..732510870ffbf 100644 --- a/examples/with-stripe-typescript/package.json +++ b/examples/with-stripe-typescript/package.json @@ -12,7 +12,6 @@ "@stripe/react-stripe-js": "^1.0.0-beta.5", "@stripe/stripe-js": "^1.0.0-beta.6", "dotenv": "latest", - "isomorphic-unfetch": "^3.0.0", "micro": "^9.3.4", "micro-cors": "^0.1.1", "next": "latest", diff --git a/examples/with-stripe-typescript/utils/api-helpers.ts b/examples/with-stripe-typescript/utils/api-helpers.ts index c4e918c8da182..13d8772ec8630 100644 --- a/examples/with-stripe-typescript/utils/api-helpers.ts +++ b/examples/with-stripe-typescript/utils/api-helpers.ts @@ -1,5 +1,3 @@ -import fetch from 'isomorphic-unfetch' - export async function fetchGetJSON(url: string) { try { const data = await fetch(url).then(res => res.json()) From 1113f0cb2c263645b4ab85cecaeb77786605dff8 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 10 Feb 2020 19:36:06 -0500 Subject: [PATCH 5/8] Updated readme --- examples/with-stripe-typescript/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/with-stripe-typescript/README.md b/examples/with-stripe-typescript/README.md index e459e068641f8..5695cb45eb94a 100644 --- a/examples/with-stripe-typescript/README.md +++ b/examples/with-stripe-typescript/README.md @@ -7,13 +7,13 @@ This is a full-stack TypeScript example using: - Frontend: - - [Next.js 9](https://nextjs.org/blog/next-9) for [SSR](https://nextjs.org/features/server-side-rendering) + - Next.js and [SWR](https://github.com/zeit/swr) - [react-stripe-js](https://github.com/stripe/react-stripe-js) for [Checkout](https://stripe.com/checkout) and [Elements](https://stripe.com/elements) - Backend - - Next.js 9 [API routes](https://nextjs.org/blog/next-9#api-routes) + - Next.js [API routes](https://nextjs.org/docs/api-routes/introduction) - [stripe-node with TypeScript](https://github.com/stripe/stripe-node#usage-with-typescript) -## Included functionality +### Included functionality - Making `.env` variables available to next: [next.config.js](next.config.js) - **_NOTE_**: when deploying with Now you need to [add your secrets](https://zeit.co/docs/v2/serverless-functions/env-and-secrets) and specify a [now.json](/now.json) file. @@ -86,7 +86,7 @@ Next, start the webhook forwarding: The CLI will print a webhook secret key to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your `.env` file. -### Deploy it to the cloud with Zeit Now +## Deploy it to the cloud with Zeit Now Install [Now](https://zeit.co/now) ([download](https://zeit.co/download)) From 4280643f18c8e49e5d0749f253e9f01d642a48b8 Mon Sep 17 00:00:00 2001 From: Thorsten Schaeff Date: Tue, 11 Feb 2020 09:04:02 +0800 Subject: [PATCH 6/8] Don't include secrets during build. --- examples/with-stripe-typescript/README.md | 2 +- examples/with-stripe-typescript/next.config.js | 2 -- examples/with-stripe-typescript/now.json | 8 +++++--- examples/with-stripe-typescript/package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/with-stripe-typescript/README.md b/examples/with-stripe-typescript/README.md index e459e068641f8..ce92d0671f52c 100644 --- a/examples/with-stripe-typescript/README.md +++ b/examples/with-stripe-typescript/README.md @@ -107,4 +107,4 @@ After the successful deploy, Now will show you the URL for your site. Copy that now secrets rm stripe_webhook_secret now secrets add stripe_webhook_secret whsec_*** -As the secrets are pulled into the application at deploy time, we will need to redeploy our app after we made changes to the secrets. Run `now` again to redeploy with the new secret value. +As the secrets are set as env vars in the project at deploy time, we will need to redeploy our app after we made changes to the secrets. Run `now` again to redeploy with the new secret value. diff --git a/examples/with-stripe-typescript/next.config.js b/examples/with-stripe-typescript/next.config.js index c1fd14db22e84..604069d81a0c7 100644 --- a/examples/with-stripe-typescript/next.config.js +++ b/examples/with-stripe-typescript/next.config.js @@ -3,7 +3,5 @@ require('dotenv').config() module.exports = { env: { STRIPE_PUBLISHABLE_KEY: process.env.STRIPE_PUBLISHABLE_KEY, - STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, - STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, }, } diff --git a/examples/with-stripe-typescript/now.json b/examples/with-stripe-typescript/now.json index 19119c02f98cc..527e1aaee55be 100644 --- a/examples/with-stripe-typescript/now.json +++ b/examples/with-stripe-typescript/now.json @@ -1,9 +1,11 @@ { + "env": { + "STRIPE_SECRET_KEY": "@stripe_secret_key", + "STRIPE_WEBHOOK_SECRET": "@stripe_webhook_secret" + }, "build": { "env": { - "STRIPE_PUBLISHABLE_KEY": "@stripe_publishable_key", - "STRIPE_SECRET_KEY": "@stripe_secret_key", - "STRIPE_WEBHOOK_SECRET": "@stripe_webhook_secret" + "STRIPE_PUBLISHABLE_KEY": "@stripe_publishable_key" } } } diff --git a/examples/with-stripe-typescript/package.json b/examples/with-stripe-typescript/package.json index b519ad5073b27..35887c7c01e0a 100644 --- a/examples/with-stripe-typescript/package.json +++ b/examples/with-stripe-typescript/package.json @@ -1,7 +1,7 @@ { "name": "with-stripe-typescript", "version": "1.0.0", - "description": "Full-stack TypeScript sample using Next.js, react-stripe-js, and stripe-node.", + "description": "Full-stack TypeScript example using Next.js, react-stripe-js, and stripe-node.", "scripts": { "dev": "next", "build": "next build", From 434c1079b65f957efbfa3624698ae9791e62a210 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 10 Feb 2020 21:55:20 -0500 Subject: [PATCH 7/8] Updated readme --- examples/with-stripe-typescript/README.md | 48 +++++++++++++++-------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/examples/with-stripe-typescript/README.md b/examples/with-stripe-typescript/README.md index 9477ae16c5ea5..73e2a25c283ca 100644 --- a/examples/with-stripe-typescript/README.md +++ b/examples/with-stripe-typescript/README.md @@ -61,20 +61,26 @@ cd with-stripe-typescript Copy the `.env.example` file into a file named `.env` in the root directory of this project: - cp .env.example .env +```bash +cp .env.example .env +``` 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#api-keys) to find your API keys and replace them in the `.env` file. - STRIPE_PUBLISHABLE_KEY= - STRIPE_SECRET_KEY= +```bash +STRIPE_PUBLISHABLE_KEY= +STRIPE_SECRET_KEY= +``` Now install the dependencies and start the development server. - npm install - npm run dev - # or - yarn - yarn dev +```bash +npm install +npm run dev +# or +yarn +yarn dev +``` ### Forward webhooks to your local dev server @@ -82,29 +88,37 @@ First [install the CLI](https://stripe.com/docs/stripe-cli) and [link your Strip Next, start the webhook forwarding: - stripe listen --forward-to localhost:3000/api/webhooks +```bash +stripe listen --forward-to localhost:3000/api/webhooks +``` The CLI will print a webhook secret key to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your `.env` file. -## Deploy it to the cloud with Zeit Now +### Deploy it to the cloud with ZEIT Now Install [Now](https://zeit.co/now) ([download](https://zeit.co/download)) Add your Stripe [secrets to Now](https://zeit.co/docs/v2/serverless-functions/env-and-secrets): - now secrets add stripe_publishable_key pk_*** - now secrets add stripe_secret_key sk_*** - now secrets add stripe_webhook_secret whsec_*** +```bash +now secrets add stripe_publishable_key pk_*** +now secrets add stripe_secret_key sk_*** +now secrets add stripe_webhook_secret whsec_*** +``` To start the deploy, run: - now +```bash +now +``` After the successful deploy, Now will show you the URL for your site. Copy that URL (`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). -**_Note_** that you're live webhook will have a different secret. To update it in your deployed application you will need to first rm the existing secret and then add the new secret: +**_Note_** that your live webhook will have a different secret. To update it in your deployed application you will need to first remove the existing secret and then add the new secret: - now secrets rm stripe_webhook_secret - now secrets add stripe_webhook_secret whsec_*** +```bash +now secrets rm stripe_webhook_secret +now secrets add stripe_webhook_secret whsec_*** +``` As the secrets are set as env vars in the project at deploy time, we will need to redeploy our app after we made changes to the secrets. Run `now` again to redeploy with the new secret value. From 6fd90b3b9703cebceea8b470792f49a4d0735175 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 10 Feb 2020 22:12:38 -0500 Subject: [PATCH 8/8] Remove isomorphic-unfetch from the readme --- examples/with-stripe-typescript/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-stripe-typescript/README.md b/examples/with-stripe-typescript/README.md index 73e2a25c283ca..8156cd9a815e0 100644 --- a/examples/with-stripe-typescript/README.md +++ b/examples/with-stripe-typescript/README.md @@ -31,7 +31,7 @@ This is a full-stack TypeScript example using: - 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). - Helpers - [utils/api-helpers.ts](utils/api-helpers.ts) - - `isomorphic-unfetch` helpers for GET and POST requests. + - helpers for GET and POST requests. - [utils/stripe-helpers.ts](utils/stripe-helpers.ts) - Format amount strings properly using `Intl.NumberFormat`. - Format amount for usage with Stripe, including zero decimal currency detection.