diff --git a/package.json b/package.json index 6a4285a8..5e91f501 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "next": "13.1.6", "nodemailer": "^6.9.5", "nodemailer-express-handlebars": "^6.1.0", + "paymob-react": "git+https://github.com/tiago-ecrop/paymob-react-oman.git", "primeicons": "^6.0.1", "primereact": "^9.2.3", "qrcode": "^1.5.3", @@ -98,4 +99,4 @@ "tailwindcss": "^3.2.4", "types/": "paypal/react-paypal-js" } -} +} \ No newline at end of file diff --git a/src/components/PaymobPayment.tsx b/src/components/PaymobPayment.tsx new file mode 100644 index 00000000..fe27543f --- /dev/null +++ b/src/components/PaymobPayment.tsx @@ -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(); + + const handleCardPayment = async () => { + try { + } catch (error) { + console.error("Error starting card payment process:", error); + } + }; + + return ( + <> + + {iframeURL} + + ); +} diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index 5809653b..3cb5b680 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -4,194 +4,147 @@ import PayPalPayment from "@/components/PayPalPayment"; import useGroups from "@/hooks/useGroups"; import usePackages from "@/hooks/usePackages"; import useUsers from "@/hooks/useUsers"; -import { User } from "@/interfaces/user"; +import {User} from "@/interfaces/user"; import clsx from "clsx"; -import { capitalize } from "lodash"; -import { useEffect, useState } from "react"; +import {capitalize} from "lodash"; +import {useEffect, useState} from "react"; import getSymbolFromCurrency from "currency-symbol-map"; import useInvites from "@/hooks/useInvites"; -import { BsArrowRepeat } from "react-icons/bs"; +import {BsArrowRepeat} from "react-icons/bs"; import InviteCard from "@/components/Medium/InviteCard"; -import { useRouter } from "next/router"; -import { PayPalScriptProvider } from "@paypal/react-paypal-js"; -import { usePaypalTracking } from "@/hooks/usePaypalTracking"; -import { ToastContainer } from "react-toastify"; +import {useRouter} from "next/router"; +import {PayPalScriptProvider} from "@paypal/react-paypal-js"; +import {usePaypalTracking} from "@/hooks/usePaypalTracking"; +import {ToastContainer} from "react-toastify"; import useDiscounts from "@/hooks/useDiscounts"; +import PaymobPayment from "@/components/PaymobPayment"; interface Props { - user: User; - hasExpired?: boolean; - clientID: string; - reload: () => void; + user: User; + hasExpired?: boolean; + clientID: string; + reload: () => void; } -export default function PaymentDue({ - user, - hasExpired = false, - clientID, - reload, -}: Props) { - const [isLoading, setIsLoading] = useState(false); - const [appliedDiscount, setAppliedDiscount] = useState(0); +export default function PaymentDue({user, hasExpired = false, clientID, reload}: Props) { + const [isLoading, setIsLoading] = useState(false); + const [appliedDiscount, setAppliedDiscount] = useState(0); - const router = useRouter(); + const router = useRouter(); - const { packages } = usePackages(); - const { discounts } = useDiscounts(); - const { users } = useUsers(); - const { groups } = useGroups(); - const { - invites, - isLoading: isInvitesLoading, - reload: reloadInvites, - } = useInvites({ to: user?.id }); - const trackingId = usePaypalTracking(); + const {packages} = usePackages(); + const {discounts} = useDiscounts(); + const {users} = useUsers(); + const {groups} = useGroups(); + const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id}); + const trackingId = usePaypalTracking(); - useEffect(() => { - const userDiscounts = discounts.filter((x) => - user.email.endsWith(`@${x.domain}`), - ); - if (userDiscounts.length === 0) return; + useEffect(() => { + const userDiscounts = discounts.filter((x) => user.email.endsWith(`@${x.domain}`)); + if (userDiscounts.length === 0) return; - const biggestDiscount = [...userDiscounts] - .sort((a, b) => b.percentage - a.percentage) - .shift(); - if (!biggestDiscount) return; + const biggestDiscount = [...userDiscounts].sort((a, b) => b.percentage - a.percentage).shift(); + if (!biggestDiscount) return; - setAppliedDiscount(biggestDiscount.percentage); - }, [discounts, user]); + setAppliedDiscount(biggestDiscount.percentage); + }, [discounts, user]); - const isIndividual = () => { - if (user?.type === "developer") return true; - if (user?.type !== "student") return false; - const userGroups = groups.filter((g) => g.participants.includes(user?.id)); + const isIndividual = () => { + if (user?.type === "developer") return true; + if (user?.type !== "student") return false; + const userGroups = groups.filter((g) => g.participants.includes(user?.id)); - if (userGroups.length === 0) return true; + if (userGroups.length === 0) return true; - const userGroupsAdminTypes = userGroups - .map((g) => users?.find((u) => u.id === g.admin)?.type) - .filter((t) => !!t); - return userGroupsAdminTypes.every((t) => t !== "corporate"); - }; + const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t); + return userGroupsAdminTypes.every((t) => t !== "corporate"); + }; - return ( - <> - - {isLoading && ( -
-
- - - Completing your payment... - -
-
- )} - {user ? ( - - {invites.length > 0 && ( -
-
-
- - Invites - - -
-
- - {invites.map((invite) => ( - { - reloadInvites(); - router.reload(); - }} - /> - ))} - -
- )} + return ( + <> + + {isLoading && ( +
+
+ + Completing your payment... +
+
+ )} + {user ? ( + + {invites.length > 0 && ( +
+
+
+ Invites + +
+
+ + {invites.map((invite) => ( + { + reloadInvites(); + router.reload(); + }} + /> + ))} + +
+ )} -
- {hasExpired && ( - - You do not have time credits for your account type! - - )} - {isIndividual() && ( -
- - To add to your use of EnCoach, please purchase one of the time - packages available below: - -
- - {packages.map((p) => ( -
-
- EnCoach's Logo - - EnCoach - {p.duration}{" "} - {capitalize( - p.duration === 1 - ? p.duration_unit.slice( - 0, - p.duration_unit.length - 1, - ) - : p.duration_unit, - )} - -
-
- {!appliedDiscount && ( - - {p.price} - {getSymbolFromCurrency(p.currency)} - - )} - {appliedDiscount && ( -
- - {p.price} - {getSymbolFromCurrency(p.currency)} - - - {( - p.price - - p.price * (appliedDiscount / 100) - ).toFixed(2)} - {getSymbolFromCurrency(p.currency)} - -
- )} - + {hasExpired && You do not have time credits for your account type!} + {isIndividual() && ( +
+ + To add to your use of EnCoach, please purchase one of the time packages available below: + +
+ + {packages.map((p) => ( +
+
+ EnCoach's Logo + + EnCoach - {p.duration}{" "} + {capitalize( + p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit, + )} + +
+
+ {!appliedDiscount && ( + + {p.price} + {getSymbolFromCurrency(p.currency)} + + )} + {appliedDiscount && ( +
+ + {p.price} + {getSymbolFromCurrency(p.currency)} + + + {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} + {getSymbolFromCurrency(p.currency)} + +
+ )} + {/* -
-
- This includes: -
    -
  • - Train your abilities for the IELTS exam
  • -
  • - - Gain insights into your weaknesses and strengths -
  • -
  • - - Allow yourself to correctly prepare for the exam -
  • -
-
-
- ))} -
-
-
- )} - {!isIndividual() && - user.type === "corporate" && - user?.corporateInformation.payment && ( -
- - To add to your use of EnCoach and that of your students and - teachers, please pay your designated package below: - -
-
- EnCoach's Logo - - EnCoach - {user.corporateInformation?.monthlyDuration}{" "} - Months - -
-
- - {user.corporateInformation.payment.value} - {getSymbolFromCurrency( - user.corporateInformation.payment.currency, - )} - - { - setIsLoading(false); - setTimeout(reload, 500); - }} - loadScript - trackingId={trackingId} - /> -
-
- This includes: -
    -
  • - - Allow a total of{" "} - { - user.corporateInformation.companyInformation - .userAmount - }{" "} - students and teachers to use EnCoach -
  • -
  • - Train their abilities for the IELTS exam
  • -
  • - - Gain insights into your students' weaknesses - and strengths -
  • -
  • - Allow them to correctly prepare for the exam
  • -
-
-
-
- )} - {!isIndividual() && user.type !== "corporate" && ( -
- - You are not the person in charge of your time credits, please - contact your administrator about this situation. - - - If you believe this to be a mistake, please contact the - platform's administration, thank you for your patience. - -
- )} - {!isIndividual() && - user.type === "corporate" && - !user.corporateInformation.payment && ( -
- - An admin nor your agent have yet set the price intended to - your requirements in terms of the amount of users you desire - and your expected monthly duration. - - - Please try again later or contact your agent or an admin, - thank you for your patience. - -
- )} -
- - ) : ( -
- )} - - ); + /> */} + { + setTimeout(reload, 500); + }} + currency={p.currency} + duration={p.duration} + duration_unit={p.duration_unit} + price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} + /> +
+
+ This includes: +
    +
  • - Train your abilities for the IELTS exam
  • +
  • - Gain insights into your weaknesses and strengths
  • +
  • - Allow yourself to correctly prepare for the exam
  • +
+
+
+ ))} +
+
+
+ )} + {!isIndividual() && user.type === "corporate" && user?.corporateInformation.payment && ( +
+ + To add to your use of EnCoach and that of your students and teachers, please pay your designated package below: + +
+
+ EnCoach's Logo + EnCoach - {user.corporateInformation?.monthlyDuration} Months +
+
+ + {user.corporateInformation.payment.value} + {getSymbolFromCurrency(user.corporateInformation.payment.currency)} + + { + setIsLoading(false); + setTimeout(reload, 500); + }} + loadScript + trackingId={trackingId} + /> +
+
+ This includes: +
    +
  • + - Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to + use EnCoach +
  • +
  • - Train their abilities for the IELTS exam
  • +
  • - Gain insights into your students' weaknesses and strengths
  • +
  • - Allow them to correctly prepare for the exam
  • +
+
+
+
+ )} + {!isIndividual() && user.type !== "corporate" && ( +
+ + You are not the person in charge of your time credits, please contact your administrator about this situation. + + + If you believe this to be a mistake, please contact the platform's administration, thank you for your + patience. + +
+ )} + {!isIndividual() && user.type === "corporate" && !user.corporateInformation.payment && ( +
+ + An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users you + desire and your expected monthly duration. + + + Please try again later or contact your agent or an admin, thank you for your patience. + +
+ )} +
+
+ ) : ( +
+ )} + + ); } diff --git a/src/pages/api/paymob.ts b/src/pages/api/paymob.ts new file mode 100644 index 00000000..2cab06cd --- /dev/null +++ b/src/pages/api/paymob.ts @@ -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; +}; diff --git a/yarn.lock b/yarn.lock index cfab02ba..043500ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4870,6 +4870,13 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" 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: version "1.0.0" 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" 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: version "3.2.1" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz" @@ -5330,6 +5345,13 @@ react@18.2.0: dependencies: 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: version "1.0.0" resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" @@ -5535,6 +5557,13 @@ scheduler@^0.23.0: dependencies: 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: version "3.0.5" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"