initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
node_modules/
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Account</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Lato:300,300italic,700,700italic"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link rel="stylesheet" href="./css/common.css" />
|
||||
<link rel="stylesheet" href="./css/account.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="account-dashboard" class="container">
|
||||
<h1>Account Dashboard</h1>
|
||||
<div id="subscription-info"></div>
|
||||
<div id="select-plan" style="display: flex; flex-direction: row"></div>
|
||||
</div>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<script src="account.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,89 @@
|
||||
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}`
|
||||
).then((res) => res.json());
|
||||
let subscription = subscriptions[0];
|
||||
|
||||
if (subscription) {
|
||||
const subscriptionInfoDiv = document.getElementById("subscription-info");
|
||||
subscriptionInfoDiv.innerHTML = `
|
||||
<hr>
|
||||
<p>Hi ${customer_email}<p>
|
||||
<p>You're currently on the ${subscription.plan.name} plan</p>
|
||||
<p>
|
||||
Status: ${subscription.status}
|
||||
</p>
|
||||
<p>
|
||||
Card on file: ${subscription.authorization.brand} card ending in ${
|
||||
subscription.authorization.last4
|
||||
} expires on ${subscription.authorization.exp_month}/${
|
||||
subscription.authorization.exp_year
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
Next payment date: ${new Date(subscription.next_payment_date)}
|
||||
</p>
|
||||
|
||||
<a href="/update-payment-method?subscription_code=${
|
||||
subscription.subscription_code
|
||||
}" target="_blank"> Manage subscription </a><br />
|
||||
`;
|
||||
} else {
|
||||
const plans = await fetch("/plans", {
|
||||
method: "get",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((error) => console.log(error));
|
||||
|
||||
let accountDashDiv = document.getElementById("account-dashboard");
|
||||
accountDashDiv.innerHTML +=
|
||||
"<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";
|
||||
|
||||
plans.forEach((plan) => {
|
||||
let planDiv = document.createElement("div");
|
||||
planDiv.innerHTML = `
|
||||
<div class="card" style="width: 18rem; margin: 1rem; text-align: center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${plan.name}</h5>
|
||||
<p class="card-subtitle mb-2 text-muted">${plan.currency} ${
|
||||
plan.amount / 100
|
||||
}/month</p>
|
||||
<p class="card-text">${plan.description}</p>
|
||||
<button class="btn btn-primary" style="width: 10rem; text-align: center" onclick="signUpForPlan('${
|
||||
plan.plan_code
|
||||
}')">Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
selectPlanDiv.append(planDiv);
|
||||
});
|
||||
accountDashDiv.append(selectPlanDiv);
|
||||
}
|
||||
});
|
||||
|
||||
async function signUpForPlan(plan_code) {
|
||||
let email = window.localStorage.getItem("customer_email");
|
||||
let { authorization_url } = await fetch("/initialize-transaction-with-plan", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
amount: 50000,
|
||||
plan: plan_code,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((error) => console.log(error));
|
||||
|
||||
window.location.href = authorization_url;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.container {
|
||||
width: auto;
|
||||
max-width: 680px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #00c3f7;
|
||||
border: none;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #00c2f7c1;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
body {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
#signin-form {
|
||||
width: 100%;
|
||||
max-width: 330px;
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#signin-form .form-control {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#signin-form input[type="email"] {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Lato:300,300italic,700,700italic"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link href="./css/common.css" rel="stylesheet" />
|
||||
<link href="./css/login.css" rel="stylesheet" />
|
||||
<title>Sign In</title>
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div class="container">
|
||||
<div class="container-sm">
|
||||
<h1 class="h3 mb-3 font-weight-normal">Log in</h1>
|
||||
<p class="mb-3">
|
||||
A simple demo on collecting subscription payments using Paystack
|
||||
</p>
|
||||
<form id="signin-form">
|
||||
<div class="mb-3">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
aria-describedby="emailHelp"
|
||||
placeholder="Email Address"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<script src="login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
const form = document.getElementById("signin-form");
|
||||
if (form) {
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById("email").value;
|
||||
|
||||
const customer = await fetch("/create-customer", {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((error) => console.log(error));
|
||||
window.localStorage.setItem("customer_email", email);
|
||||
window.localStorage.setItem("customer_code", customer.customer_code);
|
||||
window.localStorage.setItem("customer_id", customer.id);
|
||||
|
||||
window.location.href = "/account.html";
|
||||
});
|
||||
}
|
||||
});
|
||||
Generated
+1684
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "subscriptions-example",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@paystack/paystack-sdk": "^1.0.0-beta.6",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.19"
|
||||
}
|
||||
}
|
||||
Generated
+1043
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
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");
|
||||
|
||||
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");
|
||||
res.sendFile(path);
|
||||
});
|
||||
|
||||
app.get("/plans", async (req, res) => {
|
||||
let fetchPlansResponse = await paystack.plan.list({});
|
||||
|
||||
if (fetchPlansResponse.status === false) {
|
||||
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);
|
||||
});
|
||||
|
||||
app.get("/subscription", async (req, res) => {
|
||||
try {
|
||||
let { customer } = req.query;
|
||||
|
||||
if (!customer) {
|
||||
throw Error("Please include a valid customer ID");
|
||||
}
|
||||
|
||||
let fetchSubscriptionsResponse = await paystack.subscription.list({
|
||||
customer,
|
||||
});
|
||||
|
||||
if (fetchSubscriptionsResponse.status === false) {
|
||||
console.log(
|
||||
"Error fetching subscriptions: ",
|
||||
fetchSubscriptionsResponse.message
|
||||
);
|
||||
return res
|
||||
.status(400)
|
||||
.send(
|
||||
`Error fetching subscriptions: ${fetchSubscriptionsResponse.message}`
|
||||
);
|
||||
}
|
||||
|
||||
let subscriptions = fetchSubscriptionsResponse.data.filter(
|
||||
(subscription) =>
|
||||
(subscription.status === "active" ||
|
||||
subscription.status === "non-renewing") &&
|
||||
plan_codes.indexOf(subscription.plan.plan_code) !== -1
|
||||
);
|
||||
|
||||
return res.status(200).send(subscriptions);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return res.status(400).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
let initializeTransactionResponse = await paystack.transaction.initialize({
|
||||
email,
|
||||
amount,
|
||||
plan,
|
||||
channels: ["card"],
|
||||
callback_url: "http://localhost:2426/account.html",
|
||||
});
|
||||
|
||||
if (initializeTransactionResponse.status === false) {
|
||||
return console.log(
|
||||
"Error initializing transaction: ",
|
||||
initializeTransactionResponse.message
|
||||
);
|
||||
}
|
||||
let transaction = initializeTransactionResponse.data;
|
||||
return res.status(200).send(transaction);
|
||||
} catch (error) {
|
||||
return res.status(400).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
let createSubscriptionResponse = await paystack.subscription.create({
|
||||
customer,
|
||||
plan,
|
||||
authorization,
|
||||
start_date,
|
||||
});
|
||||
|
||||
if (createSubscriptionResponse.status === false) {
|
||||
return console.log(
|
||||
"Error creating subscription: ",
|
||||
createSubscriptionResponse.message
|
||||
);
|
||||
}
|
||||
let subscription = createSubscriptionResponse.data;
|
||||
return res.status(200).send(subscription);
|
||||
} catch (error) {
|
||||
return res.status(400).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
let disableSubscriptionResponse = await paystack.subscription.disable({
|
||||
code,
|
||||
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");
|
||||
} catch (error) {
|
||||
return res.status(400).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/update-payment-method", async (req, res) => {
|
||||
try {
|
||||
const { subscription_code } = req.query;
|
||||
const manageSubscriptionLinkResponse =
|
||||
await paystack.subscription.manageLink({
|
||||
code: subscription_code,
|
||||
});
|
||||
if (manageSubscriptionLinkResponse.status === false) {
|
||||
}
|
||||
|
||||
let manageSubscriptionLink = manageSubscriptionLinkResponse.data.link;
|
||||
return res.redirect(manageSubscriptionLink);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/create-customer", async (req, res) => {
|
||||
try {
|
||||
let { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
throw Error("Please include a valid email address");
|
||||
}
|
||||
|
||||
let createCustomerResponse = await paystack.customer.create({
|
||||
email,
|
||||
});
|
||||
|
||||
if (createCustomerResponse.status === false) {
|
||||
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);
|
||||
return res.status(200).send(customer);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return res.status(400).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle subscription events sent by Paystack
|
||||
app.post("/webhook", async (req, res) => {
|
||||
const hash = crypto
|
||||
.createHmac("sha512", secret)
|
||||
.update(JSON.stringify(req.body))
|
||||
.digest("hex");
|
||||
if (hash == req.headers["x-paystack-signature"]) {
|
||||
const webhook = req.body;
|
||||
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":
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Listening on port ${port}`);
|
||||
});
|
||||
Reference in New Issue
Block a user