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",
@@ -98,4 +99,4 @@
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.4",
"types/": "paypal/react-paypal-js" "types/": "paypal/react-paypal-js"
} }
} }

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,194 +4,147 @@ 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;
hasExpired?: boolean; hasExpired?: boolean;
clientID: string; clientID: string;
reload: () => void; reload: () => void;
} }
export default function PaymentDue({ export default function PaymentDue({user, hasExpired = false, clientID, reload}: Props) {
user, const [isLoading, setIsLoading] = useState(false);
hasExpired = false, const [appliedDiscount, setAppliedDiscount] = useState(0);
clientID,
reload,
}: Props) {
const [isLoading, setIsLoading] = useState(false);
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, const trackingId = usePaypalTracking();
isLoading: isInvitesLoading,
reload: reloadInvites,
} = useInvites({ to: user?.id });
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) if (!biggestDiscount) return;
.shift();
if (!biggestDiscount) return;
setAppliedDiscount(biggestDiscount.percentage); setAppliedDiscount(biggestDiscount.percentage);
}, [discounts, user]); }, [discounts, user]);
const isIndividual = () => { const isIndividual = () => {
if (user?.type === "developer") return true; if (user?.type === "developer") return true;
if (user?.type !== "student") return false; if (user?.type !== "student") return false;
const userGroups = groups.filter((g) => g.participants.includes(user?.id)); 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 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) return userGroupsAdminTypes.every((t) => t !== "corporate");
.filter((t) => !!t); };
return userGroupsAdminTypes.every((t) => t !== "corporate");
};
return ( return (
<> <>
<ToastContainer /> <ToastContainer />
{isLoading && ( {isLoading && (
<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... </div>
</span> </div>
</div> )}
</div> {user ? (
)} <Layout user={user} navDisabled={hasExpired}>
{user ? ( {invites.length > 0 && (
<Layout user={user} navDisabled={hasExpired}> <section className="flex flex-col gap-1 md:gap-3">
{invites.length > 0 && ( <div className="flex items-center gap-4">
<section className="flex flex-col gap-1 md:gap-3"> <div
<div className="flex items-center gap-4"> onClick={reloadInvites}
<div className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
onClick={reloadInvites} <span className="text-mti-black text-lg font-bold">Invites</span>
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out" <BsArrowRepeat className={clsx("text-xl", isInvitesLoading && "animate-spin")} />
> </div>
<span className="text-mti-black text-lg font-bold"> </div>
Invites <span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
</span> {invites.map((invite) => (
<BsArrowRepeat <InviteCard
className={clsx( key={invite.id}
"text-xl", invite={invite}
isInvitesLoading && "animate-spin", users={users}
)} reload={() => {
/> reloadInvites();
</div> router.reload();
</div> }}
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll"> />
{invites.map((invite) => ( ))}
<InviteCard </span>
key={invite.id} </section>
invite={invite} )}
users={users}
reload={() => {
reloadInvites();
router.reload();
}}
/>
))}
</span>
</section>
)}
<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"> {isIndividual() && (
You do not have time credits for your account type! <div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll">
</span> <span className="max-w-lg">
)} To add to your use of EnCoach, please purchase one of the time packages available below:
{isIndividual() && ( </span>
<div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll"> <div className="flex w-full flex-wrap justify-center gap-8">
<span className="max-w-lg"> <PayPalScriptProvider
To add to your use of EnCoach, please purchase one of the time options={{
packages available below: clientId: clientID,
</span> currency: "USD",
<div className="flex w-full flex-wrap justify-center gap-8"> intent: "capture",
<PayPalScriptProvider commit: true,
options={{ }}>
clientId: clientID, {packages.map((p) => (
currency: "USD", <div key={p.id} className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
intent: "capture", <div className="mb-2 flex flex-col items-start">
commit: true, <img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
}} <span className="text-xl font-semibold">
> EnCoach - {p.duration}{" "}
{packages.map((p) => ( {capitalize(
<div p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit,
key={p.id} )}
className={clsx( </span>
"flex flex-col items-start gap-6 rounded-xl bg-white p-4", </div>
)} <div className="flex w-full flex-col items-start gap-2">
> {!appliedDiscount && (
<div className="mb-2 flex flex-col items-start"> <span className="text-2xl">
<img {p.price}
src="/logo_title.png" {getSymbolFromCurrency(p.currency)}
alt="EnCoach's Logo" </span>
className="w-32" )}
/> {appliedDiscount && (
<span className="text-xl font-semibold"> <div className="flex items-center gap-2">
EnCoach - {p.duration}{" "} <span className="text-2xl line-through">
{capitalize( {p.price}
p.duration === 1 {getSymbolFromCurrency(p.currency)}
? p.duration_unit.slice( </span>
0, <span className="text-2xl text-mti-red-light">
p.duration_unit.length - 1, {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)}
) {getSymbolFromCurrency(p.currency)}
: p.duration_unit, </span>
)} </div>
</span> )}
</div> {/* <PayPalPayment
<div className="flex w-full flex-col items-start gap-2">
{!appliedDiscount && (
<span className="text-2xl">
{p.price}
{getSymbolFromCurrency(p.currency)}
</span>
)}
{appliedDiscount && (
<div className="flex items-center gap-2">
<span className="text-2xl line-through">
{p.price}
{getSymbolFromCurrency(p.currency)}
</span>
<span className="text-2xl text-mti-red-light">
{(
p.price -
p.price * (appliedDiscount / 100)
).toFixed(2)}
{getSymbolFromCurrency(p.currency)}
</span>
</div>
)}
<PayPalPayment
key={clientID} key={clientID}
clientID={clientID} clientID={clientID}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
@@ -208,127 +161,110 @@ export default function PaymentDue({
p.price * (appliedDiscount / 100) p.price * (appliedDiscount / 100)
).toFixed(2) ).toFixed(2)
} }
/> /> */}
</div> <PaymobPayment
<div className="flex flex-col items-start gap-1"> key={clientID}
<span>This includes:</span> user={user}
<ul className="flex flex-col items-start text-sm"> description="Description"
<li>- Train your abilities for the IELTS exam</li> paymentID="123"
<li> title="Title"
- Gain insights into your weaknesses and strengths setIsLoading={setIsLoading}
</li> onSuccess={() => {
<li> setTimeout(reload, 500);
- Allow yourself to correctly prepare for the exam }}
</li> currency={p.currency}
</ul> duration={p.duration}
</div> duration_unit={p.duration_unit}
</div> price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)}
))} />
</PayPalScriptProvider> </div>
</div> <div className="flex flex-col items-start gap-1">
</div> <span>This includes:</span>
)} <ul className="flex flex-col items-start text-sm">
{!isIndividual() && <li>- Train your abilities for the IELTS exam</li>
user.type === "corporate" && <li>- Gain insights into your weaknesses and strengths</li>
user?.corporateInformation.payment && ( <li>- Allow yourself to correctly prepare for the exam</li>
<div className="flex flex-col items-center"> </ul>
<span className="max-w-lg"> </div>
To add to your use of EnCoach and that of your students and </div>
teachers, please pay your designated package below: ))}
</span> </PayPalScriptProvider>
<div </div>
className={clsx( </div>
"flex flex-col items-start gap-6 rounded-xl bg-white p-4", )}
)} {!isIndividual() && user.type === "corporate" && user?.corporateInformation.payment && (
> <div className="flex flex-col items-center">
<div className="mb-2 flex flex-col items-start"> <span className="max-w-lg">
<img To add to your use of EnCoach and that of your students and teachers, please pay your designated package below:
src="/logo_title.png" </span>
alt="EnCoach's Logo" <div className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
className="w-32" <div className="mb-2 flex flex-col items-start">
/> <img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
<span className="text-xl font-semibold"> <span className="text-xl font-semibold">EnCoach - {user.corporateInformation?.monthlyDuration} Months</span>
EnCoach - {user.corporateInformation?.monthlyDuration}{" "} </div>
Months <div className="flex w-full flex-col items-start gap-2">
</span> <span className="text-2xl">
</div> {user.corporateInformation.payment.value}
<div className="flex w-full flex-col items-start gap-2"> {getSymbolFromCurrency(user.corporateInformation.payment.currency)}
<span className="text-2xl"> </span>
{user.corporateInformation.payment.value} <PayPalPayment
{getSymbolFromCurrency( key={clientID}
user.corporateInformation.payment.currency, clientID={clientID}
)} setIsLoading={setIsLoading}
</span> currency={user.corporateInformation.payment.currency}
<PayPalPayment price={user.corporateInformation.payment.value}
key={clientID} duration={user.corporateInformation.monthlyDuration}
clientID={clientID} duration_unit="months"
setIsLoading={setIsLoading} onSuccess={() => {
currency={user.corporateInformation.payment.currency} setIsLoading(false);
price={user.corporateInformation.payment.value} setTimeout(reload, 500);
duration={user.corporateInformation.monthlyDuration} }}
duration_unit="months" loadScript
onSuccess={() => { trackingId={trackingId}
setIsLoading(false); />
setTimeout(reload, 500); </div>
}} <div className="flex flex-col items-start gap-1">
loadScript <span>This includes:</span>
trackingId={trackingId} <ul className="flex flex-col items-start text-sm">
/> <li>
</div> - Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to
<div className="flex flex-col items-start gap-1"> use EnCoach
<span>This includes:</span> </li>
<ul className="flex flex-col items-start text-sm"> <li>- Train their abilities for the IELTS exam</li>
<li> <li>- Gain insights into your students&apos; weaknesses and strengths</li>
- Allow a total of{" "} <li>- Allow them to correctly prepare for the exam</li>
{ </ul>
user.corporateInformation.companyInformation </div>
.userAmount </div>
}{" "} </div>
students and teachers to use EnCoach )}
</li> {!isIndividual() && user.type !== "corporate" && (
<li>- Train their abilities for the IELTS exam</li> <div className="flex flex-col items-center">
<li> <span className="max-w-lg">
- Gain insights into your students&apos; weaknesses You are not the person in charge of your time credits, please contact your administrator about this situation.
and strengths </span>
</li> <span className="max-w-lg">
<li>- Allow them to correctly prepare for the exam</li> If you believe this to be a mistake, please contact the platform&apos;s administration, thank you for your
</ul> patience.
</div> </span>
</div> </div>
</div> )}
)} {!isIndividual() && user.type === "corporate" && !user.corporateInformation.payment && (
{!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"> An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users you
You are not the person in charge of your time credits, please desire and your expected monthly duration.
contact your administrator about this situation. </span>
</span> <span className="max-w-lg">
<span className="max-w-lg"> Please try again later or contact your agent or an admin, thank you for your patience.
If you believe this to be a mistake, please contact the </span>
platform&apos;s administration, thank you for your patience. </div>
</span> )}
</div> </div>
)} </Layout>
{!isIndividual() && ) : (
user.type === "corporate" && <div />
!user.corporateInformation.payment && ( )}
<div className="flex flex-col items-center"> </>
<span className="max-w-lg"> );
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.
</span>
<span className="max-w-lg">
Please try again later or contact your agent or an admin,
thank you for your patience.
</span>
</div>
)}
</div>
</Layout>
) : (
<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"