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,3 @@
STRIPE_SECRET_KEY=sk_test_51OIu21J7BKlr2pgsABfPagNLQmRYdn8wSDNOj9ph6q0T6ThchqyfRC585qp5iG1bHM3MrxnTHOUGQRGhZ4K6h5Sg00DmDdAv7Q
STRIPE_PUBLISHABLE_KEY=pk_test_51OIu21J7BKlr2pgsla3bQwYW92sQZ1WQDCW4wZ4YFm8WGSbPjPeciQjCRtCH40fLmkwLFhK100pOcjHfLDKUmUrg00D4CK8OKH
STATIC_DIR=./client/vanillajs

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,88 @@
import {decode,encode} from 'html-entities';
function executeMysqlConnection(query){
let con = createMySQLConnection();
con.connect(function(err) {
if (err) throw err;
con.query(`SELECT * FROM subdomain_properties WHERE SubDomain = '${subdomain}'`, function (err, results) {
if (err) throw err;
// con.end();
if(results.length > 0){
}else{
}
});
});
}
function update(connection){
}
/**
*
* Update json_data
*
* @param {object} json_data
* @param {string} db_table
* @param {object} connection
* @param {function} cb
*/
export function updateJsonData(json_data,db_table,connection,cb){
let data = encode(JSON.stringify(json_data));
const sql = `UPDATE ${db_table} SET jsontext = "${data}" WHERE mysql_id = "${json_data.json.mysql_id}"`;
connection.connect(function(err) {
if (err) throw err;
con.query(sql, function (err, results) {
if (err) throw err;
con.end();
if(cb!=undefined) cb();
});
});
}
export function updateMySQLMemberUsersJSONData(connection,email,json_data){
// const sql = 'UPDATE `users` SET `age` = 20 WHERE `name` = "Josh" LIMIT 1';
const sql = `UPDATE member_users SET json_data = "${json_data}" WHERE email = "${email}"`;
connection.connect(function(err) {
if (err) throw err;
con.query(sql, function (err, results) {
if (err) throw err;
con.end();
});
});
}
export async function getMemberUser(connection,email,callback){
connection.connect(function(err) {
if (err) throw err;
connection.query(`SELECT * FROM member_users WHERE email = '${email}'`, function (err, results) {
if (err) throw err;
con.end();
if(results.length > 0){
let membersuser = {
username: results[i].username,
json_data: results[i].json_data
};
callback(results[i]);
}else{
callback(false);
}
});
});
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
<h3>Prices</h3>

View File

@@ -0,0 +1,30 @@
<!--
<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> -->
<div class="row d-flex justify-content-center" style="background-color: #fff;">
<div class="col-6">
<div class="payments_container" style="min-height: 500px;">
<div class="mb-3" id="address-element"></div>
<div class="mb-3" id="payment-element"></div>
</div>
<button id="start_membership_id" class="btn btn-ouline-success">Start Membership</button>
</div>
</div>

View File

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

View File

@@ -0,0 +1,2 @@

View File

@@ -0,0 +1,195 @@
{
"name": "stripe",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "stripe",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"stripe": "^14.12.0"
},
"devDependencies": {}
},
"node_modules/@types/node": {
"version": "20.11.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.2.tgz",
"integrity": "sha512-cZShBaVa+UO1LjWWBPmWRR4+/eY/JR/UIEcDlVsw3okjWEu+rB7/mH6X3B/L+qJVHDLjk9QW/y2upp9wp1yDXA==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dependencies": {
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dependencies": {
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/qs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/set-function-length": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
"integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
"dependencies": {
"define-data-property": "^1.1.1",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.2",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/stripe": {
"version": "14.12.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-14.12.0.tgz",
"integrity": "sha512-3lze4QdO8fM6nh1vaRsnFpaoA0WV7DerYtjEprOwcwyfdF5LdesfQNZiT22LO1dFiYobBifDzEn8V51MXHPrPQ==",
"dependencies": {
"@types/node": ">=8.1.0",
"qs": "^6.11.0"
},
"engines": {
"node": ">=12.*"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
}
}
}

View File

@@ -0,0 +1,23 @@
{
"name": "stripe",
"version": "1.0.0",
"main": "server.js",
"devDependencies": {},
"author": "",
"license": "ISC",
"type": "module",
"description": "",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"cookie-parser": "^1.4.6",
"stripe": "^14.12.0",
"dotenv": "^16.0.0",
"ejs": "^3.1.6",
"express": "^4.17.3",
"html-entities": "^2.4.0",
"mysql2": "^3.6.0",
"node-fetch": "^3.2.1"
}
}

View File

@@ -0,0 +1,57 @@
{
"_meta": {
"template_version": 0
},
"fixtures": [
{
"name": "basic_product",
"path": "/v1/products",
"method": "post",
"params": {
"name": "Basic"
}
},
{
"name": "basic_price",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${basic_product:id}",
"lookup_key": "sample_basic",
"currency": "usd",
"unit_amount": 500,
"recurring": {
"interval": "month"
},
"metadata": {
"sample": "fixed-price"
}
}
},
{
"name": "premium_product",
"path": "/v1/products",
"method": "post",
"params": {
"name": "Premium"
}
},
{
"name": "premium_price",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${premium_product:id}",
"lookup_key": "sample_premium",
"currency": "usd",
"unit_amount": 1400,
"recurring": {
"interval": "month"
},
"metadata": {
"sample": "fixed-price"
}
}
}
]
}

