add readme
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
STATIC_DIR='./client'
|
||||
PAYSTACK_SECRET_KEY=
|
||||
PORT='5000'
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 PaystackOSS
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Paystack Subscriptions Sample App
|
||||
|
||||
This sample application shows how to integrate Paystack's Subscriptions API in your apps. For the official documentation for Paystack Subscriptions, [head over to the docs](https://paystack.com/docs/payments/subscriptions)
|
||||
|
||||
|
||||
## Demo
|
||||
|
||||
View a live demo of the app [here]().
|
||||
|
||||
## Get Started
|
||||
|
||||
### Requirements
|
||||
- **A Paystack account**: If you don't already have one, [sign up for a Paystack account](https://dashboard.paystack.com/#/signup). You'll need to do this to get your API keys.
|
||||
- **API keys**: You can grab these [from your Paystack dashboard](https://dashboard.paystack.com/#/settings/developers)
|
||||
- **Existing plans**: You'll need to have existing (active) plan objects that you can subscribe your customers to. If you don't already have any plans, you can just create a couple [from your Paystack dashboard](https://dashboard.paystack.com/#/plans?status=active)
|
||||
|
||||
### Running the sample locally
|
||||
|
||||
1. Clone this repo:
|
||||
```
|
||||
git clone https://github.com/PaystackOSS/sample-subscriptions-app
|
||||
```
|
||||
|
||||
2. Navigate to the root directory and install dependencies
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Rename the `.env.example` file to `.env` and add your Paystack secret key. You can also change the default port from 5000 to a port of your choosing:
|
||||
|
||||
```
|
||||
PAYSTACK_SECRET_KEY=sk_domain_xxxxxx
|
||||
```
|
||||
|
||||
4. Start the application
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
5. Visit http://localhost:5000 in your browser to interact with the app. You should be able to signup/login, subscribe to a plan, and view/manage your existing plan(s).
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
If you notice any issues with this app, please [open an issue](https://github.com/PaystackOSS/sample-subscriptions-app/issues/new). PRs are also more than welcome, so feel free to [submit a PR](https://github.com/PaystackOSS/sample-subscriptions-app/compare) to fix an issue, or add a new feature!
|
||||
|
||||
## License
|
||||
|
||||
This repository is made available under the MIT license. Read [LICENSE.md](https://github.com/PaystackOSS/sample-subscriptions-app/blob/master/LICENSE.md) for more information.
|
||||
|
||||
|
||||
+19
-16
@@ -1,6 +1,6 @@
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
const customer_id = window.localStorage.getItem("customer_id");
|
||||
const customer_email = window.localStorage.getItem("customer_email");
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
const customer_id = window.localStorage.getItem('customer_id');
|
||||
const customer_email = window.localStorage.getItem('customer_email');
|
||||
|
||||
const subscriptions = await fetch(
|
||||
`/subscription?customer=${customer_id}`
|
||||
@@ -8,7 +8,7 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
let subscription = subscriptions[0];
|
||||
|
||||
if (subscription) {
|
||||
const subscriptionInfoDiv = document.getElementById("subscription-info");
|
||||
const subscriptionInfoDiv = document.getElementById('subscription-info');
|
||||
subscriptionInfoDiv.innerHTML = `
|
||||
<hr>
|
||||
<p>Hi ${customer_email}<p>
|
||||
@@ -16,6 +16,9 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
<p>
|
||||
Status: ${subscription.status}
|
||||
</p>
|
||||
<p>
|
||||
Subscription Code: ${subscription.subscription_code}
|
||||
</p>
|
||||
<p>
|
||||
Card on file: ${subscription.authorization.brand} card ending in ${
|
||||
subscription.authorization.last4
|
||||
@@ -32,22 +35,22 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
}" target="_blank"> Manage subscription </a><br />
|
||||
`;
|
||||
} else {
|
||||
const plans = await fetch("/plans", {
|
||||
method: "get",
|
||||
const plans = await fetch('/plans', {
|
||||
method: 'get',
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((error) => console.log(error));
|
||||
|
||||
let accountDashDiv = document.getElementById("account-dashboard");
|
||||
let accountDashDiv = document.getElementById('account-dashboard');
|
||||
accountDashDiv.innerHTML +=
|
||||
"<p>You are currently not on any plan. Select a plan below to subscribe.</p>";
|
||||
'<p>You are currently not on any plan. Select a plan below to subscribe.</p>';
|
||||
|
||||
let selectPlanDiv = document.createElement("div");
|
||||
selectPlanDiv.style.display = "flex";
|
||||
selectPlanDiv.style.flexDirection = "row";
|
||||
let selectPlanDiv = document.createElement('div');
|
||||
selectPlanDiv.style.display = 'flex';
|
||||
selectPlanDiv.style.flexDirection = 'row';
|
||||
|
||||
plans.forEach((plan) => {
|
||||
let planDiv = document.createElement("div");
|
||||
let planDiv = document.createElement('div');
|
||||
planDiv.innerHTML = `
|
||||
<div class="card" style="width: 18rem; margin: 1rem; text-align: center">
|
||||
<div class="card-body">
|
||||
@@ -70,11 +73,11 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
});
|
||||
|
||||
async function signUpForPlan(plan_code) {
|
||||
let email = window.localStorage.getItem("customer_email");
|
||||
let { authorization_url } = await fetch("/initialize-transaction-with-plan", {
|
||||
method: "POST",
|
||||
let email = window.localStorage.getItem('customer_email');
|
||||
let { authorization_url } = await fetch('/initialize-transaction-with-plan', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
|
||||
Generated
+7
-1084
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "subscriptions-example",
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@paystack/paystack-sdk": "^1.0.0-beta.6",
|
||||
"@paystack/paystack-sdk": "^1.0.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,40 @@
|
||||
require("dotenv").config({ path: "../.env" });
|
||||
const express = require("express");
|
||||
const port = process.env.PORT;
|
||||
const { resolve } = require("path");
|
||||
const Paystack = require("@paystack/paystack-sdk");
|
||||
const crypto = require("crypto");
|
||||
require('dotenv').config({ path: './.env' });
|
||||
const express = require('express');
|
||||
const { resolve } = require('path');
|
||||
const Paystack = require('@paystack/paystack-sdk');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const app = express();
|
||||
const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY);
|
||||
const plan_codes = ["PLN_12qw4oagab13zvy", "PLN_yb73itushktdpth"];
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static(process.env.STATIC_DIR));
|
||||
|
||||
app.get("/", async (req, res) => {
|
||||
const path = resolve(process.env.STATIC_DIR + "/login.html");
|
||||
app.get('/', async (req, res) => {
|
||||
const path = resolve(process.env.STATIC_DIR + '/login.html');
|
||||
res.sendFile(path);
|
||||
});
|
||||
|
||||
app.get("/plans", async (req, res) => {
|
||||
app.get('/plans', async (req, res) => {
|
||||
let fetchPlansResponse = await paystack.plan.list({});
|
||||
|
||||
if (fetchPlansResponse.status === false) {
|
||||
console.log("Error fetching plans: ", fetchPlansResponse.message);
|
||||
console.log('Error fetching plans: ', fetchPlansResponse.message);
|
||||
return res
|
||||
.status(400)
|
||||
.send(`Error fetching subscriptions: ${fetchPlansResponse.message}`);
|
||||
}
|
||||
|
||||
let plans = fetchPlansResponse.data.filter(
|
||||
(plan) => plan_codes.indexOf(plan.plan_code) !== -1
|
||||
);
|
||||
return res.status(200).send(plans);
|
||||
return res.status(200).send(fetchPlansResponse.data);
|
||||
});
|
||||
|
||||
app.get("/subscription", async (req, res) => {
|
||||
app.get('/subscription', async (req, res) => {
|
||||
try {
|
||||
let { customer } = req.query;
|
||||
|
||||
if (!customer) {
|
||||
throw Error("Please include a valid customer ID");
|
||||
throw Error('Please include a valid customer ID');
|
||||
}
|
||||
|
||||
let fetchSubscriptionsResponse = await paystack.subscription.list({
|
||||
@@ -48,7 +43,7 @@ app.get("/subscription", async (req, res) => {
|
||||
|
||||
if (fetchSubscriptionsResponse.status === false) {
|
||||
console.log(
|
||||
"Error fetching subscriptions: ",
|
||||
'Error fetching subscriptions: ',
|
||||
fetchSubscriptionsResponse.message
|
||||
);
|
||||
return res
|
||||
@@ -60,9 +55,8 @@ app.get("/subscription", async (req, res) => {
|
||||
|
||||
let subscriptions = fetchSubscriptionsResponse.data.filter(
|
||||
(subscription) =>
|
||||
(subscription.status === "active" ||
|
||||
subscription.status === "non-renewing") &&
|
||||
plan_codes.indexOf(subscription.plan.plan_code) !== -1
|
||||
subscription.status === 'active' ||
|
||||
subscription.status === 'non-renewing'
|
||||
);
|
||||
|
||||
return res.status(200).send(subscriptions);
|
||||
@@ -72,13 +66,13 @@ app.get("/subscription", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/initialize-transaction-with-plan", async (req, res) => {
|
||||
app.post('/initialize-transaction-with-plan', async (req, res) => {
|
||||
try {
|
||||
let { email, amount, plan } = req.body;
|
||||
|
||||
if (!email || !amount || !plan) {
|
||||
throw Error(
|
||||
"Please provide a valid customer email, amount to charge, and plan code"
|
||||
'Please provide a valid customer email, amount to charge, and plan code'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,13 +80,13 @@ app.post("/initialize-transaction-with-plan", async (req, res) => {
|
||||
email,
|
||||
amount,
|
||||
plan,
|
||||
channels: ["card"],
|
||||
callback_url: "http://localhost:2426/account.html",
|
||||
channels: ['card'], // limiting the checkout to show card, as it's the only channel that subscriptions are currently available through
|
||||
callback_url: 'http://localhost:5000/account.html',
|
||||
});
|
||||
|
||||
if (initializeTransactionResponse.status === false) {
|
||||
return console.log(
|
||||
"Error initializing transaction: ",
|
||||
'Error initializing transaction: ',
|
||||
initializeTransactionResponse.message
|
||||
);
|
||||
}
|
||||
@@ -103,12 +97,12 @@ app.post("/initialize-transaction-with-plan", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/create-subscription", async (req, res) => {
|
||||
app.post('/create-subscription', async (req, res) => {
|
||||
try {
|
||||
let { customer, plan, authorization, start_date } = req.body;
|
||||
|
||||
if (!customer || !plan) {
|
||||
throw Error("Please provide a valid customer code and plan ID");
|
||||
throw Error('Please provide a valid customer code and plan ID');
|
||||
}
|
||||
|
||||
let createSubscriptionResponse = await paystack.subscription.create({
|
||||
@@ -120,7 +114,7 @@ app.post("/create-subscription", async (req, res) => {
|
||||
|
||||
if (createSubscriptionResponse.status === false) {
|
||||
return console.log(
|
||||
"Error creating subscription: ",
|
||||
'Error creating subscription: ',
|
||||
createSubscriptionResponse.message
|
||||
);
|
||||
}
|
||||
@@ -131,13 +125,13 @@ app.post("/create-subscription", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/cancel-subscription", async (req, res) => {
|
||||
app.post('/cancel-subscription', async (req, res) => {
|
||||
try {
|
||||
let { code, token } = req.body;
|
||||
|
||||
if (!code || !token) {
|
||||
throw Error(
|
||||
"Please provide a valid customer code and subscription token"
|
||||
'Please provide a valid customer code and subscription token'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,25 +140,13 @@ app.post("/cancel-subscription", async (req, res) => {
|
||||
token,
|
||||
});
|
||||
|
||||
if (disableSubscriptionResponse.status === false) {
|
||||
console.log(
|
||||
"Error disabling subscriptions: ",
|
||||
disableSubscriptionResponse.message
|
||||
);
|
||||
return res
|
||||
.status(400)
|
||||
.send(
|
||||
`Error disabling subscriptions: ${disableSubscriptionResponse.message}`
|
||||
);
|
||||
}
|
||||
|
||||
return res.send("Subscription successfully disabled");
|
||||
return res.send('Subscription successfully disabled');
|
||||
} catch (error) {
|
||||
return res.status(400).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/update-payment-method", async (req, res) => {
|
||||
app.get('/update-payment-method', async (req, res) => {
|
||||
try {
|
||||
const { subscription_code } = req.query;
|
||||
const manageSubscriptionLinkResponse =
|
||||
@@ -172,6 +154,7 @@ app.get("/update-payment-method", async (req, res) => {
|
||||
code: subscription_code,
|
||||
});
|
||||
if (manageSubscriptionLinkResponse.status === false) {
|
||||
console.log(manageSubscriptionLinkResponse.message);
|
||||
}
|
||||
|
||||
let manageSubscriptionLink = manageSubscriptionLinkResponse.data.link;
|
||||
@@ -181,12 +164,12 @@ app.get("/update-payment-method", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/create-customer", async (req, res) => {
|
||||
app.post('/create-customer', async (req, res) => {
|
||||
try {
|
||||
let { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
throw Error("Please include a valid email address");
|
||||
throw Error('Please include a valid email address');
|
||||
}
|
||||
|
||||
let createCustomerResponse = await paystack.customer.create({
|
||||
@@ -194,14 +177,15 @@ app.post("/create-customer", async (req, res) => {
|
||||
});
|
||||
|
||||
if (createCustomerResponse.status === false) {
|
||||
console.log("Error creating customer: ", createCustomerResponse.message);
|
||||
console.log('Error creating customer: ', createCustomerResponse.message);
|
||||
return res
|
||||
.status(400)
|
||||
.send(`Error creating customer: ${createCustomerResponse.message}`);
|
||||
}
|
||||
let customer = createCustomerResponse.data;
|
||||
|
||||
// This is where you would save your customer to your DB. Here, we're mocking that by just storing the customer_code in a cookie
|
||||
res.cookie("customer", customer.customer_code);
|
||||
res.cookie('customer', customer.customer_code);
|
||||
return res.status(200).send(customer);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -210,27 +194,27 @@ app.post("/create-customer", async (req, res) => {
|
||||
});
|
||||
|
||||
// Handle subscription events sent by Paystack
|
||||
app.post("/webhook", async (req, res) => {
|
||||
app.post('/webhook', async (req, res) => {
|
||||
const hash = crypto
|
||||
.createHmac("sha512", secret)
|
||||
.createHmac('sha512', secret)
|
||||
.update(JSON.stringify(req.body))
|
||||
.digest("hex");
|
||||
if (hash == req.headers["x-paystack-signature"]) {
|
||||
.digest('hex');
|
||||
if (hash == req.headers['x-paystack-signature']) {
|
||||
const webhook = req.body;
|
||||
res.status(200).send("Webhook received");
|
||||
res.status(200).send('Webhook received');
|
||||
|
||||
switch (webhook.event) {
|
||||
case "subscription.create":
|
||||
case "charge.success":
|
||||
case "invoice.update":
|
||||
case "invoice.payment_failed":
|
||||
case "subscription.not_renew":
|
||||
case "subscription.disable":
|
||||
case "subscription.expiring_cards":
|
||||
case 'subscription.create':
|
||||
case 'charge.success':
|
||||
case 'invoice.update':
|
||||
case 'invoice.payment_failed':
|
||||
case 'subscription.not_renew':
|
||||
case 'subscription.disable':
|
||||
case 'subscription.expiring_cards':
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Listening on port ${port}`);
|
||||
app.listen(process.env.PORT, () => {
|
||||
console.log(`App running at http://localhost:${process.env.PORT}`);
|
||||
});
|
||||
Generated
-1043
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@paystack/paystack-sdk": "^1.0.0-beta.6",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user