Skip to content

Commit

Permalink
Add Stripe TypeScript Example (vercel#10482)
Browse files Browse the repository at this point in the history
Co-authored-by: Luis Alvarez D. <luis@zeit.co>
  • Loading branch information
2 people authored and chibicode committed Feb 11, 2020
1 parent c3c1cfe commit 2919c1a
Show file tree
Hide file tree
Showing 24 changed files with 903 additions and 0 deletions.
4 changes: 4 additions & 0 deletions 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
13 changes: 13 additions & 0 deletions examples/with-stripe-typescript/.gitignore
@@ -0,0 +1,13 @@
.env
.DS_Store
.vscode

# Node files
node_modules/

# Typescript
dist

# Next.js
.next
.now
124 changes: 124 additions & 0 deletions examples/with-stripe-typescript/README.md
@@ -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 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<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 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<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 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 <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

0 comments on commit 2919c1a

Please sign in to comment.