Started implementing the Paymob integration

This commit is contained in:
Tiago Ribeiro
2024-05-13 10:38:05 +01:00
parent 8b2459c304
commit f967282f71
5 changed files with 407 additions and 293 deletions

View File

@@ -47,6 +47,7 @@
"next": "13.1.6", "next": "13.1.6",
"nodemailer": "^6.9.5", "nodemailer": "^6.9.5",
"nodemailer-express-handlebars": "^6.1.0", "nodemailer-express-handlebars": "^6.1.0",
"paymob-react": "git+https://github.com/tiago-ecrop/paymob-react-oman.git",
"primeicons": "^6.0.1", "primeicons": "^6.0.1",
"primereact": "^9.2.3", "primereact": "^9.2.3",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",

View File

@@ -0,0 +1,47 @@
import {DurationUnit} from "@/interfaces/paypal";
import {User} from "@/interfaces/user";
import axios from "axios";
import {useState} from "react";
import Button from "./Low/Button";
interface Props {
user: User;
currency: string;
price: number;
title: string;
description: string;
paymentID: string;
duration: number;
duration_unit: DurationUnit;
setIsLoading: (isLoading: boolean) => void;
onSuccess: (duration: number, duration_unit: DurationUnit) => void;
}
export default function PaymobPayment({
user,
price,
currency,
title,
description,
paymentID,
duration,
duration_unit,
setIsLoading,
onSuccess,
}: Props) {
const [iframeURL, setIFrameURL] = useState<string>();
const handleCardPayment = async () => {
try {
} catch (error) {
console.error("Error starting card payment process:", error);
}
};
return (
<>
<Button onClick={handleCardPayment}>Pay</Button>
{iframeURL}
</>
);
}

View File

