initial commit

This commit is contained in:
equippedcoding-master
2025-09-17 09:37:06 -05:00
parent 86108ca47e
commit e2c98790b2
55389 changed files with 6206730 additions and 0 deletions
@@ -0,0 +1,110 @@
# Subscriptions using React
This sample shows how to build a custom subscriptions form to take a payment
using the [Subscriptions
API](https://stripe.com/docs/billing/subscriptions/fixed-price), [Stripe
Elements](https://stripe.com/billing/elements) and
[React](https://reactjs.org/).
## Features
This sample consists of a `client` in React and a `server` piece available in 7
common languages.
The client is implemented using `create-react-app` to provide the boilerplate
for React. Stripe Elements is integrated using
[`react-stripe-js`](https://github.com/stripe/react-stripe-js), which is the
official React library provided by Stripe.
## How to run locally
To run this sample locally you need to start both a local dev server for the `front-end` and another server for the `back-end`.
You will need a Stripe account with its own set of [API keys](https://stripe.com/docs/development#api-keys).
Follow the steps below to run locally.
**1. Clone and configure the sample**
The Stripe CLI is the fastest way to clone and configure a sample to run locally.
**Using the Stripe CLI**
If you haven't already installed the CLI, follow the [installation steps](https://github.com/stripe/stripe-cli#installation) in the project README. The CLI is useful for cloning samples and locally testing webhooks and Stripe integrations.
In your terminal shell, run the Stripe CLI command to clone the sample:
```
stripe samples create subscription-use-cases
```
The CLI will walk you through picking your integration type, server and client languages, and configuring your .env config file with your Stripe API keys.
**Installing and cloning manually**
If you do not want to use the Stripe CLI, you can manually clone and configure the sample yourself:
```
git clone https://github.com/stripe-samples/subscription-use-cases
```
Copy the .env.example file into a file named .env in the folder of the server you want to use. For example:
```
cp .env.example server/node/.env
```
You will need a Stripe account in order to run the demo. Once you set up your account, go to the Stripe [developer dashboard](https://stripe.com/docs/development/quickstart#api-keys) to find your API keys.
```
STRIPE_PUBLISHABLE_KEY=<replace-with-your-publishable-key>
STRIPE_SECRET_KEY=<replace-with-your-secret-key>
```
**Run react frontend client**
Copy the `.env.example` file into a file named `.env` in the folder of the server you want to use. For example:
```
cp .env.example client/react/.env
```
### Running the API server
1. Go to `/server`
1. Pick the language you are most comfortable in and follow the instructions in the directory on how to run.
### Running the React client
1. Go to `/client`
1. Run `npm install`
1. Run `npm start` and your default browser should now open with the front-end being served from `http://localhost:3000/`.
### Using the sample app
When running both servers, you are now ready to use the app running in [http://localhost:3000](http://localhost:3000).
1. Enter your email address
1. Select your price
1. Enter your card details
1. 🎉
## FAQ
Q: Why did you pick these frameworks?
A: We chose the most minimal framework to convey the key Stripe calls and concepts you need to understand. These demos are meant as an educational tool that helps you roadmap how to integrate Stripe within your own system independent of the framework.
## Get support
If you found a bug or want to suggest a new [feature/use case/sample], please [file an issue](../../../../../issues).
If you have questions, comments, or need help with code, we're here to help:
- on [Discord](https://stripe.com/go/developer-chat)
- on Twitter at [@StripeDev](https://twitter.com/StripeDev)
- on Stack Overflow at the [stripe-payments](https://stackoverflow.com/tags/stripe-payments/info) tag
- by [email](mailto:support+github@stripe.com)
## Author(s)
[@ctrudeau-stripe](https://twitter.com/trudeaucj)
@@ -0,0 +1,42 @@
{
"name": "fixed-price-subscription",
"version": "0.2.0",
"private": true,
"dependencies": {
"@stripe/react-stripe-js": "^1.1.2",
"@stripe/stripe-js": "^1.6.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"autoprefixer": "^9.8.0",
"postcss-cli": "^7.1.1"
},
"proxy": "http://localhost:4242"
}
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Stripe Sample - Subscription</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<main id="root"></main>
</body>
</html>
@@ -0,0 +1,67 @@
import React, { useState, useEffect } from 'react';
import { Link, withRouter } from 'react-router-dom';
import './App.css';
const AccountSubscription = ({subscription}) => {
return (
<section>
<hr />
<h4>
<a href={`https://dashboard.stripe.com/test/subscriptions/${subscription.id}`}>
{subscription.id}
</a>
</h4>
<p>
Status: {subscription.status}
</p>
<p>
Card last4: {subscription.default_payment_method?.card?.last4}
</p>
<p>
Current period end: {(new Date(subscription.current_period_end * 1000).toString())}
</p>
{/* <Link to={{pathname: '/change-plan', state: {subscription: subscription.id }}}>Change plan</Link><br /> */}
<Link to={{pathname: '/cancel', state: {subscription: subscription.id }}}>Cancel</Link>
</section>
)
}
const Account = () => {
const [subscriptions, setSubscriptions] = useState([]);
useEffect(() => {
const fetchData = async () => {
const {subscriptions} = await fetch('/subscriptions').then(r => r.json());
setSubscriptions(subscriptions.data);
}
fetchData();
}, []);
if (!subscriptions) {
return '';
}
return (
<div>
<h1>Account</h1>
<a href="/prices">Add a subscription</a>
<a href="/">Restart demo</a>
<h2>Subscriptions</h2>
<div id="subscriptions">
{subscriptions.map(s => {
return <AccountSubscription key={s.id} subscription={s} />
})}
</div>
</div>
);
}
export default withRouter(Account);
@@ -0,0 +1,109 @@
@import url('https://fonts.googleapis.com/css?family=Raleway&display=swap');
:root {
--light-grey: #F6F9FC;
--dark-terminal-color: #0A2540;
--accent-color: #635BFF;
--radius: 3px;
}
body {
padding: 20px;
font-family: 'Raleway';
display: flex;
justify-content: center;
font-size: 1.2em;
color: var(--dark-terminal-color);
}
form > * {
margin: 10px 0;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* prices.html */
.price-list {
display: flex;
}
.price-list > div {
flex-grow: 3;
margin: 0 10px;
border: black solid 1px;
padding: 20px;
}
button {
background-color: var(--accent-color);
}
button {
background: var(--accent-color);
border-radius: var(--radius);
color: white;
border: 0;
padding: 12px 16px;
margin-top: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: block;
}
button:hover {
filter: contrast(115%);
}
button:active {
transform: translateY(0px) scale(0.98);
filter: brightness(0.9);
}
button:disabled {
opacity: 0.5;
cursor: none;
}
input, select {
display: block;
font-size: 1.1em;
width: 100%;
}
label {
display: block;
}
a {
color: var(--accent-color);
font-weight: 900;
}
small {
font-size: .6em;
}
fieldset, input, select {
border: 1px solid #efefef;
}
#payment-form {
border: #F6F9FC solid 1px;
border-radius: var(--radius);
padding: 20px;
margin: 20px 0;
box-shadow: 0 30px 50px -20px rgb(50 50 93 / 25%), 0 30px 60px -30px rgb(0 0 0 / 30%);
}
#messages {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New';
background-color: #0A253C;
color: #00D924;
padding: 20px;
margin: 20px 0;
border-radius: var(--radius);
font-size:0.7em;
}
@@ -0,0 +1,33 @@
import React from 'react';
import './App.css';
import { BrowserRouter as Switch, Route } from 'react-router-dom';
import Account from './Account';
import Cancel from './Cancel';
import Prices from './Prices';
import Register from './Register';
import Subscribe from './Subscribe';
function App(props) {
return (
<Switch>
<Route exact path="/">
<Register />
</Route>
<Route path="/prices">
<Prices />
</Route>
<Route path="/subscribe">
<Subscribe />
</Route>
<Route path="/account">
<Account />
</Route>
<Route path="/cancel">
<Cancel />
</Route>
</Switch>
);
}
export default App;
@@ -0,0 +1,38 @@
import React, { useState } from 'react';
import { withRouter } from 'react-router-dom';
import './App.css';
import { Redirect } from 'react-router-dom';
const Cancel = ({location}) => {
const [cancelled, setCancelled] = useState(false);
const handleClick = async (e) => {
e.preventDefault();
await fetch('/cancel-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
subscriptionId: location.state.subscription
}),
})
setCancelled(true);
};
if(cancelled) {
return <Redirect to={`/account`} />
}
return (
<div>
<h1>Cancel</h1>
<button onClick={handleClick}>Cancel</button>
</div>
)
}
export default withRouter(Cancel);
@@ -0,0 +1,63 @@
import React, { useState, useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
const Prices = () => {
const [prices, setPrices] = useState([]);
const [subscriptionData, setSubscriptionData] = useState(null);
useEffect(() => {
const fetchPrices = async () => {
const {prices} = await fetch('/config').then(r => r.json());
setPrices(prices);
};
fetchPrices();
}, [])
const createSubscription = async (priceId) => {
const {subscriptionId, clientSecret} = await fetch('/create-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
priceId
}),
}).then(r => r.json());
setSubscriptionData({ subscriptionId, clientSecret });
}
if(subscriptionData) {
return <Redirect to={{
pathname: '/subscribe',
state: subscriptionData
}} />
}
return (
<div>
<h1>Select a plan</h1>
<div className="price-list">
{prices.map((price) => {
return (
<div key={price.id}>
<h3>{price.product.name}</h3>
<p>
${price.unit_amount / 100} / month
</p>
<button onClick={() => createSubscription(price.id)}>
Select
</button>
</div>
)
})}
</div>
</div>
);
}
export default withRouter(Prices);
@@ -0,0 +1,57 @@
import React, { useState } from 'react';
import './App.css';
import { Redirect } from 'react-router-dom';
const Register = (props) => {
const [email, setEmail] = useState('jenny.rosen@example.com');
const [customer, setCustomer] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
const {customer} = await fetch('/create-customer', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
}),
}).then(r => r.json());
setCustomer(customer);
};
if(customer) {
return <Redirect to={{pathname: '/prices'}} />
}
return (
<main>
<h1>Sample Photo Service</h1>
<img src="https://picsum.photos/280/320?random=4" alt="picsum generated" width="140" height="160" />
<p>
Unlimited photo hosting, and more. Cancel anytime.
</p>
<form onSubmit={handleSubmit}>
<label>
Email
<input
type="text"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required />
</label>
<button type="submit">
Register
</button>
</form>
</main>
);
}
export default Register;
@@ -0,0 +1,106 @@
import React, { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { withRouter } from 'react-router-dom';
import {
CardElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
import { Redirect } from 'react-router-dom';
const Subscribe = ({location}) => {
// Get the lookup key for the price from the previous page redirect.
const [clientSecret] = useState(location.state.clientSecret);
const [subscriptionId] = useState(location.state.subscriptionId);
const [name, setName] = useState('Jenny Rosen');
const [messages, _setMessages] = useState('');
const [paymentIntent, setPaymentIntent] = useState();
// helper for displaying status messages.
const setMessage = (message) => {
_setMessages(`${messages}\n\n${message}`);
}
// Initialize an instance of stripe.
const stripe = useStripe();
const elements = useElements();
if (!stripe || !elements) {
// Stripe.js has not loaded yet. Make sure to disable
// form submission until Stripe.js has loaded.
return '';
}
// When the subscribe-form is submitted we do a few things:
//
// 1. Tokenize the payment method
// 2. Create the subscription
// 3. Handle any next actions like 3D Secure that are required for SCA.
const handleSubmit = async (e) => {
e.preventDefault();
// 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 card Element to tokenize payment details
let { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: name,
}
}
});
if(error) {
// show error and collect new card details.
setMessage(error.message);
return;
}
setPaymentIntent(paymentIntent);
}
if(paymentIntent && paymentIntent.status === 'succeeded') {
return <Redirect to={{pathname: '/account'}} />
}
return (
<>
<h1>Subscribe</h1>
<p>
Try the successful test card: <span>4242424242424242</span>.
</p>
<p>
Try the test card that requires SCA: <span>4000002500003155</span>.
</p>
<p>
Use any <i>future</i> expiry date, CVC,5 digit postal code
</p>
<hr />
<form onSubmit={handleSubmit}>
<label>
Full name
<input type="text" id="name" value={name} onChange={(e) => setName(e.target.value)} />
</label>
<CardElement />
<button>
Subscribe
</button>
<div>{messages}</div>
</form>
</>
)
}
export default withRouter(Subscribe);
@@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {Elements} from '@stripe/react-stripe-js';
import {loadStripe} from '@stripe/stripe-js';
fetch('/config')
.then((response) => response.json())
.then((data) => {
const stripePromise = loadStripe(data.publishableKey);
ReactDOM.render(
<React.StrictMode>
<Elements stripe={stripePromise}>
<App />
</Elements>
</React.StrictMode>,
document.getElementById('root')
);
})
.catch((error) => {
console.error('Error:', error);
});
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>Account</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="css/base.css" rel="stylesheet" />
<script src="https://js.stripe.com/v3/"></script>
<script src="account.js" defer></script>
</head>
<body>
<main>
<h1>Account</h1>
<a href="prices.html">Add a subscription</a>
<a href="register.html">Restart demo</a>
<h2>Subscriptions</h2>
<div id="subscriptions">
<!-- see account.js to see how this div is populated -->
</div>
</main>
</body>
</html>
@@ -0,0 +1,36 @@
document.addEventListener('DOMContentLoaded', async () => {
// Fetch the list of subscriptions for this customer.
const {subscriptions} = await fetch('/subscriptions').then((r) => r.json());
// Construct and display each subscription, its status, last4 of the card
// used, and the current period end.
const subscriptionsDiv = document.querySelector('#subscriptions');
subscriptionsDiv.innerHTML = subscriptions.data.map((subscription) => {
let last4 = subscription.default_payment_method?.card?.last4 || '';
return `
<hr>
<h4>
<a href="https://dashboard.stripe.com/test/subscriptions/${subscription.id}">
${subscription.id}
</a>
</h4>
<p>
Status: ${subscription.status}
</p>
<p>
Card last4: ${last4}
</p>
<small>If the last4 is blank, ensure webhooks are being handled. The default payment method is set in the webhook handler.</small>
<p>
Current period end: ${new Date(subscription.current_period_end * 1000)}
</p>
<!--<a href="change-payment-method.html?subscription=${subscription.id}"> Update payment method </a><br />
<a href="change-plan.html?subscription=${subscription.id}"> Change plan </a><br /> -->
<a href="cancel.html?subscription=${subscription.id}"> Cancel </a><br />
`;
}).join('<br />');
});
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Cancel</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="css/base.css" rel="stylesheet" />
<script src="https://js.stripe.com/v3/"></script>
<script src="cancel.js" defer></script>
</head>
<body>
<main>
<h1>Cancel</h1>
<button id="cancel-btn">Cancel</button>
<div id="messages"></div>
</main>
</body>
</html>
@@ -0,0 +1,41 @@
document.addEventListener('DOMContentLoaded', async () => {
// Fetch the ID of the subscription from the query string
// params.
const params = new URLSearchParams(window.location.search);
const subscriptionId = params.get('subscription');
// When the cancel button is clicked, send an AJAX request
// to our server to cancel the subscription.
const cancelBtn = document.querySelector('#cancel-btn');
cancelBtn.addEventListener('click', async (e) => {
e.preventDefault();
setMessage("Cancelling subscription...");
const {subscription} = await fetch('/cancel-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
subscriptionId
}),
})
.then((response) => response.json())
// Display the status of the subscription after attempting to
// cancel.
setMessage(`Subscription status: ${subscription.status}`);
setMessage(`Redirecting back to account in 7s.`);
// Redirect to the account page.
setTimeout(() => {
window.location.href = "account.html";
}, 7 * 1000);
});
const setMessage = (message) => {
const messagesDiv = document.querySelector('#messages');
messagesDiv.innerHTML += "<br>" + message;
}
});
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<title>Change plan</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="css/base.css" rel="stylesheet" />
<script src="https://js.stripe.com/v3/"></script>
<script src="account.js" defer></script>
</head>
<body>
<main>
<h1>Change plan</h1>
Current, new
<div class="price-list">
<div>
<h3>Basic</h3>
<p>
$5.00 / month
</p>
<a href="subscribe.html?price=basic">
Select
</a>
</div>
<div>
<h3>Premium</h3>
<p>
$15.00 / month
</p>
<a href="subscribe.html?price=premium">
Select
</a>
</div>
</div>
</main>
</body>
</html>
@@ -0,0 +1,69 @@
@import url('https://fonts.googleapis.com/css?family=Raleway&display=swap');
:root {
--gray-offset: rgba(0, 0, 0, 0.03);
--gray-border: rgba(0, 0, 0, 0.15);
--gray-light: rgba(0, 0, 0, 0.4);
--gray-mid: rgba(0, 0, 0, 0.7);
--gray-dark: rgba(0, 0, 0, 0.9);
--body-color: var(--gray-mid);
--headline-color: var(--gray-dark);
--accent-color: #ed5f74;
--radius: 6px;
}
body {
padding: 20px;
font-family: 'Raleway';
display: flex;
justify-content: center;
font-size: 1.2em;
}
form > * {
margin: 10px 0;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
button {
background-color: #ed5f74;
}
button {
background: var(--accent-color);
border-radius: var(--radius);
color: white;
border: 0;
padding: 12px 16px;
margin-top: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: block;
}
button:hover {
filter: contrast(115%);
}
button:active {
transform: translateY(0px) scale(0.98);
filter: brightness(0.9);
}
button:disabled {
opacity: 0.5;
cursor: none;
}
/* prices.html */
.price-list {
display: flex;
}
.price-list > div {
flex-grow: 3;
margin: 0 10px;
border: black solid 1px;
padding: 20px;
}
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Start a Subscription</title>
<link href="css/base.css" rel="stylesheet" />
</head>
<body>
<main>
<p>Welcome to the Stripe sample for starting a new fixed price subscription.</p>
<p><a href="register.html">Start Demo</a></p>
</main>
</body>
</html>
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Subscription prices</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="css/base.css" rel="stylesheet" />
<script src="./prices.js" defer></script>
</head>
<body>
<main>
<h1>Select a plan</h1>
<div id="price-list" class="price-list">
loading...
</div>
</main>
</body>
</html>
@@ -0,0 +1,55 @@
// Fetch price data.
const pricesDiv = document.querySelector('#price-list');
fetch('/config')
.then((response) => response.json())
.then((data) => {
pricesDiv.innerHTML = '';
if(!data.prices) {
pricesDiv.innerHTML = `
<h3>No prices found</h3>
<p>This sample requires two prices, one with the lookup_key sample_basic and another with the lookup_key sample_premium</p>
<p>You can create these through the API or with the Stripe CLI using the provided seed.json fixture file with: <code>stripe fixtures seed.json</code>
`
}
data.prices.forEach((price) => {
pricesDiv.innerHTML += `
<div>
<span>
${price.unit_amount / 100} /
${price.currency} /
${price.recurring.interval}
</span>
<button onclick="createSubscription('${price.id}')">Select</button>
</div>
`;
});
})
.catch((error) => {
console.error('Error:', error);
});
const createSubscription = (priceId) => {
return fetch('/create-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
priceId: priceId,
}),
})
.then((response) => response.json())
.then((data) => {
window.sessionStorage.setItem('subscriptionId', data.subscriptionId);
window.sessionStorage.setItem('clientSecret', data.clientSecret);
window.location.href = '/subscribe.html';
})
.catch((error) => {
console.error('Error:', error);
});
}
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<title>Register</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="css/base.css" rel="stylesheet" />
<script src="https://js.stripe.com/v3/"></script>
<script src="/register.js" defer></script>
</head>
<body>
<main>
<h1>Sample Photo Service</h1>
<img src="https://picsum.photos/280/320?random=1" width="140" height="160" alt="stock image" />
<p>
Unlimited photo hosting, and more. Cancel anytime.
</p>
<form id="signup-form">
<label>
Email
<input id="email" type="text" placeholder="Email address" value="test@example.com" required />
</label>
<button type="submit">
Register
</button>
</form>
</main>
</body>
</html>
@@ -0,0 +1,31 @@
document.addEventListener('DOMContentLoaded', async () => {
const signupForm = document.querySelector('#signup-form');
if (signupForm) {
signupForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Grab reference to the emailInput. The email address
// entered will be passed to the server and used to create
// a customer. Email addresses do NOT uniquely identify
// customers in Stripe.
const emailInput = document.querySelector('#email');
// Create a customer. This will also set a cookie on the server
// to simulate having a logged in user.
const {customer} = await fetch('/create-customer', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: emailInput.value,
}),
}).then(r => r.json());
// Redirect to the pricing page.
window.location.href = '/prices.html';
});
} else {
alert("No sign up form with ID `signup-form` found on the page.");
}
});
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<title>Subscribe</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="css/base.css" rel="stylesheet" />
<script src="https://js.stripe.com/v3/"></script>
<script src="subscribe.js" defer></script>
</head>
<body>
<main>
<h1>Subscribe</h1>
<p>
Try the successful test card: <span>4242424242424242</span>.
</p>
<p>
Try the test card that requires SCA: <span>4000002500003155</span>.
</p>
<p>
Use any <i>future</i> expiry date, CVC, and 5 digit postal code.
</p>
<hr />
<form id="subscribe-form">
<label>
Full name
<input type="text" id="name" value="Jenny Rosen" />
</label>
<div id="card-element">
<!-- the card element will be mounted here -->
</div>
<button type="submit">
Subscribe
</button>
<div id="messages"></div>
</form>
</main>
</body>
</html>
@@ -0,0 +1,60 @@
// helper method for displaying a status message.
const setMessage = (message) => {
const messageDiv = document.querySelector('#messages');
messageDiv.innerHTML += "<br>" + message;
}
// Fetch public key and initialize Stripe.
let stripe, cardElement;
fetch('/config')
.then((resp) => resp.json())
.then((resp) => {
stripe = Stripe(resp.publishableKey);
const elements = stripe.elements();
cardElement = elements.create('card');
cardElement.mount('#card-element');
});
// Extract the client secret query string argument. This is
// required to confirm the payment intent from the front-end.
const subscriptionId = window.sessionStorage.getItem('subscriptionId');
const clientSecret = window.sessionStorage.getItem('clientSecret');
// This sample only supports a Subscription with payment
// upfront. If you offer a trial on your subscription, then
// instead of confirming the subscription's latest_invoice's
// payment_intent. You'll use stripe.confirmCardSetup to confirm
// the subscription's pending_setup_intent.
// See https://stripe.com/docs/billing/subscriptions/trials
// Payment info collection and confirmation
// When the submit button is pressed, attempt to confirm the payment intent
// with the information input into the card element form.
// - handle payment errors by displaying an alert. The customer can update
// the payment information and try again
// - Stripe Elements automatically handles next actions like 3DSecure that are required for SCA
// - Complete the subscription flow when the payment succeeds
const form = document.querySelector('#subscribe-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const nameInput = document.getElementById('name');
// Create payment method and confirm payment intent.
stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: nameInput.value,
},
}
}).then((result) => {
if(result.error) {
setMessage(`Payment failed: ${result.error.message}`);
} else {
// Redirect the customer to their account page
setMessage('Success! Redirecting to your account.');
window.location.href = '/account.html';
}
});
});