#
Step 6 - Monetize your application
Available in beta only
This is available in beta for now. Be aware that some performance issues might occur.
This is a guide to help you to understand how you can monetize your quable application.
#
Monetize your application
In this tutorial we'll guide you on how to monetize with your app! Whether you're considering a single-purchase approach or a recurring revenue, this guide will show you how to effectively implement these monetization methods.
#
Requirements
We'll use the quable-partner-js package for handling financial operations :
yarn add @quable/quable-partner-js
or
npm install @quable/quable-partner-js
We'll also use the uuid package to generate unique ids :
yarn add uuid
yarn add -D @types/uuid
or
npm install uuid
npm install -D @types/uuid
#
Quable Partner JS
The payment module of the Quable Partner JS package provides the necessary tools to manage financial operations. This includes generating payment links and retrieving payment information, giving us control over financial transactions.
#
Setup billing configuration
#
Create billing data structure
Add theses new types interfaces in helper/types.ts
:
interface ProductPriceInput {
name: string;
description?: string;
images?: string[];
}
interface PriceDataInput {
product: ProductPriceInput;
amount: number;
currency: string;
}
export interface PriceInput {
priceId?: string;
priceData?: PriceDataInput;
}
export interface BillingConfig {
quantity: number;
appId: string;
price: PriceInput;
isTest?: boolean;
successUrl?: string;
interval?: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
trialPeriod?: number;
}
#
Create billing config file
Create a directory in the helper
folder named billing
. Add insid configs.ts
with the following code:
import { BillingConfig } from "../types";
export const appId = process.env.QUABLE_PARTNER_APP_ID ?? "quable-app-template";
export const billingConfigs: BillingConfig[] = [
{
appId,
price: {
priceData: {
product: {
name: "app-pay-monthly",
description: "Application pay subscription test",
images: [],
},
amount: 15,
currency: "EUR",
},
},
quantity: 1,
isTest: true,
interval: "MONTHLY",
},
{
appId,
price: {
priceData: {
product: {
name: "app-pay-onetime",
description: "Application pay onetime payment test",
images: [],
},
amount: 1500,
currency: "EUR",
},
},
quantity: 1,
isTest: true,
},
];
With this code, we define two payment options: one for a monthly subscription called app-pay-monthly
priced at 15 euros per month, and another for a one-time payment option named app-pay-onetime
priced at 1500 euros. Both options share the same application ID for identification purpose.
#
Display billing price
#
Create billing controller
We're now creating a controller to manage incoming requests.
Create a file named billing.controller.ts
in the controllers
folder and paste the given code:
import { Request, Response } from "express";
class BillingController {
public index = async (_req: Request, res: Response) => {
return res.render("pages/billing");
};
}
export const billingController = new BillingController();
#
Create billing routes
Create a new file named billing.routes.ts
in the routes
directory and paste the following code:
import { billingController } from "src/controllers/billing.controller";
import { Router } from "express";
const billingRouter = Router();
billingRouter.get("/", billingController.index);
export default billingRouter;
After defining the routes, you must register the route in index.ts
.
import billingRouter from "./routes/billing.routes";
/*
... code
*/
app.use("/:customer/billing", billingRouter);
#
Create billing view
With the backend now configured, let's create a view to bill our user. Create a file called index.ejs
inside a new folder views/pages/billing
and insert the given code:
<div id="billing-page">
<h5>Choose your Pricing plan</h5>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<div class="plan-box card">
<div class="p-4 card-body price-item">
<h5>Pay Monthly</h5>
<div class="py-4">
<h2>
<sup>
<small>€</small>
</sup>
15 / <span class="font-size-13">Month</span>
</h2>
</div>
<div class="text-center plan-btn">
<button
class="btn btn-primary btn-sm waves-effect waves-light"
priceid="app-pay-monthly"
>
Subscribe
</button>
</div>
</div>
<div class="p-4 card-body price-item">
<h5>Onetime Payment</h5>
<div class="py-4">
<h2>
<sup>
<small>€</small>
</sup>
1500/ <span class="font-size-13">Live time</span>
</h2>
</div>
<div class="text-center plan-btn">
<button
class="btn btn-primary btn-sm waves-effect waves-light"
priceid="app-pay-onetime"
>
Subscribe
</button>
</div>
</div>
</div>
</div>
</div>
</div>
Note
The priceId attribute on the button should match the name of the price defined in the billing configuration file. This ensures that the correct pricing plan is selected when the user clicks the button.
Finally, update the index.ejs
template with a link to the document page. You should also include the authenticated customer variable the URL.
<div class="center">
<img
src="/images/quable-logo.png"
alt="quable_logo"
height="100"
class="mb-2"
/>
<h1 class="mb-4 text-bold">Quable App Template</h1>
<a class="mx-1" href="<%- customer %>/translation">Translation</a>
<a class="mx-1" href="<%- customer %>/document">Document</a>
<a class="mx-1" href="<%- customer %>/billing">Billing</a>
</div>
You should now see the following result
#
Create a payment
#
Create billing utilities
For the rest of the tutorial we'll be using these utility functions to keep our code more organized.
Create a file in the billing
folder named utils.ts
and paste the following code:
import {
QuablePartnerAPI,
SubscriptionPaymentModel,
OneTimePaymentModel,
} from "@quable/quable-partner-js";
import { BillingConfig, PriceInput } from "../types";
import { v4 as uuidv4 } from "uuid";
export const validateOneTimePayment = async (
quableInstanceName: string,
billingConfig: BillingConfig,
appId: string
) => {
const partnerClient = new QuablePartnerAPI({
accessToken: process.env.QUABLE_PARTNER_SECRET || "",
partnerId: process.env.QUABLE_PARTNER_ID || "",
});
let hasPayment = false;
const payments = await partnerClient.Payment.OneTimePayment.getPayments({
customer: quableInstanceName,
appId,
status: "paid",
});
if (payments.length > 0) {
for (const payment of payments) {
hasPayment = verifyPayment(payment, billingConfig);
if (hasPayment) {
break;
}
}
}
return hasPayment;
};
export const validateSubscriptionPayment = async (
quableInstanceName: string,
billingConfigs: BillingConfig[],
appId: string
) => {
const partnerClient = new QuablePartnerAPI({
accessToken: process.env.QUABLE_PARTNER_SECRET!,
partnerId: process.env.QUABLE_PARTNER_ID!,
});
let hasPayment = false;
let paymentUptoDate = false;
let payments: SubscriptionPaymentModel[];
try {
payments = await partnerClient.Payment.SubscriptionPayment.getPayments({
customer: quableInstanceName,
appId,
});
} catch (error) {
payments = [];
}
if (payments.length > 0) {
for (const billingConfig of billingConfigs) {
hasPayment = verifyPayment(payments[0], billingConfig);
if (hasPayment) {
paymentUptoDate = isDateInIntervalType(
new Date(payments[0].createdAt),
billingConfig?.interval
);
break;
}
}
}
if (!hasPayment || !paymentUptoDate) {
return false;
}
return true;
};
const isDateInIntervalType = (dateToCheck: Date, intervalType?: string) => {
const currentDate = new Date();
switch (intervalType) {
case "DAILY":
return dateToCheck.getDate() === currentDate.getDate();
case "WEEKLY":
return (
dateToCheck.getFullYear() === currentDate.getFullYear() &&
dateToCheck.getMonth() === currentDate.getMonth() &&
dateToCheck.getDate() >= currentDate.getDate() - currentDate.getDay() &&
dateToCheck.getDate() <=
currentDate.getDate() + (6 - currentDate.getDay())
);
case "MONTHLY":
return (
dateToCheck.getFullYear() === currentDate.getFullYear() &&
dateToCheck.getMonth() === currentDate.getMonth()
);
case "YEARLY":
return dateToCheck.getFullYear() === currentDate.getFullYear();
default:
return false;
}
};
export const formatPriceId = (price: PriceInput) => {
if (price.priceId) {
return price.priceId;
} else if (price.priceData && price.priceData?.product?.name) {
return price.priceData.product.name;
} else {
throw new Error("Price id not found");
}
};
export const verifyPayment = (
payment: OneTimePaymentModel | SubscriptionPaymentModel,
billingConfig: BillingConfig
) => {
return formatPriceId(payment.price) === formatPriceId(billingConfig.price);
};
export const clientHasPayment = async (
quableInstanceName: string,
billingConfigs: BillingConfig[],
appId: string
) => {
let hasOneTimePayment = false;
let hasSubscriptionPayment = false;
try {
if (billingConfigs && billingConfigs.length > 0) {
const subscriptionBillings = billingConfigs.filter((el) => el.interval);
const onetimeBillings = billingConfigs.filter((el) => !el.interval);
if (subscriptionBillings.length > 0) {
hasSubscriptionPayment = await validateSubscriptionPayment(
quableInstanceName,
subscriptionBillings,
appId
);
}
if (onetimeBillings.length > 0) {
hasOneTimePayment = await validateOneTimePayment(
quableInstanceName,
onetimeBillings[0],
appId
);
}
if (!hasSubscriptionPayment && !hasOneTimePayment) {
return false;
}
}
return true;
} catch (error) {
return false;
}
};
export const generateUUID = () => {
return uuidv4();
};
validateOneTimePayment
: This function takes a Quable instance name, a billing configuration object, and an app ID as parameters. It checks if the customer has made a one-time payment for the app that matches the billing configuration. It returns true if a matching payment is found, and false otherwise.validateSubscriptionPayment
: This function takes a Quable instance name, an array of billing configuration objects, and an app ID as parameters. It checks if the customer has an active subscription for the app that matches one of the billing configurations. It returns true if a matching subscription is found, and false otherwise.isDateInIntervalType
: This function takes a date and an interval type as parameters and returns true if the date falls within the specified interval type, and false otherwise. The supported interval types are "DAILY", "WEEKLY", "MONTHLY", and "YEARLY".formatPriceId
: This function takes a price object as a parameter and returns the price ID. If the price object has a priceId property, it returns the priceId. Otherwise, it tries to extract the price ID from the price data. If no price ID can be found, it throws an error.verifyPayment
: This function takes a payment object and a billing configuration object as parameters and returns true if the payment matches the billing configuration, and false otherwise. It checks if the price ID of the payment matches the price ID of the billing configuration.clientHasPayment
: This function takes a Quable instance name, an array of billing configuration objects, and an app ID as parameters. It checks if the customer has made a one-time payment or has an active subscription for the app that matches one of the billing configurations. It returns true if a matching payment or subscription is found, and false otherwise.generateUUID
: This function generates a UUID using the uuid package.
#
Billing service
Let's create a service to manage billing-related tasks. Create a file named billing.service.ts
within the services
directory and paste the provided code:
import { QuablePartnerAPI } from "@quable/quable-partner-js";
import { appId, billingConfigs } from "src/helper/billing/configs";
import {
formatPriceId,
clientHasPayment,
generateUUID,
} from "src/helper/billing/utils";
class BillingService {
private partnerClient = new QuablePartnerAPI({
partnerId: process.env.QUABLE_PARTNER_ID!,
accessToken: process.env.QUABLE_PARTNER_SECRET!,
});
public generateCheckoutLink = async (
instanceName: string,
priceId: string
) => {
const response: any = {
statusCode: 201,
message: "OK",
data: null,
};
try {
if (billingConfigs.length === 0) {
response.data = { url: null };
return response;
}
const validPrices = billingConfigs.filter((el) => {
return formatPriceId(el.price) === priceId;
});
if (validPrices.length < 1) {
response.message = "Price not found";
response.statusCode = 404;
return response;
}
const hasPayment = await clientHasPayment(
instanceName,
validPrices,
appId
);
if (hasPayment) {
response.data = { url: null };
return response;
}
let checkoutUrl = "";
const billingConfig = validPrices[0];
const checkoutData: any = {
appCheckoutId: generateUUID(),
customer: instanceName,
...billingConfig,
};
if (billingConfig.interval) {
const checkout =
await this.partnerClient.Payment.SubscriptionPayment.generatePaymentLink(
checkoutData
);
checkoutUrl = checkout.url;
} else {
const checkout =
await this.partnerClient.Payment.OneTimePayment.generatePaymentLink(
checkoutData
);
checkoutUrl = checkout.url;
}
response.data = { url: checkoutUrl };
} catch (error) {
response.statusCode = 500;
response.message = "Something went wrong, please try again later";
}
return response;
};
}
export const billingService = new BillingService();
generateCheckoutLink
method generates a checkout link for a specified price ID and instance name. It first checks if there are any matching billing configurations and if the customer has not already made a payment. If both conditions are met, it generates a checkout link.
#
Update billing controller
We're now going to update our billing.controller.ts
and add the following method:
import { billingService } from "src/services/billing.service";
import { Request, Response } from "express";
class BillingController {
/*
... code
*/
public generateCheckoutLink = async (req: any, res: Response) => {
const response = await billingService.generateCheckoutLink(
req.quableInstance.name,
req.params.id
);
return res.status(response.statusCode).send(response);
};
}
export const billingController = new BillingController();
#
Update billing routes
Update the billing.routes.ts
file with the following routes:
billingRouter.post("/pricing/:id", billingController.generateCheckoutLink);
The router defines route for generating a checkout link.
#
Payment link generation with Javascript
We'll now add the ability to respond to button click events and send a request to generate the payment link. Let's create inside public/scripts
folder, a new file called billing.js
and paste the provided code below:
import { createLoader } from "./ui/loader.js";
import { customFetch } from "./utils/custom-fetch.js";
const toast = new Notyf({ position: { x: "center", y: "bottom" } });
document.addEventListener("DOMContentLoaded", () => {
const billingPageContainer = document.querySelector("#billing-page");
if (billingPageContainer) {
const priceItems = billingPageContainer.querySelectorAll(".price-item");
handleItemClick(priceItems);
}
});
const handleItemClick = (priceItems) => {
for (const priceItem of priceItems) {
const payBtn = priceItem.querySelector("button");
payBtn.addEventListener("click", async (event) => {
event.preventDefault();
const priceId = event.target.getAttribute("priceId");
const loader = createLoader();
event.target.replaceWith(loader);
const url = `billing/pricing/${priceId}`;
const { data, error } = await customFetch(url, { method: "POST" });
if (error) {
toast.error(error);
loader.replaceWith(event.target);
return;
}
if (data.url) {
window.location.href = data.url;
} else {
loader.replaceWith(event.target);
toast.success("You have already subscribed");
}
});
}
};
The provided code add click event listeners on price items. When a price item is clicked, it sends a request to the backend to generate payment link. If they haven't already paid, it takes them to the checkout page to pay for the plan, otherwise it lets them know with a message.
#
Update billing view
We'll now update the view to include our script so that the buttons can respond to click events. Update the billing/index.ejs
and add the following code:
<script type="module" defer src="/scripts/billing.js"></script>
Congratulation ! You can now make a payment for a product and collect the money directly from your stripe account.
#
Check customer payment
In this section we'll add a method to check the status of a customer's payment.
#
Update billing service
Update our billing.service.ts
with the following method:
import { formatPriceId, clientHasPayment } from 'src/helper/billing/utils';
/*
.. code ..
*/
public checkPaymentStatus = async (instanceName: string, priceId: string) => {
const response: any = {
statusCode: 200,
message: 'OK',
data: null,
};
try {
if (billingConfigs.length === 0) {
response.data = true;
return response;
}
const validPrices = billingConfigs.filter((el) => {
return formatPriceId(el.price) === priceId;
});
if (validPrices.length < 1) {
response.message = 'Price not found';
response.statusCode = 404;
return response;
}
const hasPayment = await clientHasPayment(
instanceName,
validPrices,
appId,
);
response.data = hasPayment;
} catch (error) {
response.statusCode = 500;
response.message = 'Something went wrong, please try again later';
}
return response;
};
The checkPaymentStatus
function checks if a user has an active payment for a specified pricing plan by first determining the existence of billing configurations. If no configurations are found, it assumes active payment. Otherwise, it filters these configurations for the specified plan, returning an error if none match, or use clientHasPayment
to verify active payment for matching plans.
#
Update billing controller
Update billing.controller.ts
with the following method :
public checkPaymentStatus = async (req: any, res: Response) => {
const response = await billingService.checkPaymentStatus(
req.quableInstance.name,
req.params.id
);
return res.status(response.statusCode).send(response);
};
#
Update billing routes
Update the billing.routes.ts
file with the following routes:
billingRouter.get("/pricing/:id/status", billingController.checkPaymentStatus);
#
Update billing view
We'll now update billing.ejs
as follows so that all buttons are disabled by default until their payment status is checked:
<div id="billing-page">
<h5>Choose your Pricing plan</h5>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<div class="plan-box card">
<div class="p-4 card-body price-item">
<h5>Pay Monthly</h5>
<div class="py-4">
<h2>
<sup><small>€</small></sup> 15 /
<span class="font-size-13">Month</span>
</h2>
</div>
<div class="text-center plan-btn">
<button
class="btn btn-primary btn-sm waves-effect waves-light"
priceid="app-pay-monthly"
disabled
>
<i
style="margin-right: -4px;"
class="bx bx-loader-alt bx-spin"
></i>
</button>
</div>
</div>
<div class="p-4 card-body price-item">
<h5>Onetime Payment</h5>
<div class="py-4">
<h2>
<sup><small>€</small></sup> 1500/
<span class="font-size-13">Live time</span>
</h2>
</div>
<div class="text-center plan-btn">
<button
class="btn btn-primary btn-sm waves-effect waves-light"
priceid="app-pay-onetime"
disabled
>
<i
style="margin-right: -4px;"
class="bx bx-loader-alt bx-spin"
></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="module" defer src="/scripts/billing.js"></script>
#
Updating billing script
Let's update our js script to disable/enable the subscribe button based on the payment status as follows :
document.addEventListener("DOMContentLoaded", () => {
const billingPageContainer = document.querySelector("#billing-page");
if (billingPageContainer) {
const priceItems = billingPageContainer.querySelectorAll(".price-item");
handleItemClick(priceItems);
checkPaymentStatus(priceItems);
}
});
/*
... code
*/
const checkPaymentStatus = async (priceItems) => {
for (const priceItem of priceItems) {
const payBtn = priceItem.querySelector("button");
const priceId = payBtn.getAttribute("priceId");
const loader = payBtn.querySelector("i");
const url = `billing/pricing/${priceId}/status`;
const { data: result, error } = await customFetch(url, { method: "GET" });
if (error) {
toast.error(error);
payBtn.disabled = false;
payBtn.textContent = "Subscribe";
loader.remove();
return;
}
loader.remove();
if (result === true || result.data === true) {
payBtn.textContent = "Subscribed";
} else {
payBtn.disabled = false;
payBtn.textContent = "Subscribe";
}
}
};
If you have already paid for a pricing plan the button should now be disabled.
#
Billing middleware
In this section, we'll set up a billing middleware to block access to translation routes and authorize only customers with valid payments to access translation resources.
#
Create billing middleware
Create a new file named billing.middleware.ts
and paste the following the code :
import { NextFunction, Response } from "express";
import { appId, billingConfigs } from "src/helper/billing/configs";
import { clientHasPayment, formatPriceId } from "src/helper/billing/utils";
export const billingMiddleware = async (
req: any,
res: Response,
next: NextFunction
) => {
try {
const validPrice = billingConfigs.filter((el) => {
return formatPriceId(el.price) === "app-pay-onetime";
});
const hasPayment = await clientHasPayment(req.customer, validPrice, appId);
if (!hasPayment) {
throw null;
}
next();
} catch (error) {
res.redirect(`/${req.customer}/billing`);
}
};
This middleware, checks if a user has paid for app-pay-onetime
before they can proceed to the next step. If the payment is not valid, users are redirected to the billing page.
In our middleware, we app-pay-onetime
because we already paid for app-pay-monthly
. So on your side, if you want to make sure your middleware works properly, replace your app-pay-onetime
by the product name you want to check. You can also use all products if you want your customer to have access to the translation if they have app-pay-monthly
or app-pay-onetime
payments.
#
Update translation routes
Open the index.ts
file and modify the translation route to use our middleware as follows :
app.use("/:customer/translation", billingMiddleware, translationRouter);
This ensures that any request to access translation resource will pass through our middleware which will then verify the customer translation payment status. If you try to access translation page again you should now be redirected back to billing if you haven't made any payment for translation.
Done ✅
In this tutorial we learned how to monetize a Quable application using one-time and subscription payments methods, setting up billing configurations, handling payment generation, and checking payment status. We also saw how you can restrict access to paid resources and how to bill customers with a minimal frontend.