View File

@@ -0,0 +1,616 @@
import { createRequire } from 'module';
import path from 'path';
import { fileURLToPath } from 'url';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const fs = require('fs');
import {decode,encode} from 'html-entities';
//import "dotenv/config";
import express from "express";
// import http from "http";
import https from "https";
// import * as paypal from "./paypal-api.js";
var mysql = require('mysql2');
import * as helperjs from "./helper.js";
// const express = require('express');
// const app = express();
const { resolve } = require('path');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
// Replace if using a different env file or config
// require('dotenv').config({ path: './.env' });
// const stripe = require('stripe')('sk_test_...');
const subdomain = process.argv[2];
const mypath = path.join(__dirname, '../../system/db/.env');
require('dotenv').config({ path: mypath })
console.log(process.env.MY_DATABASE);
console.log(mypath);
console.log(subdomain);
/**
* Create a mysql connect to the database with creditals.
* Make sure to call connection.end() after calling connection.connect()
* @returns mysql_connection
*/
function createMySQLConnection(){
return mysql.createConnection({
host: process.env.MY_HOSTNAME,
user: process.env.MY_USER,
password: process.env.MY_PASSWORD,
database: process.env.MY_DATABASE,
insecureAuth : true
});
}
let con = createMySQLConnection();
con.connect(function(err) {
if (err) throw err;
con.query(`SELECT * FROM subdomain_properties WHERE SubDomain = '${subdomain}'`, function (err, results) {
if (err) throw err;
// con.end();
if(results.length > 0){
let managed_domain = {};
for(let i=0; i < results.length; i++){
//console.log(results[i].SubDomain + " : " + results[i].PropertyName + " : " + results[i].PropertyValue);
managed_domain[results[i].PropertyName] = results[i].PropertyValue;
}
con.query(`SELECT * FROM configurations WHERE category="processors"`, function (err1, results1) {
if (err1) throw err1;
con.end();
if(results1.length > 0){
let processors = [];
for(let n=0; n < results1.length; n++){
let processor = results1[n];
processor.json = JSON.parse(decode(processor.json));
if(processor.json.processor=="stripe"){
processors.push(processor);
}
}
run(managed_domain, processors);
}else{
console.log(`No active server for subdomain ${subdomain}`);
}
});
}else{
console.log(`Error starting server! No port number found for subdomain ${subdomain}`);
}
});
});
async function func__webhook_create(stripe,req,res,data){
const webhookEndpoint = await stripe.webhookEndpoints.create({
enabled_events: data.clientdata.enabled_events, //['charge.succeeded', 'charge.failed'],
url: 'https://stripewebhooks.appfactory.studio/webhook_action'// 'https://example.com/my/webhook/endpoint',
});
}
async function func__webhook_update(stripe,req,res,data){
const webhookEndpoint = await stripe.webhookEndpoints.update(
'we_1Mr5jULkdIwHu7ix1ibLTM0x',
{
url: 'https://example.com/new_endpoint',
}
);
}
async function func__webhook_stuff(){
const deleted = await stripe.webhookEndpoints.del('we_1Mr5jULkdIwHu7ix1ibLTM0x');
}
async function func_server_products_list(stripe,req,res,mydata){
try{
const products = await stripe.products.list({});
res.jsonp({products});
}catch(err){
res.jsonp({
error: { message: error.message }
});
}
}
async function func_server_create_subscription_plan(stripe,req,res,data){
const interval = data.clientdata.interval;
const unit_amount = data.clientdata.unit_amount;
const product_name = data.clientdata.product_name;
const lookup_key = data.clientdata.lookup_key;
const processor = data.processor;
try {
const price = await stripe.prices.create({
currency: 'usd',
unit_amount: unit_amount,
lookup_key: lookup_key,
recurring: {
interval: interval,
},
product_data: {
name: product_name,
}
});
if(processor.json.subscriptions==undefined){
processor.json.subscriptions = [];
}
processor.json.subscriptions.push(price);
helperjs.updateJsonData(processor,"plugin_processors",createMySQLConnection());
res.jsonp({
price: price,
processor: processor
});
} catch (error){
res.jsonp({
error: { message: error.message }
});
}
}
/**
* This creates the subscription for the customer.
*
*
* @param {*} stripe
* @param {*} req
* @param {*} res
* @param {*} data
* @returns
*/
async function func_client_create_subscription_for_customer(stripe,req,res,data){
const email = data.clientdata.email;
const customerId = data.clientdata.customer_id;
const priceId = data.clientdata.price_id;
try {
const prices = await stripe.prices.list({
lookup_keys: ['sample_basic', 'sample_premium'],
expand: ['data.product']
});
const customer = await stripe.customer.create({
email: data.clientdata.email,
});
// helperjs.getMemberUser(createMySQLConnection(), email, function(user){
// helperjs.updateMySQLMemberUsersJSONData(createMySQLConnection(),email,json_data);
// });
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{
price: priceId,
}],
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method: 'on_subscription'
},
expand: ['latest_invoice.payment_intent'],
});
// /home/appfactorystudio/websites/www15/portal/admin/services/stripe/
res.jsonp({
response: "true",
price: priceId,
customerId: customerId,
subscription: subscription,
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client_secret
});
} catch (error) {
res.jsonp({
error: { message: error.message }
});
}
}
async function func__subscription_product_create(stripe,req,res,data){
try{
const product = await stripe.products.create({
name: data.clientdata.product_name,
});
res.jsonp({products});
}catch(err){
res.jsonp({
error: { message: error.message }
});
}
res.jsonp({
response: product,
});
}
async function func__subscription_screate_schedule(stripe,req,res,data){
const subscriptionSchedule = await stripe.subscriptionSchedules.create
({
customer: 'cus_GBHHxuvBvO26Ea',
start_date: 'now',
end_behavior: 'release',
phases: [
{
items: [
{
price: 'price_1GqNdGAJVYItwOKqEHb',
quantity: 1,
},
],
iterations: 12,
},
],
});
// const orderID = req.query.orderID;
// const captureData = await paypal.refundPayment(orderID);
res.jsonp(subscriptionSchedule);
// const subscriptionSchedule = await stripe.subscriptionSchedules.create
// ({
// from_subscription: 'sub_GB98WOvaRAWPl6',
// });
}
async function func__subscription_create_product(stripe,req,res,data){
const product = await stripe.products.create({
name: data.clientdata.name,
});
res.jsonp(product);
}
async function func__subscription_create_price(stripe,req,res,data){
const price = await stripe.prices.create(data.clientdata.price_data);
res.jsonp(price);
}
async function func__config(stripe,req,res,data){
const prices = await stripe.prices.list({
lookup_keys: ['sample_basic', 'sample_premium'],
expand: ['data.product']
});
res.jsonp({
prices: prices.data,
});
}
async function func__create_customer(stripe,req,res,data){
// Create a new customer object
const customer = await stripe.customer.create({
email: data.clientdata.email,
});
// Save the customer.id in your database alongside your user.
// We're simulating authentication with a cookie.
// res.cookie('customer', customer.id, { maxAge: 900000, httpOnly: true });
let email = data.clientdata.email;
helperjs.getMemberUser(createMySQLConnection(), email, function(user){
helperjs.updateMySQLMemberUsersJSONData(createMySQLConnection(),email,json_data);
});
res.jsonp({ customer: customer });
}
async function func__create_subscription(stripe,req,res,data){
// Simulate authenticated user. In practice this will be the
// Stripe Customer ID related to the authenticated user.
const customerId = req.cookies['customer'];
// Create the subscription
const priceId = req.body.priceId;
try {
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{
price: priceId,
}],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
});
res.send({
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client_secret,
});
res.jsonp(price);
} catch (error) {
// res.jsonp(price);
return res.status(400).send({ error: { message: error.message } });
}
}
async function func__invoice_preview(stripe,req,res,data){
const customerId = req.cookies['customer'];
const priceId = process.env[req.query.newPriceLookupKey.toUpperCase()];
const subscription = await stripe.subscriptions.retrieve(
req.query.subscriptionId
);
const invoice = await stripe.invoices.retrieveUpcoming({
customer: customerId,
subscription: req.query.subscriptionId,
subscription_items: [ {
id: subscription.items.data[0].id,
price: priceId,
}],
});
res.send({ invoice });
}
async function func__cancel_subscription(stripe,req,res,data){
// Cancel the subscription
try {
const deletedSubscription = await stripe.subscriptions.del(
req.body.subscriptionId
);
res.send({ subscription: deletedSubscription });
res.jsonp(price);
} catch (error) {
// res.jsonp(price);
return res.status(400).send({ error: { message: error.message } });
}
}
async function func__update_subscription(stripe,req,res,data){
try {
const subscription = await stripe.subscriptions.retrieve(
req.body.subscriptionId
);
const updatedSubscription = await stripe.subscriptions.update(
req.body.subscriptionId, {
items: [{
id: subscription.items.data[0].id,
price: process.env[req.body.newPriceLookupKey.toUpperCase()],
}],
}
);
res.send({ subscription: updatedSubscription });
res.jsonp(price);
} catch (error) {
// res.jsonp(price);
return res.status(400).send({ error: { message: error.message } });
}
}
async function func__subscriptions_list(stripe,req,res,data){
// Simulate authenticated user. In practice this will be the
// Stripe Customer ID related to the authenticated user.
const customerId = req.cookies['customer'];
const subscriptions = await stripe.subscriptions.list({
customer: customerId,
status: 'all',
expand: ['data.default_payment_method'],
});
res.json({subscriptions});
}
function run(managed_domain,processors){
// function run(opts,opts1){
let active_processor = null;
for(let n=0; n < processors.length; n++){
if(processors[n].json.active==true){
active_processor = processors[n];
break;
}
}
console.log(active_processor);
console.log("Starting Stripe Server on port: " + active_processor.json.service_port);
let privateKey = fs.readFileSync(managed_domain.CertPrivateKeyLocation, 'utf8');
let certificate = fs.readFileSync(managed_domain.CertPublicKeyLocation, 'utf8');
let credentials = {key: privateKey, cert: certificate};
let app = express();
let clientId = active_processor.json.client_id;
let clientSecret = active_processor.json.client_secret;
let base = "";
let mode = active_processor.json.mode;
// if(mode=="sandbox"){
// base = "https://api-m.sandbox.paypal.com";
// }else{
// base = "https://api-m.paypal.com";
// }
app.get("/"+active_processor.json.service_end_point, async (req, res) => {
const stripe = require('stripe')(clientSecret, {
apiVersion: '2022-08-01',
appInfo: { // For sample support and debugging, not required for production:
name: "stripe-samples/subscription-use-cases/fixed-price",
version: "0.0.1",
url: "https://github.com/stripe-samples/subscription-use-cases/fixed-price"
}
});
// console.log(base);
let mydata = {
clientdata: JSON.parse(req.query.data),
managed_domain: managed_domain,
processor: active_processor
};
try {
if(req.query.type=="check"){
res.jsonp({action:true});
}else if(req.query.type=="admin-create-subscription"){
func_server_create_subscription_plan(stripe,req,res,mydata);
}else if(req.query.type=="admin_list_products"){
func_server_products_list(stripe,req,res,mydata);
}else if(req.query.type=="subscription-create-product"){
func_client_create_subscription_for_customer(stripe,req,res,mydata);
}else if(req.query.type=="subscription-create-product"){
func__subscription_create_product(stripe,req,res,mydata);
}else if(req.query.type=="subscription_create_product"){
func__subscription_product_create(stripe,req,res,mydata);
}else if(req.query.type=="subscription_create_price"){
func__subscription_create_price(stripe,req,res,mydata);
}else if(req.query.type=="subscriptions_list"){
func__subscriptions_list(stripe,req,res,mydata);
}else if(req.query.type=="subscription_update"){
func__update_subscription(stripe,req,res,mydata);
}else if(req.query.type=="subscription_cancel"){
func__cancel_subscription(stripe,req,res,mydata);
}else if(req.query.type=="invoice-preview"){
func__invoice_preview(stripe,req,res,mydata);
}else if(req.query.type=="subscription_create"){
func__create_subscription(stripe,req,res,mydata);
}else if(req.query.type=="customer_create"){
func__create_customer(stripe,req,res,mydata);
}else if(req.query.type=="config"){
func__config(stripe,req,res,mydata);
}else if(req.query.type=="webhook_create"){
func__webhook_create(stripe,req,res,mydata);
}else if(req.query.type=="subscription_create_schedule"){
func__subscription_screate_schedule(stripe,req,res,mydata);
}else{
res.jsonp("working");
}
} catch (err) {
console.log(err.message)
res.jsonp(err.message);
}
});
// https://www.appfactory.studio/portal/admin/api/php/webhook.php
// https://stripewebhooks.appfactory.studio/webhook_action
// https://webhooks.appfactory.studio/stripe/webhook_action
app.post('/webhook_action', bodyParser.raw({ type: 'application/json' }),
async (req, res) => {
const stripe = require('stripe')(clientSecret, {
apiVersion: '2022-08-01',
appInfo: { // For sample support and debugging, not required for production:
name: "stripe-samples/subscription-use-cases/fixed-price",
version: "0.0.1",
url: "https://github.com/stripe-samples/subscription-use-cases/fixed-price"
}
});
// Retrieve the event by verifying the signature using the raw body and secret.
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.header('Stripe-Signature'),
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.log(err);
console.log(`⚠️ Webhook signature verification failed.`);
console.log(
`⚠️ Check the env file and enter the correct webhook secret.`
);
return res.sendStatus(400);
}
// Extract the object from the event.
const dataObject = event.data.object;
// Handle the event
// Review important events for Billing webhooks
// https://stripe.com/docs/billing/webhooks
// Remove comment to see the various objects sent for this sample
switch (event.type) {
case 'invoice.payment_succeeded':
if(dataObject['billing_reason'] == 'subscription_create') {
// The subscription automatically activates after successful payment
// Set the payment method used to pay the first invoice
// as the default payment method for that subscription
const subscription_id = dataObject['subscription']
const payment_intent_id = dataObject['payment_intent']
// Retrieve the payment intent used to pay the subscription
const payment_intent = await stripe.paymentIntents.retrieve(payment_intent_id);
try {
const subscription = await stripe.subscriptions.update(
subscription_id,
{
default_payment_method: payment_intent.payment_method,
},
);
console.log("Default payment method set for subscription:" + payment_intent.payment_method);
} catch (err) {
console.log(err);
console.log(`⚠️ Falied to update the default payment method for subscription: ${subscription_id}`);
}
};
break;
case 'invoice.payment_failed':
// If the payment fails or the customer does not have a valid payment method,
// an invoice.payment_failed event is sent, the subscription becomes past_due.
// Use this webhook to notify your user that their payment has
// failed and to retrieve new card details.
break;
case 'invoice.finalized':
// If you want to manually send out invoices to your customers
// or store them locally to reference to avoid hitting Stripe rate limits.
break;
case 'customer.subscription.deleted':
if (event.request != null) {
// handle a subscription cancelled by your request
// from above.
} else {
// handle subscription cancelled automatically based
// upon your subscription settings.
}
break;
case 'customer.subscription.trial_will_end':
// Send notification to your user that the trial will end
break;
default:
// Unexpected event type
}
res.sendStatus(200);
}
);
var httpsServer = https.createServer(credentials, app);
httpsServer.listen(active_processor.json.service_port);
}
//{"obj":"www15-stripe-vCLAgOX2"}{"message":"success"}
// pm2 delete www15;pm2 start server.js --name "www15" -- www15;pm2 log --lines 200 www15
// pm2 start server.js --name "www15" -- www15
// pm2 start server.js --name "www10" -- www10
// pm2 delete www15;pm2 start server.js --name www15 -- www15;pm2 logs www --lines 1000
// pm2 logs --lines 1000
//process.argv.forEach(function (val, index, array) {console.log(index + ': ' + val);});
// pm2 start server.js --name "www15_paypal" --namespace payapl -- www15
// pm2 start server.js --name "www15_stripe" --namespace stripe -- www15
// pm2 log --lines 500 www15-paypal

