├── now.json ├── .prettierrc ├── next-env.d.ts ├── next.config.js ├── public └── favicon.png ├── tailwind.config.js ├── components ├── Logo.tsx ├── GitHub.tsx ├── Nav.tsx ├── FixedCharge.tsx └── Subscription.tsx ├── .gitignore ├── README.md ├── postcss.config.js ├── statickit.json ├── tsconfig.json ├── package.json ├── style.css ├── LICENSE └── pages ├── _app.tsx ├── index.tsx └── subscription.tsx /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "statickit" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | siteId: 'f839109ab2d6' 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/statickit-projects/stripe-react/HEAD/public/favicon.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: {} 4 | }, 5 | variants: { 6 | boxShadow: ['responsive', 'hover', 'focus', 'focus-within'], 7 | backgroundColor: ['responsive', 'hover', 'focus', 'active'], 8 | opacity: ['responsive', 'hover', 'focus', 'disabled'] 9 | }, 10 | plugins: [] 11 | }; 12 | -------------------------------------------------------------------------------- /components/Logo.tsx: -------------------------------------------------------------------------------- 1 | const Logo = () => { 2 | return ( 3 | ` 6 | }} 7 | /> 8 | ); 9 | }; 10 | 11 | export default Logo; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stripe React Example 2 | 3 | How to build Stripe payment forms with React & [StaticKit](https://statickit.com). 4 | 5 | [**Read the guide →**](https://statickit.com/guides/stripe-payments-react) 6 | 7 | ## Usage 8 | 9 | To run this example: 10 | 11 | ```bash 12 | git clone https://github.com/statickit-projects/stripe-react.git 13 | cd stripe-react 14 | npm install 15 | npm run dev 16 | ``` 17 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | ...(process.env.NODE_ENV === `production` 6 | ? { 7 | '@fullhuman/postcss-purgecss': { 8 | content: [`./components/**/*.tsx`, `./pages/**/*.tsx`], 9 | defaultExtractor: content => content.match(/[\w-/:]+(? { 2 | return ( 3 | GitHub` 6 | }} 7 | /> 8 | ); 9 | }; 10 | 11 | export default GitHub; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@statickit/functions": "github:unstacked/statickit-functions#30f0390a-e66a-4077-bc9a-0f59441ec245", 12 | "@statickit/react": "^2.0.1", 13 | "@stripe/react-stripe-js": "^1.0.0-beta.3", 14 | "@stripe/stripe-js": "^1.0.0-beta.4", 15 | "fathom-client": "^2.0.2", 16 | "next": "9.3.2", 17 | "react": "16.12.0", 18 | "react-dom": "16.12.0" 19 | }, 20 | "devDependencies": { 21 | "@fullhuman/postcss-purgecss": "^2.0.5", 22 | "@types/node": "^13.5.2", 23 | "@types/react": "^16.9.19", 24 | "autoprefixer": "^9.7.4", 25 | "tailwindcss": "^1.1.4", 26 | "typescript": "^3.7.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* purgecss start ignore */ 2 | @tailwind base; 3 | @tailwind components; 4 | 5 | .StripeElement { 6 | @apply p-4 rounded bg-gray-800 shadow-lg text-gray-200 leading-tight; 7 | } 8 | 9 | .StripeElement--focus { 10 | @apply shadow-outline; 11 | } 12 | 13 | .input-wrapper { 14 | @apply flex p-4 rounded bg-gray-800 shadow-lg text-gray-200 leading-tight; 15 | } 16 | 17 | .input-wrapper:focus-within { 18 | @apply shadow-outline; 19 | } 20 | 21 | .input-wrapper > input::placeholder { 22 | @apply opacity-100; 23 | } 24 | 25 | .btn { 26 | @apply p-4 bg-purple-600 shadow-lg leading-tight text-white font-bold rounded w-full; 27 | } 28 | 29 | .btn:active { 30 | @apply bg-purple-700; 31 | transform: translateY(1px); 32 | } 33 | 34 | .btn:disabled { 35 | @apply opacity-75; 36 | } 37 | 38 | /* purgecss end ignore */ 39 | 40 | @tailwind utilities; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Unstack, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import Head from 'next/head'; 3 | import Router from 'next/router'; 4 | import { StaticKitProvider } from '@statickit/react'; 5 | import { loadStripe } from '@stripe/stripe-js'; 6 | import { Elements } from '@stripe/react-stripe-js'; 7 | import * as Fathom from 'fathom-client'; 8 | import '../style.css'; 9 | 10 | const stripePromise = loadStripe('pk_test_AEGjmWosrdHvOvnujk0cNHjQ'); 11 | 12 | Router.events.on('routeChangeComplete', () => { 13 | Fathom.trackPageview(); 14 | }); 15 | 16 | function App({ Component, pageProps }) { 17 | useEffect(() => { 18 | if (process.env.NODE_ENV === 'production') { 19 | Fathom.load(); 20 | Fathom.setSiteId('ZFEWBXJZ'); 21 | Fathom.trackPageview(); 22 | } 23 | }, []); 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 |
36 | ); 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import Nav from '../components/Nav'; 4 | import FixedCharge from '../components/FixedCharge'; 5 | 6 | const FixedChargePage = () => { 7 | return ( 8 |
9 | 10 | Stripe React Examples 11 | 12 | 13 |
44 | ); 45 | }; 46 | 47 | export default FixedChargePage; 48 | -------------------------------------------------------------------------------- /pages/subscription.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import Nav from '../components/Nav'; 4 | import Subscription from '../components/Subscription'; 5 | 6 | const SubscriptionPage = () => { 7 | return ( 8 |
9 | 10 | Stripe React Examples: Subscription 11 | 12 | 13 |
44 | ); 45 | }; 46 | 47 | export default SubscriptionPage; 48 | -------------------------------------------------------------------------------- /components/Nav.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import { useRouter } from 'next/router'; 4 | import Logo from './Logo'; 5 | import GitHub from './GitHub'; 6 | 7 | const NavLink = ({ href, children }) => { 8 | const router = useRouter(); 9 | const isActive = router.asPath === href; 10 | const className = isActive 11 | ? 'mx-px px-4 py-3 border-b-2 border-indigo-600 hover:border-indigo-500 text-gray-300 hover:text-gray-200 whitespace-no-wrap' 12 | : 'mx-px px-4 py-3 border-b-2 border-transparent text-gray-600 hover:text-gray-500 whitespace-no-wrap'; 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | const Nav = () => { 22 | return ( 23 |
24 |
25 | 26 | 27 | 28 |
29 |
30 |

Stripe React Examples

31 |
32 | Fixed Charge 33 | Subscription 34 |
35 |
36 |
37 | 42 | 43 | 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default Nav; 50 | -------------------------------------------------------------------------------- /components/FixedCharge.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; 3 | import { useStaticKit } from '@statickit/react'; 4 | import { createCustomer, createCharge } from '@statickit/functions'; 5 | 6 | type FormState = 'idle' | 'submitting' | 'succeeded'; 7 | 8 | const FixedCharge = () => { 9 | const client = useStaticKit(); 10 | const stripe = useStripe(); 11 | const elements = useElements(); 12 | 13 | const [email, setEmail] = useState(''); 14 | const [stripeError, setStripeError] = useState(null); 15 | const [formState, setFormState] = useState('idle'); 16 | 17 | const handleSubmit = async (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | setFormState('submitting'); 20 | 21 | // Tokenize the credit card 22 | const { error, token } = await stripe.createToken( 23 | elements.getElement(CardElement) 24 | ); 25 | 26 | setStripeError(error); 27 | 28 | // Bail if tokenization failed 29 | if (!token) { 30 | setFormState('idle'); 31 | return; 32 | } 33 | 34 | // Create the customer in Stripe and attach the tokenized credit card 35 | const customerResult = await createCustomer(client, { 36 | email, 37 | source: token.id 38 | }); 39 | 40 | // Bail if customer creation failed 41 | if (customerResult.status !== 'ok') { 42 | setFormState('idle'); 43 | return; 44 | } 45 | 46 | // Charge the customer 47 | const { status } = await createCharge(client, { 48 | amount: 2500, 49 | customerToken: customerResult.customerToken 50 | }); 51 | 52 | setFormState(status === 'ok' ? 'succeeded' : 'idle'); 53 | }; 54 | 55 | if (formState === 'succeeded') { 56 | return ( 57 |

58 | Charge succeeded! 👍 59 |

60 | ); 61 | } 62 | 63 | const isSubmitting = formState === 'submitting'; 64 | const buttonText = isSubmitting ? 'Submitting...' : 'Pay $25'; 65 | 66 | return ( 67 |
68 |
69 | 82 |
83 |
84 | setStripeError(e.error)} 86 | options={{ 87 | style: { 88 | base: { 89 | fontSize: '16px', 90 | fontSmoothing: 'antialiased', 91 | color: '#fff', 92 | '::placeholder': { 93 | color: '#718096' 94 | } 95 | } 96 | } 97 | }} 98 | /> 99 | 100 | {stripeError && ( 101 |
102 | {stripeError.message} 103 |
104 | )} 105 |
106 | 107 | 110 |
111 | ); 112 | }; 113 | 114 | export default FixedCharge; 115 | -------------------------------------------------------------------------------- /components/Subscription.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; 3 | import { useStaticKit } from '@statickit/react'; 4 | import { createCustomer, createSubscription } from '@statickit/functions'; 5 | 6 | type FormState = 'idle' | 'submitting' | 'succeeded'; 7 | 8 | const Subscription = () => { 9 | const client = useStaticKit(); 10 | const stripe = useStripe(); 11 | const elements = useElements(); 12 | 13 | const [email, setEmail] = useState(''); 14 | const [plan, setPlan] = useState('plan_00001'); 15 | const [stripeError, setStripeError] = useState(null); 16 | const [formState, setFormState] = useState('idle'); 17 | 18 | const handleSubmit = async (e: React.FormEvent) => { 19 | e.preventDefault(); 20 | setFormState('submitting'); 21 | 22 | // Tokenize the credit card 23 | const { error, token } = await stripe.createToken( 24 | elements.getElement(CardElement) 25 | ); 26 | 27 | setStripeError(error); 28 | 29 | // Bail if tokenization failed 30 | if (!token) { 31 | setFormState('idle'); 32 | return; 33 | } 34 | 35 | // Create the customer in Stripe and attach the tokenized credit card 36 | const customerResult = await createCustomer(client, { 37 | email, 38 | source: token.id 39 | }); 40 | 41 | // Bail if customer creation failed 42 | if (customerResult.status !== 'ok') { 43 | setFormState('idle'); 44 | return; 45 | } 46 | 47 | // Start the Stripe subscription 48 | const { status } = await createSubscription(client, { 49 | customerToken: customerResult.customerToken, 50 | items: [{ plan }] 51 | }); 52 | 53 | setFormState(status === 'ok' ? 'succeeded' : 'idle'); 54 | }; 55 | 56 | if (formState === 'succeeded') { 57 | return ( 58 |

59 | Subscription started! 👍 60 |

61 | ); 62 | } 63 | 64 | const isSubmitting = formState === 'submitting'; 65 | const buttonText = isSubmitting ? 'Submitting...' : 'Subscribe'; 66 | 67 | return ( 68 |
69 |
70 | 83 |
84 | 85 |
86 | 111 |
112 | 113 |
114 | setStripeError(e.error)} 116 | options={{ 117 | style: { 118 | base: { 119 | fontSize: '16px', 120 | fontSmoothing: 'antialiased', 121 | color: '#fff', 122 | '::placeholder': { 123 | color: '#718096' 124 | } 125 | } 126 | } 127 | }} 128 | /> 129 | 130 | {stripeError && ( 131 |
132 | {stripeError.message} 133 |
134 | )} 135 |
136 | 137 | 140 |
141 | ); 142 | }; 143 | 144 | export default Subscription; 145 | --------------------------------------------------------------------------------