forked from vercel/next.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Stripe TypeScript Example (vercel#10482)
Co-authored-by: Luis Alvarez D. <[email protected]>
- Loading branch information
Showing
24 changed files
with
903 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Stripe keys | ||
STRIPE_PUBLISHABLE_KEY=pk_12345 | ||
STRIPE_SECRET_KEY=sk_12345 | ||
STRIPE_WEBHOOK_SECRET=whsec_1234 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
.env | ||
.DS_Store | ||
.vscode | ||
|
||
# Node files | ||
node_modules/ | ||
|
||
# Typescript | ||
dist | ||
|
||
# Next.js | ||
.next | ||
.now |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
# 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-rqrss | ||
- 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 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 [API routes](https://nextjs.org/docs/api-routes/introduction) | ||
- [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) | ||
- 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: | ||
|
||
```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. | ||
|
||
```bash | ||
STRIPE_PUBLISHABLE_KEY=<replace-with-your-publishable-key> | ||
STRIPE_SECRET_KEY=<replace-with-your-secret-key> | ||
``` | ||
|
||
Now install the dependencies and start the development server. | ||
|
||
```bash | ||
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: | ||
|
||
```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 | ||
|
||
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): | ||
|
||
```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: | ||
|
||
```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 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: | ||
|
||
```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. |
64 changes: 64 additions & 0 deletions
64
examples/with-stripe-typescript/components/CheckoutForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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<HTMLInputElement> = e => | ||
setInput({ | ||
...input, | ||
[e.currentTarget.name]: e.currentTarget.value, | ||
}) | ||
|
||
const handleSubmit: React.FormEventHandler<HTMLFormElement> = 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 ( | ||
<form onSubmit={handleSubmit}> | ||
<CustomDonationInput | ||
name={'customDonation'} | ||
value={input.customDonation} | ||
min={config.MIN_AMOUNT} | ||
max={config.MAX_AMOUNT} | ||
step={config.AMOUNT_STEP} | ||
currency={config.CURRENCY} | ||
onChange={handleInputChange} | ||
/> | ||
<button type="submit" disabled={!stripe}> | ||
Donate {formatAmountForDisplay(input.customDonation, config.CURRENCY)} | ||
</button> | ||
</form> | ||
) | ||
} | ||
|
||
export default CheckoutForm |
38 changes: 38 additions & 0 deletions
38
examples/with-stripe-typescript/components/CustomDonationInput.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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<HTMLInputElement>) => void | ||
} | ||
|
||
const CustomDonationInput: React.FunctionComponent<Props> = ({ | ||
name, | ||
value, | ||
min, | ||
max, | ||
currency, | ||
step, | ||
onChange, | ||
}) => ( | ||
<label> | ||
Custom donation amount ({formatAmountForDisplay(min, currency)}- | ||
{formatAmountForDisplay(max, currency)}): | ||
<input | ||
type="number" | ||
name={name} | ||
value={value} | ||
min={min} | ||
max={max} | ||
step={step} | ||
onChange={onChange} | ||
></input> | ||
</label> | ||
) | ||
|
||
export default CustomDonationInput |
150 changes: 150 additions & 0 deletions
150
examples/with-stripe-typescript/components/ElementsForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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 <h2>Processing...</h2> | ||
|
||
case 'requires_action': | ||
return <h2>Authenticating...</h2> | ||
|
||
case 'succeeded': | ||
return <h2>Payment Succeeded 🥳</h2> | ||
|
||
case 'error': | ||
return ( | ||
<> | ||
<h2>Error 😭</h2> | ||
<p>{errorMessage}</p> | ||
</> | ||
) | ||
|
||
default: | ||
return null | ||
} | ||
} | ||
|
||
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = e => | ||
setInput({ | ||
...input, | ||
[e.currentTarget.name]: e.currentTarget.value, | ||
}) | ||
|
||
const handleSubmit: React.FormEventHandler<HTMLFormElement> = 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 ( | ||
<> | ||
<form onSubmit={handleSubmit}> | ||
<CustomDonationInput | ||
name="customDonation" | ||
value={input.customDonation} | ||
min={config.MIN_AMOUNT} | ||
max={config.MAX_AMOUNT} | ||
step={config.AMOUNT_STEP} | ||
currency={config.CURRENCY} | ||
onChange={handleInputChange} | ||
/> | ||
<fieldset> | ||
<legend>Your payment details:</legend> | ||
<label> | ||
Cardholder name: | ||
<input | ||
type="Text" | ||
name="cardholderName" | ||
onChange={handleInputChange} | ||
required={true} | ||
/> | ||
</label> | ||
<CardElement | ||
options={{ | ||
style: { | ||
base: { | ||
fontSize: '16px', | ||
color: '#424770', | ||
'::placeholder': { | ||
color: '#aab7c4', | ||
}, | ||
}, | ||
invalid: { | ||
color: '#9e2146', | ||
}, | ||
}, | ||
}} | ||
/> | ||
</fieldset> | ||
<button | ||
type="submit" | ||
disabled={ | ||
!['initial', 'succeeded', 'error'].includes(payment.status) || | ||
!stripe | ||
} | ||
> | ||
Donate {formatAmountForDisplay(input.customDonation, config.CURRENCY)} | ||
</button> | ||
</form> | ||
<PaymentStatus status={payment.status} /> | ||
<PrintObject content={payment} /> | ||
</> | ||
) | ||
} | ||
|
||
export default ElementsForm |
Oops, something went wrong.