@@ -4,19 +4,20 @@ import PayPalPayment from "@/components/PayPalPayment";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import usePackages from "@/hooks/usePackages"; import usePackages from "@/hooks/usePackages";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import { User } from "@/interfaces/user"; import {User} from "@/interfaces/user";
import clsx from "clsx"; import clsx from "clsx";
import { capitalize } from "lodash"; import {capitalize} from "lodash";
import { useEffect, useState } from "react"; import {useEffect, useState} from "react";
import getSymbolFromCurrency from "currency-symbol-map"; import getSymbolFromCurrency from "currency-symbol-map";
import useInvites from "@/hooks/useInvites"; import useInvites from "@/hooks/useInvites";
import { BsArrowRepeat } from "react-icons/bs"; import {BsArrowRepeat} from "react-icons/bs";
import InviteCard from "@/components/Medium/InviteCard"; import InviteCard from "@/components/Medium/InviteCard";
import { useRouter } from "next/router"; import {useRouter} from "next/router";
import { PayPalScriptProvider } from "@paypal/react-paypal-js"; import {PayPalScriptProvider} from "@paypal/react-paypal-js";
import { usePaypalTracking } from "@/hooks/usePaypalTracking"; import {usePaypalTracking} from "@/hooks/usePaypalTracking";
import { ToastContainer } from "react-toastify"; import {ToastContainer} from "react-toastify";
import useDiscounts from "@/hooks/useDiscounts"; import useDiscounts from "@/hooks/useDiscounts";
import PaymobPayment from "@/components/PaymobPayment";
interface Props { interface Props {
user: User; user: User;
@@ -25,37 +26,24 @@ interface Props {
reload: () => void; reload: () => void;
} }
export default function PaymentDue({ export default function PaymentDue({user, hasExpired = false, clientID, reload}: Props) {
user,
hasExpired = false,
clientID,
reload,
}: Props) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [appliedDiscount, setAppliedDiscount] = useState(0); const [appliedDiscount, setAppliedDiscount] = useState(0);
const router = useRouter(); const router = useRouter();
const { packages } = usePackages(); const {packages} = usePackages();
const { discounts } = useDiscounts(); const {discounts} = useDiscounts();
const { users } = useUsers(); const {users} = useUsers();
const { groups } = useGroups(); const {groups} = useGroups();
const { const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
invites,
isLoading: isInvitesLoading,
reload: reloadInvites,
} = useInvites({ to: user?.id });
const trackingId = usePaypalTracking(); const trackingId = usePaypalTracking();
useEffect(() => { useEffect(() => {
const userDiscounts = discounts.filter((x) => const userDiscounts = discounts.filter((x) => user.email.endsWith(`@${x.domain}`));
user.email.endsWith(`@${x.domain}`),
);
if (userDiscounts.length === 0) return; if (userDiscounts.length === 0) return;
const biggestDiscount = [...userDiscounts] const biggestDiscount = [...userDiscounts].sort((a, b) => b.percentage - a.percentage).shift();
.sort((a, b) => b.percentage - a.percentage)
.shift();
if (!biggestDiscount) return; if (!biggestDiscount) return;
setAppliedDiscount(biggestDiscount.percentage); setAppliedDiscount(biggestDiscount.percentage);
@@ -68,9 +56,7 @@ export default function PaymentDue({
if (userGroups.length === 0) return true; if (userGroups.length === 0) return true;
const userGroupsAdminTypes = userGroups const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t);
.map((g) => users?.find((u) => u.id === g.admin)?.type)
.filter((t) => !!t);
return userGroupsAdminTypes.every((t) => t !== "corporate"); return userGroupsAdminTypes.every((t) => t !== "corporate");
}; };
@@ -81,9 +67,7 @@ export default function PaymentDue({
<div className="absolute left-0 top-0 z-[999] h-screen w-screen overflow-hidden bg-black/60"> <div className="absolute left-0 top-0 z-[999] h-screen w-screen overflow-hidden bg-black/60">
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-8 text-white"> <div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-8 text-white">
<span className={clsx("loading loading-infinity w-48")} /> <span className={clsx("loading loading-infinity w-48")} />
<span className={clsx("text-2xl font-bold")}> <span className={clsx("text-2xl font-bold")}>Completing your payment...</span>
Completing your payment...
</span>
</div> </div>
</div> </div>
)} )}
@@ -94,17 +78,9 @@ export default function PaymentDue({
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div <div
onClick={reloadInvites} onClick={reloadInvites}
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out" className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
> <span className="text-mti-black text-lg font-bold">Invites</span>
<span className="text-mti-black text-lg font-bold"> <BsArrowRepeat className={clsx("text-xl", isInvitesLoading && "animate-spin")} />
Invites
</span>
<BsArrowRepeat
className={clsx(
"text-xl",
isInvitesLoading && "animate-spin",
)}
/>
</div> </div>
</div> </div>
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll"> <span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
@@ -124,16 +100,11 @@ export default function PaymentDue({
)} )}
<div className="flex w-full flex-col items-center justify-center gap-4 text-center"> <div className="flex w-full flex-col items-center justify-center gap-4 text-center">
{hasExpired && ( {hasExpired && <span className="text-lg font-bold">You do not have time credits for your account type!</span>}
<span className="text-lg font-bold">
You do not have time credits for your account type!
</span>
)}
{isIndividual() && ( {isIndividual() && (
<div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll"> <div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll">
<span className="max-w-lg"> <span className="max-w-lg">
To add to your use of EnCoach, please purchase one of the time To add to your use of EnCoach, please purchase one of the time packages available below:
packages available below:
</span> </span>
<div className="flex w-full flex-wrap justify-center gap-8"> <div className="flex w-full flex-wrap justify-center gap-8">
<PayPalScriptProvider <PayPalScriptProvider
@@ -142,30 +113,15 @@ export default function PaymentDue({
currency: "USD", currency: "USD",
intent: "capture", intent: "capture",
commit: true, commit: true,
}} }}>
>
{packages.map((p) => ( {packages.map((p) => (
<div <div key={p.id} className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
key={p.id}
className={clsx(
"flex flex-col items-start gap-6 rounded-xl bg-white p-4",
)}
>
<div className="mb-2 flex flex-col items-start"> <div className="mb-2 flex flex-col items-start">
<img <img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
src="/logo_title.png"
alt="EnCoach's Logo"
className="w-32"
/>
<span className="text-xl font-semibold"> <span className="text-xl font-semibold">
EnCoach - {p.duration}{" "} EnCoach - {p.duration}{" "}
{capitalize( {capitalize(
p.duration === 1 p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit,
? p.duration_unit.slice(
0,
p.duration_unit.length - 1,
)
: p.duration_unit,
)} )}
</span> </span>
</div> </div>
@@ -183,15 +139,12 @@ export default function PaymentDue({
{getSymbolFromCurrency(p.currency)} {getSymbolFromCurrency(p.currency)}
</span> </span>
<span className="text-2xl text-mti-red-light"> <span className="text-2xl text-mti-red-light">
{( {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)}
p.price -
p.price * (appliedDiscount / 100)
).toFixed(2)}
{getSymbolFromCurrency(p.currency)} {getSymbolFromCurrency(p.currency)}
</span> </span>
</div> </div>
)} )}
<PayPalPayment {/* <PayPalPayment
key={clientID} key={clientID}
clientID={clientID} clientID={clientID}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
@@ -208,18 +161,29 @@ export default function PaymentDue({
p.price * (appliedDiscount / 100) p.price * (appliedDiscount / 100)
).toFixed(2) ).toFixed(2)
} }
/> */}
<PaymobPayment
key={clientID}
user={user}
description="Description"
paymentID="123"
title="Title"
setIsLoading={setIsLoading}
onSuccess={() => {
setTimeout(reload, 500);
}}
currency={p.currency}
duration={p.duration}
duration_unit={p.duration_unit}
price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)}
/> />
</div> </div>
<div className="flex flex-col items-start gap-1"> <div className="flex flex-col items-start gap-1">
<span>This includes:</span> <span>This includes:</span>
<ul className="flex flex-col items-start text-sm"> <ul className="flex flex-col items-start text-sm">
<li>- Train your abilities for the IELTS exam</li> <li>- Train your abilities for the IELTS exam</li>
<li> <li>- Gain insights into your weaknesses and strengths</li>
- Gain insights into your weaknesses and strengths <li>- Allow yourself to correctly prepare for the exam</li>
</li>
<li>
- Allow yourself to correctly prepare for the exam
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -228,36 +192,20 @@ export default function PaymentDue({
</div> </div>
</div> </div>
)} )}
{!isIndividual() && {!isIndividual() && user.type === "corporate" && user?.corporateInformation.payment && (
user.type === "corporate" &&
user?.corporateInformation.payment && (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<span className="max-w-lg"> <span className="max-w-lg">
To add to your use of EnCoach and that of your students and To add to your use of EnCoach and that of your students and teachers, please pay your designated package below:
teachers, please pay your designated package below:
</span> </span>
<div <div className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
className={clsx(
"flex flex-col items-start gap-6 rounded-xl bg-white p-4",
)}
>
<div className="mb-2 flex flex-col items-start"> <div className="mb-2 flex flex-col items-start">
<img <img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
src="/logo_title.png" <span className="text-xl font-semibold">EnCoach - {user.corporateInformation?.monthlyDuration} Months</span>
alt="EnCoach's Logo"
className="w-32"
/>
<span className="text-xl font-semibold">
EnCoach - {user.corporateInformation?.monthlyDuration}{" "}
Months
</span>
</div> </div>
<div className="flex w-full flex-col items-start gap-2"> <div className="flex w-full flex-col items-start gap-2">
<span className="text-2xl"> <span className="text-2xl">
{user.corporateInformation.payment.value} {user.corporateInformation.payment.value}
{getSymbolFromCurrency( {getSymbolFromCurrency(user.corporateInformation.payment.currency)}
user.corporateInformation.payment.currency,
)}
</span> </span>
<PayPalPayment <PayPalPayment
key={clientID} key={clientID}
@@ -279,18 +227,11 @@ export default function PaymentDue({
<span>This includes:</span> <span>This includes:</span>
<ul className="flex flex-col items-start text-sm"> <ul className="flex flex-col items-start text-sm">
<li> <li>
- Allow a total of{" "} - Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to
{ use EnCoach
user.corporateInformation.companyInformation
.userAmount
}{" "}
students and teachers to use EnCoach
</li> </li>
<li>- Train their abilities for the IELTS exam</li> <li>- Train their abilities for the IELTS exam</li>
<li> <li>- Gain insights into your students&apos; weaknesses and strengths</li>
- Gain insights into your students&apos; weaknesses
and strengths
</li>
<li>- Allow them to correctly prepare for the exam</li> <li>- Allow them to correctly prepare for the exam</li>
</ul> </ul>
</div> </div>
@@ -300,27 +241,22 @@ export default function PaymentDue({
{!isIndividual() && user.type !== "corporate" && ( {!isIndividual() && user.type !== "corporate" && (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<span className="max-w-lg"> <span className="max-w-lg">
You are not the person in charge of your time credits, please You are not the person in charge of your time credits, please contact your administrator about this situation.
contact your administrator about this situation.
</span> </span>
<span className="max-w-lg"> <span className="max-w-lg">
If you believe this to be a mistake, please contact the If you believe this to be a mistake, please contact the platform&apos;s administration, thank you for your
platform&apos;s administration, thank you for your patience. patience.
</span> </span>
</div> </div>
)} )}
{!isIndividual() && {!isIndividual() && user.type === "corporate" && !user.corporateInformation.payment && (
user.type === "corporate" &&
!user.corporateInformation.payment && (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<span className="max-w-lg"> <span className="max-w-lg">
An admin nor your agent have yet set the price intended to An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users you
your requirements in terms of the amount of users you desire desire and your expected monthly duration.
and your expected monthly duration.
</span> </span>
<span className="max-w-lg"> <span className="max-w-lg">
Please try again later or contact your agent or an admin, Please try again later or contact your agent or an admin, thank you for your patience.
thank you for your patience.
</span> </span>
</div> </div>
)} )}

101
src/pages/api/paymob.ts Normal file
View File

@@ -0,0 +1,101 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs, setDoc, doc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {Payment} from "@/interfaces/paypal";
import {v4} from "uuid";
import ShortUniqueId from "short-unique-id";
import axios from "axios";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
interface BillingData {
apartment: string;
email: string;
floor: string;
first_name: string;
street: string;
building: string;
phone_number: string;
shipping_method: string;
postal_code: string;
city: string;
country: string;
last_name: string;
state: string;
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const snapshot = await getDocs(collection(db, "payments"));
res.status(200).json(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})),
);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const body = req.body as Payment;
const shortUID = new ShortUniqueId();
await setDoc(doc(db, "payments", shortUID.randomUUID(8)), body);
res.status(200).json({ok: true});
}
const authenticatePaymob = async () => {
const response = await axios.post<{token: string}>(
"https://oman.paymob.com/api/auth/tokens",
{
api_key: process.env.PAYMOB_API_KEY,
},
{headers: {Authorization: `Bearer ${process.env.PAYMOB_SECRET_KEY}`}},
);
return response.data.token;
};
const createOrder = async (token: string) => {
const response = await axios.post<{id: number}>(
"https://oman.paymob.com/api/ecommerce/orders",
{auth_token: token, delivery_needed: "false", currency: "OMR", amount_cents: "100", items: []},
{headers: {Authorization: `Bearer ${token}`}},
);
return response.data.id;
};
const createTransactionIFrame = async (token: string, orderID: number, billingData: BillingData) => {
const response = await axios.post<{token: string}>(
"https://oman.paymob.com/api/acceptance/payment_keys",
{
auth_token: token,
amount_cents: "100",
order_id: orderID,
currency: "OMR",
expiration: 3600,
integration_id: 1540,
lock_order_when_paid: "true",
billing_data: billingData,
},
{headers: {Authorization: `Bearer ${token}`}},
);
return response.data.token;
};

View File

@@ -4870,6 +4870,13 @@ path-type@^4.0.0:
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
"paymob-react@git+https://github.com/tiago-ecrop/paymob-react-oman.git":
version "1.0.0"
resolved "git+https://github.com/tiago-ecrop/paymob-react-oman.git#9e7d1e86f01d29dd10192bbd371517849a264e5d"
dependencies:
react "^18.2.0"
react-dom "^18.2.0"
picocolors@^1.0.0: picocolors@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
@@ -5201,6 +5208,14 @@ react-dom@18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-dom@^18.2.0:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.2"
react-fast-compare@^3.0.1: react-fast-compare@^3.0.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz"
@@ -5330,6 +5345,13 @@ react@18.2.0:
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
react@^18.2.0:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
dependencies:
loose-envify "^1.1.0"
read-cache@^1.0.0: read-cache@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz"
@@ -5535,6 +5557,13 @@ scheduler@^0.23.0:
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler@^0.23.2:
version "0.23.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
dependencies:
loose-envify "^1.1.0"
seedrandom@^3.0.5: seedrandom@^3.0.5:
version "3.0.5" version "3.0.5"
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"