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

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
});

View File

@@ -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