Merged in develop (pull request #45)

Update - 05/03/2024
This commit is contained in:
Tiago Ribeiro
2024-03-05 17:29:33 +00:00
11 changed files with 317 additions and 142 deletions

View File

@@ -76,7 +76,7 @@ export default function InteractiveSpeaking({
onBack({
exercise: id,
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
score: {correct: 1, total: 1, missing: 0},
score: {correct: 100, total: 100, missing: 0},
type,
});
};
@@ -96,7 +96,7 @@ export default function InteractiveSpeaking({
onNext({
exercise: id,
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
score: {correct: 1, total: 1, missing: 0},
score: {correct: 100, total: 100, missing: 0},
type,
});
};
@@ -131,7 +131,7 @@ export default function InteractiveSpeaking({
onNext({
exercise: id,
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
score: {correct: 1, total: 1, missing: 0},
score: {correct: 100, total: 100, missing: 0},
type,
});
}
@@ -176,7 +176,7 @@ export default function InteractiveSpeaking({
{
exercise: id,
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
score: {correct: 1, total: 1, missing: 0},
score: {correct: 100, total: 100, missing: 0},
module: "speaking",
exam: examID,
type,

View File

@@ -81,7 +81,7 @@ export default function Speaking({id, title, text, video_url, type, prompts, use
onNext({
exercise: id,
solutions: storagePath ? [{id, solution: storagePath}] : [],
score: {correct: 1, total: 1, missing: 0},
score: {correct: 100, total: 100, missing: 0},
type,
});
};
@@ -94,7 +94,7 @@ export default function Speaking({id, title, text, video_url, type, prompts, use
onBack({
exercise: id,
solutions: storagePath ? [{id, solution: storagePath}] : [],
score: {correct: 1, total: 1, missing: 0},
score: {correct: 100, total: 100, missing: 0},
type,
});
};

View File

@@ -41,7 +41,7 @@ export default function Writing({
if (inputText.length > 0 && saveTimer % 10 === 0) {
setUserSolutions([
...storeUserSolutions.filter((x) => x.exercise !== id),
{exercise: id, solutions: [{id, solution: inputText}], score: {correct: 1, total: 1, missing: 0}, type, module: "writing"},
{exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type, module: "writing"},
]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -65,7 +65,7 @@ export default function Writing({
useEffect(() => {
if (hasExamEnded)
onNext({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 1, total: 1, missing: 0}, type, module: "writing"});
onNext({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type, module: "writing"});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]);
@@ -148,7 +148,9 @@ export default function Writing({
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 1, total: 1, missing: 0}, type})}
onClick={() =>
onBack({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type})
}
className="max-w-[200px] self-end w-full">
Back
</Button>
@@ -159,7 +161,7 @@ export default function Writing({
onNext({
exercise: id,
solutions: [{id, solution: inputText.replaceAll(/\s{2,}/g, " ")}],
score: {correct: 1, total: 1, missing: 0},
score: {correct: 100, total: 100, missing: 0},
type,
module: "writing",
})

View File

@@ -1,77 +1,124 @@
import {DurationUnit} from "@/interfaces/paypal";
import {CreateOrderActions, CreateOrderData, OnApproveActions, OnApproveData, OnCancelledActions, OrderResponseBody} from "@paypal/paypal-js";
import {PayPalButtons, PayPalScriptProvider, usePayPalScriptReducer} from "@paypal/react-paypal-js";
import { DurationUnit } from "@/interfaces/paypal";
import {
CreateOrderActions,
CreateOrderData,
OnApproveActions,
OnApproveData,
OnCancelledActions,
OrderResponseBody,
} from "@paypal/paypal-js";
import {
PayPalButtons,
PayPalScriptProvider,
usePayPalScriptReducer,
} from "@paypal/react-paypal-js";
import axios from "axios";
import {useEffect, useState} from "react";
import {toast} from "react-toastify";
import { useState, useEffect } from "react";
import { toast } from "react-toastify";
interface Props {
clientID: string;
currency: string;
price: number;
duration: number;
duration_unit: DurationUnit;
loadScript?: boolean;
setIsLoading: (isLoading: boolean) => void;
onSuccess: (duration: number, duration_unit: DurationUnit) => void;
clientID: string;
currency: string;
price: number;
duration: number;
duration_unit: DurationUnit;
loadScript?: boolean;
setIsLoading: (isLoading: boolean) => void;
onSuccess: (duration: number, duration_unit: DurationUnit) => void;
trackingId?: string;
}
export default function PayPalPayment({clientID, price, currency, duration, duration_unit, loadScript, setIsLoading, onSuccess}: Props) {
const createOrder = async (data: CreateOrderData, actions: CreateOrderActions): Promise<string> => {
setIsLoading(true);
export default function PayPalPayment({
clientID,
price,
currency,
duration,
duration_unit,
loadScript,
setIsLoading,
onSuccess,
trackingId,
}: Props) {
const createOrder = async (
data: CreateOrderData,
actions: CreateOrderActions
): Promise<string> => {
if (!trackingId) {
throw new Error("trackingId is not set");
}
setIsLoading(true);
return axios
.post<OrderResponseBody>("/api/paypal", {currencyCode: currency, price})
.then((response) => response.data)
.then((data) => data.id);
};
return axios
.post<OrderResponseBody>("/api/paypal", {
currencyCode: currency,
price,
trackingId,
})
.then((response) => response.data)
.then((data) => data.id);
};
const onApprove = async (data: OnApproveData, actions: OnApproveActions) => {
const request = await axios.post<{ok: boolean; reason?: string}>("/api/paypal/approve", {id: data.orderID, duration, duration_unit});
const onApprove = async (data: OnApproveData, actions: OnApproveActions) => {
if (!trackingId) {
throw new Error("trackingId is not set");
}
if (request.status !== 200) {
toast.error("Something went wrong, please try again later");
return;
}
const request = await axios.post<{ ok: boolean; reason?: string }>(
"/api/paypal/approve",
{ id: data.orderID, duration, duration_unit, trackingId }
);
toast.success("Your account has been credited more time!");
return onSuccess(duration, duration_unit);
};
if (request.status !== 200) {
toast.error("Something went wrong, please try again later");
return;
}
const onError = async (data: Record<string, unknown>) => {
setIsLoading(false);
};
toast.success("Your account has been credited more time!");
return onSuccess(duration, duration_unit);
};
const onCancel = async (data: Record<string, unknown>, actions: OnCancelledActions) => {
setIsLoading(false);
};
const onError = async (data: Record<string, unknown>) => {
setIsLoading(false);
};
return loadScript ? (
<PayPalScriptProvider
options={{
clientId: clientID,
currency,
intent: "capture",
commit: true,
vault: true,
}}>
<PayPalButtons
className="w-full"
style={{layout: "vertical"}}
createOrder={createOrder}
onApprove={onApprove}
onCancel={onCancel}
onError={onError}
/>
</PayPalScriptProvider>
) : (
<PayPalButtons
className="w-full"
style={{layout: "vertical"}}
createOrder={createOrder}
onApprove={onApprove}
onCancel={onCancel}
onError={onError}
/>
);
const onCancel = async (
data: Record<string, unknown>,
actions: OnCancelledActions
) => {
setIsLoading(false);
};
if (trackingId) {
return loadScript ? (
<PayPalScriptProvider
options={{
clientId: clientID,
currency,
intent: "capture",
commit: true,
vault: true,
}}
>
<PayPalButtons
className="w-full"
style={{ layout: "vertical" }}
createOrder={createOrder}
onApprove={onApprove}
onCancel={onCancel}
onError={onError}
/>
</PayPalScriptProvider>
) : (
<PayPalButtons
className="w-full"
style={{ layout: "vertical" }}
createOrder={createOrder}
onApprove={onApprove}
onCancel={onCancel}
onError={onError}
/>
);
}
return null;
}

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from "react";
import axios from "axios";
export const usePaypalTracking = () => {
const [trackingId, setTrackingId] = useState<string>();
useEffect(() => {
axios
.put<{ ok: boolean; trackingId: string }>("/api/paypal/raas")
.then((response) => {
if (response.data.ok) {
setTrackingId(response.data.trackingId);
}
})
.catch((error) => {
console.error(error);
});
}, []);
return trackingId;
};

View File

@@ -14,6 +14,7 @@ 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";
interface Props {
user: User;
@@ -31,7 +32,8 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
const {users} = useUsers();
const {groups} = useGroups();
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
const trackingId = usePaypalTracking();
const isIndividual = () => {
if (user?.type === "developer") return true;
if (user?.type !== "student") return false;
@@ -121,6 +123,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
onSuccess={() => {
setTimeout(reload, 500);
}}
trackingId={trackingId}
/>
</div>
<div className="flex flex-col items-start gap-1">
@@ -165,6 +168,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
setTimeout(reload, 500);
}}
loadScript
trackingId={trackingId}
/>
</div>
<div className="flex flex-col items-start gap-1">

View File

@@ -1,58 +1,79 @@
// 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 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 axios from "axios";
import {DurationUnit, TokenError, TokenSuccess} from "@/interfaces/paypal";
import {base64} from "@firebase/util";
import {v4} from "uuid";
import {OrderResponseBody} from "@paypal/paypal-js";
import {getAccessToken} from "@/utils/paypal";
import { DurationUnit, TokenError, TokenSuccess } from "@/interfaces/paypal";
import { base64 } from "@firebase/util";
import { v4 } from "uuid";
import { OrderResponseBody } from "@paypal/paypal-js";
import { getAccessToken } from "@/utils/paypal";
import moment from "moment";
import {Group} from "@/interfaces/user";
import { Group } from "@/interfaces/user";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(404).json({ok: false, reason: "Method not supported!"});
if (!req.session.user) return res.status(401).json({ok: false});
if (req.method !== "POST")
return res.status(404).json({ ok: false, reason: "Method not supported!" });
if (!req.session.user) return res.status(401).json({ ok: false });
const accessToken = await getAccessToken();
if (!accessToken) return res.status(401).json({ok: false, reason: "Authorization failed!"});
const accessToken = await getAccessToken();
if (!accessToken)
return res.status(401).json({ ok: false, reason: "Authorization failed!" });
const {id, duration, duration_unit} = req.body as {id: string; duration: number; duration_unit: DurationUnit};
const { id, duration, duration_unit, trackingId } = req.body as {
id: string;
duration: number;
duration_unit: DurationUnit;
trackingId: string;
};
const request = await axios.post(
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders/${id}/capture`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
if (!trackingId)
return res.status(401).json({ ok: false, reason: "Missing tracking id!" });
if (request.data.status === "COMPLETED") {
const user = req.session.user;
const subscriptionExpirationDate = req.session.user.subscriptionExpirationDate;
const today = moment(new Date());
const dateToBeAddedTo = !subscriptionExpirationDate
? today
: moment(subscriptionExpirationDate).isAfter(today)
? moment(subscriptionExpirationDate)
: today;
const request = await axios.post(
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders/${id}/capture`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"PayPal-Client-Metadata-Id": trackingId,
},
}
);
const updatedExpirationDate = dateToBeAddedTo.add(duration, duration_unit);
await setDoc(
doc(db, "users", req.session.user.id),
{subscriptionExpirationDate: updatedExpirationDate.toISOString(), status: "active"},
{merge: true},
);
if (request.data.status === "COMPLETED") {
const user = req.session.user;
const subscriptionExpirationDate =
req.session.user.subscriptionExpirationDate;
const today = moment(new Date());
const dateToBeAddedTo = !subscriptionExpirationDate
? today
: moment(subscriptionExpirationDate).isAfter(today)
? moment(subscriptionExpirationDate)
: today;
const updatedExpirationDate = dateToBeAddedTo.add(duration, duration_unit);
await setDoc(
doc(db, "users", req.session.user.id),
{
subscriptionExpirationDate: updatedExpirationDate.toISOString(),
status: "active",
},
{ merge: true }
);
try {
await setDoc(
doc(db, 'paypalpayments', v4()),
@@ -72,31 +93,40 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
console.error('Failed to insert paypal payment!', err);
}
if (user.type === "corporate") {
const snapshot = await getDocs(collection(db, "groups"));
const groups: Group[] = (
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Group[]
).filter((x) => x.admin === user.id);
if (user.type === "corporate") {
const snapshot = await getDocs(collection(db, "groups"));
const groups: Group[] = (
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Group[]
).filter((x) => x.admin === user.id);
await Promise.all(
groups
.flatMap((x) => x.participants)
.map(
async (x) =>
await setDoc(
doc(db, "users", x),
{subscriptionExpirationDate: updatedExpirationDate.toISOString(), status: "active"},
{merge: true},
),
),
);
}
await Promise.all(
groups
.flatMap((x) => x.participants)
.map(
async (x) =>
await setDoc(
doc(db, "users", x),
{
subscriptionExpirationDate:
updatedExpirationDate.toISOString(),
status: "active",
},
{ merge: true }
)
)
);
}
return res.status(200).json({ok: true});
}
return res.status(200).json({ ok: true });
}
res.status(404).json({ok: false, reason: "Order ID not found or purchase was not approved!"});
res
.status(404)
.json({
ok: false,
reason: "Order ID not found or purchase was not approved!",
});
}

View File

@@ -20,7 +20,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const accessToken = await getAccessToken();
if (!accessToken) return res.status(401).json({ok: false, reason: "Authorization failed!"});
const {currencyCode, price} = req.body as {currencyCode: string; price: number};
const {currencyCode, price, trackingId} = req.body as {currencyCode: string; price: number, trackingId: string};
if(!trackingId) return res.status(401).json({ok: false, reason: "Missing tracking id!"});
const request = await axios.post<OrderResponseBody>(
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders`,
@@ -34,11 +36,24 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
reference_id: v4(),
},
],
payment_source: {
paypal: {
email_address: req.session.user.email || "",
experience_context: {
payment_method_preference: "IMMEDIATE_PAYMENT_REQUIRED",
locale: "en-US",
landing_page: "LOGIN",
shipping_preference: "NO_SHIPPING",
user_action: "PAY_NOW",
},
},
},
intent: "CAPTURE",
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
'PayPal-Client-Metadata-Id': trackingId,
},
},
);

View File

@@ -0,0 +1,55 @@
// 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 } from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import { v4 } from "uuid";
import { OrderResponseBody } from "@paypal/paypal-js";
import { getAccessToken } from "@/utils/paypal";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "PUT")
return res.status(404).json({ ok: false, reason: "Method not supported!" });
if (!req.session.user) return res.status(401).json({ ok: false });
const accessToken = await getAccessToken();
if (!accessToken)
return res.status(401).json({ ok: false, reason: "Authorization failed!" });
const trackingId = `${req.session.user.id}-${Date.now()}`;
try {
const request = await axios.put(
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v1/risk/transaction-contexts/${process.env.PAYPAL_MERCHANT_ID}/${trackingId}`,
{
additional_data: [
{
key: "user_id",
value: req.session.user.id,
},
],
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
return res.status(request.status).json({
ok: true,
trackingId,
});
} catch (err) {
return res
.status(500)
.json({ ok: false, reason: "Failed to create tracking ID" });
}
}

View File

@@ -28,7 +28,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
const audioFile = files.audio;
const audioFileRef = ref(storage, `${fields.root}/${(audioFile as any).path.split("/").pop()!.replace("upload_", "")}`);
const audioFileRef = ref(storage, `${fields.root}/${(audioFile as any).path.replace("upload_", "")}`);
const binary = fs.readFileSync((audioFile as any).path).buffer;
const snapshot = await uploadBytes(audioFileRef, binary);

View File

@@ -170,7 +170,9 @@ export default function Tickets() {
{dateSorting === "asc" && <BsArrowUp />}
</button>
) as any,
cell: (info) => moment(info.getValue()).format("DD/MM/YYYY - HH:mm"),
cell: (info) => (
<span className="whitespace-nowrap">{moment(info.getValue()).format("DD/MM/YYYY - HH:mm")}</span>
),
}),
columnHelper.accessor("subject", {
header: "Subject",