initial commit 2

This commit is contained in:
equippedcoding-master
2025-09-17 15:19:57 -05:00
parent e2c98790b2
commit 1c59875b8a
55391 changed files with 15 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
{
"name": "subscription-with-per-seat-prices",
"configureDotEnv": true,
"integrations": [
{
"name": "usage-based-subscriptions",
"clients": ["web"],
"servers": ["java", "node", "php", "python", "ruby","go", "dotnet"]
},
{
"name": "fixed-price-subscriptions",
"clients": ["vanillajs", "react"],
"servers": ["java", "node", "php-slim", "python", "ruby","go", "dotnet"]
}
]
}

View File

@@ -0,0 +1,14 @@
# Stripe keys
STRIPE_PUBLISHABLE_KEY=pk_12345
STRIPE_SECRET_KEY=sk_12345
STRIPE_WEBHOOK_SECRET=whsec_1234
# Stripe key for React front end
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_12345
# Billing variables
BASIC=price_12345...
PREMIUM=price_7890...
# Environment variables
STATIC_DIR=../../client/vanillajs

View File

@@ -0,0 +1,48 @@
.env
.DS_Store
.vscode/*
!.vscode/extensions.json
.byebug_history
**/Debug/**
**/project-cache/**
**/.vs/**
**/obj/**
# Dependencies
node_modules
package-lock.json
yarn.lock
!/yarn.lock
composer.lock
# Ruby files
Gemfile.lock
# Python files
__pycache__
venv
env
# PHP files
vendor
logs
# Java files
.settings
target/
.classpath
.factorypath
.project
# Typescript
dist
# React
build
# act
.secrets
spec/examples.txt
sample-ci/

View File

@@ -0,0 +1 @@
--require spec_helper

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem 'rspec'
gem 'rest-client'
gem 'byebug'
gem 'stripe'
gem 'dotenv'
gem 'selenium-webdriver'
gem 'capybara'
gem 'capybara-screenshot'
gem 'rspec-github', require: false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Stripe, Inc. (https://stripe.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,273 @@
# Set up subscriptions with Stripe Billing
This sample shows how to create a customer and subscribe them to a plan with
[Stripe Billing](https://stripe.com/billing). For the official documentation
for Stripe billing checkout the [overview](https://stripe.com/docs/billing).
| | [Checkout](https://github.com/stripe-samples/checkout-single-subscription) | [Fixed-price-subscriptions with Elements](./fixed-price-subscriptions) | [Usage-based-subscriptions with Elements](./usage-based-subscriptions) |
| :-------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| **Demo** | [checkout.stripe.dev](https://checkout.stripe.dev) | | | |
| **Define prices in: CLI, Dashboard, or API** Create a price with the Stripe: CLI, Dashboard, or API. | ✅ | ✅ | ✅ |
| **Charge users a fixed price on a recurring basis** Create a subscription with a fixed price recurring monthly/yearly/etc. | ✅ | ✅ | |
| **Charge customers based on their usage.** Create a metered subscriptions so you can charge customers based on their usage. | ✅ | | ✅ |
| **Apple Pay & Google Pay support** | ✅ Built in, no extra code needed | | |
| **Coupon support for subscriptions** | ✅ | ✅ | ✅ |
The hosted demos linked above are running in test mode -- use
`4242424242424242` as a test card number with any CVC + future expiration date.
Use the `4000002500003155` test card number to trigger a 3D Secure challenge
flow.
Read more about test cards on Stripe at https://stripe.com/docs/testing.
## Run the sample locally
_This sample can be installed two ways -- Stripe CLI or git clone. The `.env`
configuration will vary depending on which way you install._
### Requirements
- **A Stripe account**: You can sign up for a Stripe account here: https://dashboard.stripe.com/register
- **Stripe API Keys**: Available in your Stripe dashboard here: https://dashboard.stripe.com/test/apikeys
- **2 Prices**: This sample demonstrates two tiers of pricing. You'll need two Price objects on your Stripe account with `lookup_key` set to `sample_basic` and `sample_premium`. See [How to create Prices](#how-to-create-prices) below for more information.
### Installing the sample
The Stripe CLI is the fastest way to clone and configure a sample to run
locally and is the recommended approach as it will only download the code
required for the server language you select. Alternatively, you can download
and run directly with this repository.
#### Option 1: Installing with Stripe CLI
1. If you haven't already installed the CLI, follow the [installation
steps](https://stripe.com/docs/stripe-cli#install). The CLI is useful for
cloning samples and locally testing webhooks and Stripe integrations.
2. Ensure the CLI is linked to your Stripe account by running:
```sh
stripe login
```
3. Start the sample installer and follow the prompts with:
```sh
stripe samples create subscription-use-cases
```
The CLI will walk you through picking your integration type, server and client
languages, and partially configuring your `.env` file with your Stripe API keys.
4. Move into the server directory:
```sh
cd subscription-use-cases/server
```
5. Open `server/.env`. The `STATIC_DIR` value
should be `../client` when installed using the Stripe CLI.
```yml
# Stripe keys
STRIPE_PUBLISHABLE_KEY=pk_12345
STRIPE_SECRET_KEY=sk_12345
STRIPE_WEBHOOK_SECRET=whsec_1234
# Stripe key for React front end
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_12345
# Environment variables
STATIC_DIR=../client
```
6. Follow the instructions in `server/README.md` for how to build and/or run the server.
7. View in the browser: [localhost:4242](http://localhost:4242) and test with `4242424242424242`.
8. [Optional] Forward webhook events
You can use the Stripe CLI to forward webhook events to your server running
locally.
```
stripe listen --forward-to localhost:4242/webhook
```
You should see events logged in the console where the CLI is running.
When you are ready to create a live webhook endpoint, follow our guide in the
docs on [configuring a webhook endpoint in the
dashboard](https://stripe.com/docs/webhooks/setup#configure-webhook-settings).
#### Option 2: Installing manually
If you do not want to use the Stripe CLI, you can manually clone and configure the sample:
1. Clone the repository
```sh
git clone git@github.com:stripe-samples/subscription-use-cases.git
cd subscription-use-cases
```
2. Configure `.env`
The `.env` file contains the API keys and some settings to enable the sample to
run with data for your Stripe account.
Copy the `.env.example` file from the root of the project into a file named
`.env` in the folder of the server language you want to use. For example with node:
```sh
cp .env.example fixed-price-subscriptions/server/node/.env
cd fixed-price-subscriptions/server/node
```
For example with ruby:
```sh
cp .env.example fixed-price-subscriptions/server/ruby/.env
cd fixed-price-subscriptions/server/ruby
```
3. Edit the copied `.env` and populate all of the variables. For more information see: [`.env` config](#env-config)
```yml
# Stripe keys
STRIPE_PUBLISHABLE_KEY=pk_12345
STRIPE_SECRET_KEY=sk_12345
STRIPE_WEBHOOK_SECRET=whsec_1234
# Stripe key for React front end
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_12345
# Billing variables
BASIC=price_12345...
PREMIUM=price_7890...
# Environment variables
STATIC_DIR=../../client/vanillajs
```
4. Follow the server instructions on how to run:
Pick the server language you want and follow the instructions in the server folder README on how to run.
```
cd fixed-price-subscriptions/server/node # there's a README in this folder with instructions
npm install
npm start
```
5. [Optional] Forward webhook events
You can use the Stripe CLI to forward webhook events to your server running
locally.
If you haven't already, [install the CLI](https://stripe.com/docs/stripe-cli)
and [link your Stripe
account](https://stripe.com/docs/stripe-cli#link-account).
```
stripe listen --forward-to localhost:4242/webhook
```
You should see events logged in the console where the CLI is running.
When you are ready to create a live webhook endpoint, follow our guide in the
docs on [configuring a webhook endpoint in the
dashboard](https://stripe.com/docs/webhooks/setup#configure-webhook-settings).
## How to create Prices
### With Stripe CLI Fixtures
Use the `seed.json` fixture file:
```sh
stripe fixtures seed_v1.json
```
### With Stripe CLI API calls
Or run the following commands and copy the resulting IDs.
```sh
stripe prices create --unit-amount 500 --currency usd -d "recurring[interval]=month" -d "product_data[name]=basic" --lookup-key sample_basic
```
```sh
stripe prices create --unit-amount 900 --currency usd -d "recurring[interval]=month" -d "product_data[name]=premium" --lookup-key sample_premium
```
### With cURL
Replace `sk_test_xxx` with your secret API key:
```sh
curl https://api.stripe.com/v1/prices \
-u sk_test_xxx: \
-d "unit_amount"=500 \
-d "currency"=usd \
-d "recurring[interval]"=month \
-d "product_data[name]"=basic \
-d "lookup_key"=sample_basic \
```
```sh
curl https://api.stripe.com/v1/prices \
-u sk_test_xxx: \
-d "unit_amount"=900 \
-d "currency"=usd \
-d "recurring[interval]"=month \
-d "product_data[name]"=premium \
-d "lookup_key"=sample_premium \
```
## `.env` config
Example configuration file [`.env.example`](./.env.example)
- **STRIPE_PUBLISHABLE_KEY**: Found in the dashboard here: https://dashboard.stripe.com/test/apikeys
- **STRIPE_SECRET_KEY**: Found in the dashboard here: https://dashboard.stripe.com/test/apikeys
- **STRIPE_WEBHOOK_SECRET**: If using the Stripe CLI (recommended) run `stripe listen --print-secret`, otherwise you can find the signing secret for the webhook endpoint in the dashboard by viewing the details of the endpoint here: https://dashboard.stripe.com/test/webhooks.
- **STATIC_DIR**: The path to the directory containing the client side code. For vanillajs on the client, this will be `../../client/vanillajs`.
## 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)
Sign up to [stay updated with developer news](https://go.stripe.global/dev-digest).
## Author(s)
- [@ctrudeau-stripe](https://twitter.com/trudeaucj)
- [@suz-stripe](https://twitter.com/noopkat)
- [@dawn-stripe](https://twitter.com/dawnlambeth)
- [@cjavilla-stripe](https://twitter.com/cjav_dev)
## Contributors
<a href="https://github.com/stripe-samples/subscription-use-cases/graphs/contributors">
<img src="https://contrib.rocks/image?repo=stripe-samples/subscription-use-cases" />
</a>
Made with [contrib.rocks](https://contrib.rocks).

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,38 @@
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');
console.log(subscriptions);
subscriptionsDiv.innerHTML = subscriptions.data.map((subscription) => {
console.log(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,62 @@
// 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';
}
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

View File

@@ -0,0 +1,31 @@
<!--
<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 mb-5" style="min-height: 500px;">
<div class="mb-3" id="address-element"></div>
<div class="mb-3" id="payment-element"></div>
<button id="stripe_start_membership_id" style="display: none;" class="mb-5 btn btn-outline-success w-100">Start Membership</button>
</div>
</div>
</div>

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,31 @@
<!--
<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 mb-5" style="min-height: 500px;">
<div class="mb-3" id="address-element"></div>
<div class="mb-3" id="payment-element"></div>
<button id="start_membership_id" style="display: none;" class="mb-5 btn btn-outline-success w-100">Start Membership</button>
</div>
</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,26 @@
{
"name": "stripe-subscription-with-fixed-price",
"version": "1.0.0",
"description": "A Stripe sample implementing cards for subscriptions with fixed prices.",
"main": "server.js",
"scripts": {
"server": "node server/node/server.js",
"reactClient": "cd client/react && npm start",
"startReact": "concurrently \"yarn reactClient\" \"yarn server\"",
"startVanillajs": "yarn server",
"start": "yarn server",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "stripe-demos",
"license": "ISC",
"dependencies": {
"autoprefixer": "^9.7.6",
"body-parser": "^1.19.0",
"concurrently": "4.1.2",
"cookie-parser": "^1.4.5",
"dotenv": "^8.0.0",
"express": "^4.17.1",
"postcss-cli": "^7.1.1",
"stripe": "^7.4.0"
}
}

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,89 @@
{
"_meta": {
"template_version": 0
},
"fixtures": [
{
"name": "pro_basic",
"path": "/v1/products",
"method": "post",
"params": {
"name": "Professional"
}
},
{
"name": "pro_basic_price",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${pro_basic:id}",
"lookup_key": "pro_basic_v1",
"currency": "usd",
"unit_amount": 2500,
"recurring": {
"interval": "month"
},
"metadata": {
"sample": "fixed-price"
}
}
},
{
"name": "organization",
"path": "/v1/products",
"method": "post",
"params": {
"name": "Organization"
}
},
{
"name": "organization_price",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${organization:id}",
"lookup_key": "organization_v1",
"currency": "usd",
"unit_amount": 5000,
"recurring": {
"interval": "month"
},
"metadata": {
"sample": "fixed-price"
}
}
},
{
"name": "enterprise",
"path": "/v1/products",
"method": "post",
"params": {
"name": "Enterprise"
}
},
{
"name": "enterprise_price",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${enterprise:id}",
"lookup_key": "enterprise_v1",
"currency": "usd",
"unit_amount": 19900,
"recurring": {
"interval": "month"
},
"metadata": {
"sample": "fixed-price"
}
}
}
]
}

View File

@@ -0,0 +1,113 @@
{
"_meta": {
"template_version": 0
},
"fixtures": [
{
"name": "personal_v2",
"path": "/v1/products",
"method": "post",
"params": {
"name": "Personal"
}
},
{
"name": "personal_v2",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${personal_v2:id}",
"lookup_key": "personal_v2",
"currency": "usd",
"unit_amount": 1500,
"recurring": {
"interval": "month"
},
"metadata": {
"sample": "fixed-price"
}
}
},
{
"name": "business_v2",
"path": "/v1/products",
"method": "post",
"params": {
"name": "Business"
}
},
{
"name": "business_v2",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${business_v2:id}",
"lookup_key": "business_v2",
"currency": "usd",
"unit_amount": 2500,
"recurring": {
"interval": "month"
},
"metadata": {
"sample": "fixed-price"
}
}
},
{
"name": "organization_v2",
"path": "/v1/products",
"method": "post",
"params": {
"name": "Organization"
}
},
{
"name": "organization_v2",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${organization_v2:id}",
"lookup_key": "organization_v2",
"currency": "usd",
"unit_amount": 5000,
"recurring": {
"interval": "month"
},
"metadata": {
"sample": "fixed-price"
}
}
},
{
"name": "enterprise_v2",
"path": "/v1/products",
"method": "post",
"params": {
"name": "Enterprise"
}
},
{
"name": "enterprise_price",
"path": "/v1/prices",
"method": "post",
"params": {
"product": "${enterprise_v2:id}",
"lookup_key": "enterprise_v2",
"currency": "usd",
"unit_amount": 20000,
"recurring": {
"interval": "month"
},
"metadata": {
"sample": "fixed-price"
}
}
}
]
}

View File

@@ -0,0 +1,25 @@
# Subscriptions with fixed price
An [Express server](http://expressjs.com) implementation.
## Requirements
- Node v10+
- [Configured .env file](../../../README.md#env-config)
## How to run
1. Install dependencies
```
npm install
```
2. Run the application
```
npm start
```
3. Go to `localhost:4242` to see the demo

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,24 @@
{
"name": "appfactorystudio_stripe_subscriptions",
"version": "1.0.0",
"description": "Stripe subscriptions.",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js"
},
"author": "appfactorystudio",
"dependencies": {
"autoprefixer": "^10.4.0",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"dotenv": "^16.0.0",
"express": "^4.17.1",
"stripe": "14.3.0",
"ejs": "^3.1.6",
"html-entities": "^2.4.0",
"mysql2": "^3.6.0",
"node-fetch": "^3.2.1"
}
}

View File

@@ -0,0 +1,111 @@
<?php
$param1 = $argv[1];
function _sendEmail($param2,$param3 ){
$config_domain = __get_main_configurations();
$managed_domain = $config_domain["managed_domain"];
$config = $config_domain["configurations"];
// echo json_encode($config);
// echo json_encode($GLOBALS) . "\n";
// echo $_ENV['MY_HOST'] . "\n";
$integration = GetActiveEmailIntegration($config);
if($integration==null){
EchoJsonResponse("Email integration is null!");
return;
}
$otherdata = json_decode(file_get_contents($param3), true);
$otherdata["mail_handlers"]["email_response"]["integration"] = $integration;
$otherdata["mail_handlers"]["email_confirm"]["integration"] = $integration;
$otherdata["mail_handlers"]["email_alert"]["integration"] = $integration;
Utils_WriteFile("/mnt/appfactorystudio/tmp/tmpfile1.json",$otherdata);
SendEmailBuilder($otherdata["mail_handlers"]["email_response"],$managed_domain, function($resp1) use($otherdata,$managed_domain) {
Utils_WriteFile("/mnt/appfactorystudio/tmp/tmpfile2.json",$resp1);
SendEmailBuilder($otherdata["mail_handlers"]["email_confirm"],$managed_domain, function($resp2) use($otherdata,$managed_domain,$resp1){
Utils_WriteFile("/mnt/appfactorystudio/tmp/tmpfile2.json",$resp2);
SendEmailBuilder($otherdata["mail_handlers"]["email_alert"],$managed_domain, function($resp3) use($otherdata,$managed_domain,$resp1,$resp2){
// EchoJsonObject(["resp1" => $resp1, "resp2" => $resp2]);
Utils_WriteFile("/mnt/appfactorystudio/tmp/tmpfile3.json",$resp3);
});
});
});
// $db = new DB();
// $rows = $db->query("SELECT * FROM general_data_storage WHERE name='$param2'",[],PDO::FETCH_ASSOC);
// if($rows->count() > 0){
// $mydata = $rows->results()[0];
// $mydata["json"] = html_entity_decode($mydata["json"]);
// $otherdata = json_decode($mydata["json"], true);
// $otherdata["mail_handlers"]["email_response"]["integration"] = $integration;
// $otherdata["mail_handlers"]["email_confirm"]["integration"] = $integration;
// // EchoJsonObject(["data" => json_last_error(), "param" => $param2, "count" => $rows->count()]);
// SendEmailBuilder($otherdata["mail_handlers"]["email_response"],$managed_domain, function($resp1) use($otherdata,$managed_domain) {
// SendEmailBuilder($otherdata["mail_handlers"]["email_confirm"],$managed_domain, function($resp2) use($otherdata,$managed_domain,$resp1){
// EchoJsonObject(["mail_handlers" => $otherdata, "resp1" => $resp1, "resp2" => $resp2]);
// });
// });
// // EchoJsonResponse("Sending...");
// }else{
// EchoJsonResponse("No record found");
// }
// $db = null;
}
function _sendEmail2($param2,$param3 ){
$config_domain = __get_main_configurations();
$managed_domain = $config_domain["managed_domain"];
$config = $config_domain["configurations"];
$integration = GetActiveEmailIntegration($config);
if($integration==null){
EchoJsonResponse("Email integration is null!");
return;
}
$otherdata = json_decode(file_get_contents($param3), true);
foreach ($otherdata["mail_handlers"] as $key => $value) {
// $value[$key]["integration"] = $integration;
$otherdata["mail_handlers"][$key]["integration"] = $integration;
$mail = $otherdata["mail_handlers"][$key];
SendEmailBuilder($mail,$managed_domain, function($resp) use($otherdata,$managed_domain,$key){
EchoJsonObject(["resp1" => $resp["emails"]]);
});
}
}
if($param1=="send_email"){
require dirname( __DIR__,4 ) . "/core/api/php/includes/init.php";
require dirname( __DIR__,4 ) . "/core/api/php/includes/functions.php";
$param2 = $argv[2];
$param3 = $argv[3];
_sendEmail($param2,$param3);
}
if($param1=="send_email2"){
require dirname( __DIR__,4 ) . "/core/api/php/includes/init.php";
require dirname( __DIR__,4 ) . "/core/api/php/includes/functions.php";
$param2 = $argv[2];
$param3 = $argv[3];
_sendEmail2($param2,$param3);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,269 @@
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(`⚠️ Failed 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}!`));

View File

@@ -0,0 +1,15 @@
{
"name": "webhook",
"version": "1.0.0",
"description": "",
"main": "webhook.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"stripe": "^17.3.1"
}
}

View File

@@ -0,0 +1,54 @@
// This is your test secret API key.
const stripe = require('stripe')('sk_test_51OIu21J7BKlr2pgsABfPagNLQmRYdn8wSDNOj9ph6q0T6ThchqyfRC585qp5iG1bHM3MrxnTHOUGQRGhZ4K6h5Sg00DmDdAv7Q');
// Replace this endpoint secret with your endpoint's unique secret
// If you are testing with the CLI, find the secret by running 'stripe listen'
// If you are using an endpoint defined with the API or dashboard, look in your webhook settings
// at https://dashboard.stripe.com/webhooks
// whsec_ce937ab59c83dcecc659324f312e6044cc80246e3932d9b93bee27a3a6a513ef
const endpointSecret = 'whsec_ce937ab59c83dcecc659324f312e6044cc80246e3932d9b93bee27a3a6a513ef';
const express = require('express');
const app = express();
app.post('/webhook', express.raw({type: 'application/json'}), (request, response) => {
let event = request.body;
// Only verify the event if you have an endpoint secret defined.
// Otherwise use the basic event deserialized with JSON.parse
if (endpointSecret) {
// Get the signature sent by Stripe
const signature = request.headers['stripe-signature'];
try {
event = stripe.webhooks.constructEvent(
request.body,
signature,
endpointSecret
);
} catch (err) {
console.log(`⚠️ Webhook signature verification failed.`, err.message);
return response.sendStatus(400);
}
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log(`PaymentIntent for ${paymentIntent.amount} was successful!`);
// Then define and call a method to handle the successful payment intent.
// handlePaymentIntentSucceeded(paymentIntent);
break;
case 'payment_method.attached':
const paymentMethod = event.data.object;
console.log(paymentMethod);
// Then define and call a method to handle the successful attachment of a PaymentMethod.
// handlePaymentMethodAttached(paymentMethod);
break;
default:
// Unexpected event type
console.log(`Unhandled event type ${event.type}.`);
}
// Return a 200 response to acknowledge receipt of the event
response.send();
});
app.listen(4242, () => console.log('Running on port 4242'));

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 KiB

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}!`));

View File

@@ -0,0 +1,183 @@
# Subscriptions with fixed price
Create a subscription for an online service with fixed-price options, and work with Stripe Elements to host a payment form on your servers.
This sample shows how to create a customer, set up a card for recurring use, and subscribe them to a subscription plan with
[Stripe Billing](https://stripe.com/billing).
<!-- acct_1OIu21J7BKlr2pgs -->
**Demo**
<img src="./demo.gif" alt="Preview of sample" style="max-width:25%;">
Read more about test cards on Stripe at https://stripe.com/docs/testing.
### Features:
- 💳Securely collect card details
- 🔒Save the payment method details to a customer
- 🚫Handle payment failures
- 💰Subscribe the customer to a subscription plan
- Upgrade and downgrade on prices
## How to run locally
This sample includes [7 server implementations](server/) in our most popular languages. Follow the steps below to run one of the servers locally. This example also includes VanillaJS (no view frameworks) and a React example implementation. Follow the instructions below to get them running.
**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:
```
git clone git@github.com:stripe-samples/subscription-use-cases.git
```
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>
```
`STATIC_DIR` tells the server where the client files are located and does not need to be modified unless you move the server files.
**[Optional] 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
```
You will need to take the Stripe publishable key and set only one variable for the React server.
```
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_12345
```
**2. Create Products and Prices on Stripe**
This sample requires [Prices](https://stripe.com/docs/api/prices) with
`lookup_key`s of `sample_basic` and `sample_premium` to render the pricing page
and create the Subscription. Products and Prices are objects on Stripe that you
use to model a subscription.
### With Stripe CLI Fixtures
Use the `seed.json` fixture file:
```sh
stripe fixtures seed.json
```
### With Stripe CLI API calls
Or run the following commands and copy the resulting IDs.
```sh
stripe prices create --unit-amount 500 --currency usd -d "recurring[interval]=month" -d "product_data[name]=basic" --lookup-key sample_basic
```
```sh
stripe prices create --unit-amount 900 --currency usd -d "recurring[interval]=month" -d "product_data[name]=premium" --lookup-key sample_premium
```
### With cURL
Replace `sk_test_xxx` with your secret API key:
```sh
curl https://api.stripe.com/v1/prices \
-u sk_test_xxx: \
-d "unit_amount"=500 \
-d "currency"=usd \
-d "recurring[interval]"=month \
-d "product_data[name]"=basic \
-d "lookup_key"=sample_basic \
```
```sh
curl https://api.stripe.com/v1/prices \
-u sk_test_xxx: \
-d "unit_amount"=900 \
-d "currency"=usd \
-d "recurring[interval]"=month \
-d "product_data[name]"=premium \
-d "lookup_key"=sample_premium \
```
**3. Follow the server instructions on how to run:**
Pick the server language you want and follow the instructions in the server folder README on how to run.
```
cd server/node # there's a README in this folder with instructions
npm install
npm start
```
** To run the React client & the node server**
```
cd server/node # there's a README in this folder with instructions
npm install
npm start
```
**4. [Optional] Run a webhook locally:**
You can use the Stripe CLI to forward webhook events to your server running locally.
If you haven't already, [install the CLI](https://stripe.com/docs/stripe-cli) and [link your Stripe account](https://stripe.com/docs/stripe-cli#link-account).
```
stripe listen --forward-to localhost:4242/webhook
```
The CLI will print a webhook secret key to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your .env file.
You should see events logged in the console where the CLI is running.
When you are ready to create a live webhook endpoint, follow our guide in the docs on [configuring a webhook endpoint in the dashboard](https://stripe.com/docs/webhooks/setup#configure-webhook-settings).
## 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)
- [@suz-stripe](https://twitter.com/noopkat)