add readme

This commit is contained in:
tolu-paystack
2023-03-09 14:22:54 +01:00
parent f555c197ff
commit 6f53b7d3df
9 changed files with 154 additions and 2231 deletions
+3
View File
@@ -0,0 +1,3 @@
STATIC_DIR='./client'
PAYSTACK_SECRET_KEY=
PORT='5000'
+21
View File
@@ -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.
+51
View File
@@ -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
View File
@@ -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,
+7 -1084
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -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"
}
}
+47 -63
View File
@@ -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}`);
});
-1043
View File
File diff suppressed because it is too large Load Diff
-19
View File
@@ -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"
}
}