View File

@@ -0,0 +1,274 @@
const express = require('express');
const app = express();
const { resolve } = require('path');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
// Replace if using a different env file or config
require('dotenv').config({ path: './.env' });
if (
!process.env.STRIPE_SECRET_KEY ||
!process.env.STRIPE_PUBLISHABLE_KEY ||
!process.env.STATIC_DIR
) {
console.log(
'The .env file is not configured. Follow the instructions in the readme to configure the .env file. https://github.com/stripe-samples/subscription-use-cases'
);
console.log('');
process.env.STRIPE_SECRET_KEY
? ''
: console.log('Add STRIPE_SECRET_KEY to your .env file.');
process.env.STRIPE_PUBLISHABLE_KEY
? ''
: console.log('Add STRIPE_PUBLISHABLE_KEY to your .env file.');
process.env.STATIC_DIR
? ''
: console.log(
'Add STATIC_DIR to your .env file. Check .env.example in the root folder for an example'
);
process.exit();
}
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01',
appInfo: { // For sample support and debugging, not required for production:
name: "stripe-samples/subscription-use-cases/fixed-price",
version: "0.0.1",
url: "https://github.com/stripe-samples/subscription-use-cases/fixed-price"
}
});
// Use static to serve static assets.
app.use(express.static(process.env.STATIC_DIR));
// Use cookies to simulate logged in user.
app.use(cookieParser());
// Use JSON parser for parsing payloads as JSON on all non-webhook routes.
app.use((req, res, next) => {
if (req.originalUrl === '/webhook') {
next();
} else {
bodyParser.json()(req, res, next);
}
});
app.get('/', (req, res) => {
const path = resolve(process.env.STATIC_DIR + '/register.html');
res.sendFile(path);
});
app.get('/config', async (req, res) => {
const prices = await stripe.prices.list({
lookup_keys: ['sample_basic', 'sample_premium'],
expand: ['data.product']
});
res.send({
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
prices: prices.data,
});
});
app.post('/create-customer', async (req, res) => {
// Create a new customer object
const customer = await stripe.customers.create({
email: req.body.email,
});
// Save the customer.id in your database alongside your user.
// We're simulating authentication with a cookie.
res.cookie('customer', customer.id, { maxAge: 900000, httpOnly: true });
res.send({ customer: customer });
});
app.post('/create-subscription', async (req, res) => {
// Simulate authenticated user. In practice this will be the
// Stripe Customer ID related to the authenticated user.
const customerId = req.cookies['customer'];
// Create the subscription
const priceId = req.body.priceId;
try {
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{
price: priceId,
}],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
});
res.send({
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client_secret,
});
} catch (error) {
return res.status(400).send({ error: { message: error.message } });
}
});
app.get('/invoice-preview', async (req, res) => {
const customerId = req.cookies['customer'];
const priceId = process.env[req.query.newPriceLookupKey.toUpperCase()];
const subscription = await stripe.subscriptions.retrieve(
req.query.subscriptionId
);
const invoice = await stripe.invoices.retrieveUpcoming({
customer: customerId,
subscription: req.query.subscriptionId,
subscription_items: [ {
id: subscription.items.data[0].id,
price: priceId,
}],
});
res.send({ invoice });
});
app.post('/cancel-subscription', async (req, res) => {
// Cancel the subscription
try {
const deletedSubscription = await stripe.subscriptions.del(
req.body.subscriptionId
);
res.send({ subscription: deletedSubscription });
} catch (error) {
return res.status(400).send({ error: { message: error.message } });
}
});
app.post('/update-subscription', async (req, res) => {
try {
const subscription = await stripe.subscriptions.retrieve(
req.body.subscriptionId
);
const updatedSubscription = await stripe.subscriptions.update(
req.body.subscriptionId, {
items: [{
id: subscription.items.data[0].id,
price: process.env[req.body.newPriceLookupKey.toUpperCase()],
}],
}
);
res.send({ subscription: updatedSubscription });
} catch (error) {
return res.status(400).send({ error: { message: error.message } });
}
});
app.get('/subscriptions', async (req, res) => {
// Simulate authenticated user. In practice this will be the
// Stripe Customer ID related to the authenticated user.
const customerId = req.cookies['customer'];
const subscriptions = await stripe.subscriptions.list({
customer: customerId,
status: 'all',
expand: ['data.default_payment_method'],
});
res.json({subscriptions});
});
app.post('/webhook',
bodyParser.raw({ type: 'application/json' }),
async (req, res) => {
// Retrieve the event by verifying the signature using the raw body and secret.
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.header('Stripe-Signature'),
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.log(err);
console.log(`⚠️ Webhook signature verification failed.`);
console.log(
`⚠️ Check the env file and enter the correct webhook secret.`
);
return res.sendStatus(400);
}
// Extract the object from the event.
const dataObject = event.data.object;
// Handle the event
// Review important events for Billing webhooks
// https://stripe.com/docs/billing/webhooks
// Remove comment to see the various objects sent for this sample
switch (event.type) {
case 'invoice.payment_succeeded':
if(dataObject['billing_reason'] == 'subscription_create') {
// The subscription automatically activates after successful payment
// Set the payment method used to pay the first invoice
// as the default payment method for that subscription
const subscription_id = dataObject['subscription']
const payment_intent_id = dataObject['payment_intent']
// Retrieve the payment intent used to pay the subscription
const payment_intent = await stripe.paymentIntents.retrieve(payment_intent_id);
try {
const subscription = await stripe.subscriptions.update(
subscription_id,
{
default_payment_method: payment_intent.payment_method,
},
);
console.log("Default payment method set for subscription:" + payment_intent.payment_method);
} catch (err) {
console.log(err);
console.log(`⚠️ Falied to update the default payment method for subscription: ${subscription_id}`);
}
};
break;
case 'invoice.payment_failed':
// If the payment fails or the customer does not have a valid payment method,
// an invoice.payment_failed event is sent, the subscription becomes past_due.
// Use this webhook to notify your user that their payment has
// failed and to retrieve new card details.
break;
case 'invoice.finalized':
// If you want to manually send out invoices to your customers
// or store them locally to reference to avoid hitting Stripe rate limits.
break;
case 'customer.subscription.deleted':
if (event.request != null) {
// handle a subscription cancelled by your request
// from above.
} else {
// handle subscription cancelled automatically based
// upon your subscription settings.
}
break;
case 'customer.subscription.trial_will_end':
// Send notification to your user that the trial will end
break;
default:
// Unexpected event type
}
res.sendStatus(200);
}
);
app.listen(4242, () => console.log(`Node server listening on port http://localhost:${4242}!`));