Merged in feature-paypal-simple (pull request #44)
Improvements on the Paypal Integration Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -1,77 +1,124 @@
|
|||||||
import {DurationUnit} from "@/interfaces/paypal";
|
import { DurationUnit } from "@/interfaces/paypal";
|
||||||
import {CreateOrderActions, CreateOrderData, OnApproveActions, OnApproveData, OnCancelledActions, OrderResponseBody} from "@paypal/paypal-js";
|
import {
|
||||||
import {PayPalButtons, PayPalScriptProvider, usePayPalScriptReducer} from "@paypal/react-paypal-js";
|
CreateOrderActions,
|
||||||
|
CreateOrderData,
|
||||||
|
OnApproveActions,
|
||||||
|
OnApproveData,
|
||||||
|
OnCancelledActions,
|
||||||
|
OrderResponseBody,
|
||||||
|
} from "@paypal/paypal-js";
|
||||||
|
import {
|
||||||
|
PayPalButtons,
|
||||||
|
PayPalScriptProvider,
|
||||||
|
usePayPalScriptReducer,
|
||||||
|
} from "@paypal/react-paypal-js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {useEffect, useState} from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {toast} from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clientID: string;
|
clientID: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
price: number;
|
price: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_unit: DurationUnit;
|
duration_unit: DurationUnit;
|
||||||
loadScript?: boolean;
|
loadScript?: boolean;
|
||||||
setIsLoading: (isLoading: boolean) => void;
|
setIsLoading: (isLoading: boolean) => void;
|
||||||
onSuccess: (duration: number, duration_unit: DurationUnit) => void;
|
onSuccess: (duration: number, duration_unit: DurationUnit) => void;
|
||||||
|
trackingId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PayPalPayment({clientID, price, currency, duration, duration_unit, loadScript, setIsLoading, onSuccess}: Props) {
|
export default function PayPalPayment({
|
||||||
const createOrder = async (data: CreateOrderData, actions: CreateOrderActions): Promise<string> => {
|
clientID,
|
||||||
setIsLoading(true);
|
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
|
return axios
|
||||||
.post<OrderResponseBody>("/api/paypal", {currencyCode: currency, price})
|
.post<OrderResponseBody>("/api/paypal", {
|
||||||
.then((response) => response.data)
|
currencyCode: currency,
|
||||||
.then((data) => data.id);
|
price,
|
||||||
};
|
trackingId,
|
||||||
|
})
|
||||||
|
.then((response) => response.data)
|
||||||
|
.then((data) => data.id);
|
||||||
|
};
|
||||||
|
|
||||||
const onApprove = async (data: OnApproveData, actions: OnApproveActions) => {
|
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});
|
if (!trackingId) {
|
||||||
|
throw new Error("trackingId is not set");
|
||||||
|
}
|
||||||
|
|
||||||
if (request.status !== 200) {
|
const request = await axios.post<{ ok: boolean; reason?: string }>(
|
||||||
toast.error("Something went wrong, please try again later");
|
"/api/paypal/approve",
|
||||||
return;
|
{ id: data.orderID, duration, duration_unit, trackingId }
|
||||||
}
|
);
|
||||||
|
|
||||||
toast.success("Your account has been credited more time!");
|
if (request.status !== 200) {
|
||||||
return onSuccess(duration, duration_unit);
|
toast.error("Something went wrong, please try again later");
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const onError = async (data: Record<string, unknown>) => {
|
toast.success("Your account has been credited more time!");
|
||||||
setIsLoading(false);
|
return onSuccess(duration, duration_unit);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCancel = async (data: Record<string, unknown>, actions: OnCancelledActions) => {
|
const onError = async (data: Record<string, unknown>) => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return loadScript ? (
|
const onCancel = async (
|
||||||
<PayPalScriptProvider
|
data: Record<string, unknown>,
|
||||||
options={{
|
actions: OnCancelledActions
|
||||||
clientId: clientID,
|
) => {
|
||||||
currency,
|
setIsLoading(false);
|
||||||
intent: "capture",
|
};
|
||||||
commit: true,
|
|
||||||
vault: true,
|
if (trackingId) {
|
||||||
}}>
|
return loadScript ? (
|
||||||
<PayPalButtons
|
<PayPalScriptProvider
|
||||||
className="w-full"
|
options={{
|
||||||
style={{layout: "vertical"}}
|
clientId: clientID,
|
||||||
createOrder={createOrder}
|
currency,
|
||||||
onApprove={onApprove}
|
intent: "capture",
|
||||||
onCancel={onCancel}
|
commit: true,
|
||||||
onError={onError}
|
vault: true,
|
||||||
/>
|
}}
|
||||||
</PayPalScriptProvider>
|
>
|
||||||
) : (
|
<PayPalButtons
|
||||||
<PayPalButtons
|
className="w-full"
|
||||||
className="w-full"
|
style={{ layout: "vertical" }}
|
||||||
style={{layout: "vertical"}}
|
createOrder={createOrder}
|
||||||
createOrder={createOrder}
|
onApprove={onApprove}
|
||||||
onApprove={onApprove}
|
onCancel={onCancel}
|
||||||
onCancel={onCancel}
|
onError={onError}
|
||||||
onError={onError}
|
/>
|
||||||
/>
|
</PayPalScriptProvider>
|
||||||
);
|
) : (
|
||||||
|
<PayPalButtons
|
||||||
|
className="w-full"
|
||||||
|
style={{ layout: "vertical" }}
|
||||||
|
createOrder={createOrder}
|
||||||
|
onApprove={onApprove}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onError={onError}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/hooks/usePaypalTracking.tsx
Normal file
20
src/hooks/usePaypalTracking.tsx
Normal 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;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ 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";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -31,6 +32,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {groups} = useGroups();
|
const {groups} = useGroups();
|
||||||
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
|
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
|
||||||
|
const trackingId = usePaypalTracking();
|
||||||
|
|
||||||
const isIndividual = () => {
|
const isIndividual = () => {
|
||||||
if (user?.type === "developer") return true;
|
if (user?.type === "developer") return true;
|
||||||
@@ -121,6 +123,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setTimeout(reload, 500);
|
setTimeout(reload, 500);
|
||||||
}}
|
}}
|
||||||
|
trackingId={trackingId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start gap-1">
|
<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);
|
setTimeout(reload, 500);
|
||||||
}}
|
}}
|
||||||
loadScript
|
loadScript
|
||||||
|
trackingId={trackingId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start gap-1">
|
<div className="flex flex-col items-start gap-1">
|
||||||
|
|||||||
@@ -1,57 +1,78 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import {app} from "@/firebase";
|
import { app } from "@/firebase";
|
||||||
import {getFirestore, collection, getDocs, setDoc, doc} from "firebase/firestore";
|
import {
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
getFirestore,
|
||||||
import {sessionOptions} from "@/lib/session";
|
collection,
|
||||||
|
getDocs,
|
||||||
|
setDoc,
|
||||||
|
doc,
|
||||||
|
} from "firebase/firestore";
|
||||||
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {DurationUnit, TokenError, TokenSuccess} from "@/interfaces/paypal";
|
import { DurationUnit, TokenError, TokenSuccess } from "@/interfaces/paypal";
|
||||||
import {base64} from "@firebase/util";
|
import { base64 } from "@firebase/util";
|
||||||
import {v4} from "uuid";
|
import { v4 } from "uuid";
|
||||||
import {OrderResponseBody} from "@paypal/paypal-js";
|
import { OrderResponseBody } from "@paypal/paypal-js";
|
||||||
import {getAccessToken} from "@/utils/paypal";
|
import { getAccessToken } from "@/utils/paypal";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {Group} from "@/interfaces/user";
|
import { Group } from "@/interfaces/user";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method !== "POST") return res.status(404).json({ok: false, reason: "Method not supported!"});
|
if (req.method !== "POST")
|
||||||
if (!req.session.user) return res.status(401).json({ok: false});
|
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();
|
const accessToken = await getAccessToken();
|
||||||
if (!accessToken) return res.status(401).json({ok: false, reason: "Authorization failed!"});
|
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(
|
if (!trackingId)
|
||||||
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders/${id}/capture`,
|
return res.status(401).json({ ok: false, reason: "Missing tracking id!" });
|
||||||
{},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (request.data.status === "COMPLETED") {
|
const request = await axios.post(
|
||||||
const user = req.session.user;
|
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders/${id}/capture`,
|
||||||
const subscriptionExpirationDate = req.session.user.subscriptionExpirationDate;
|
{},
|
||||||
const today = moment(new Date());
|
{
|
||||||
const dateToBeAddedTo = !subscriptionExpirationDate
|
headers: {
|
||||||
? today
|
Authorization: `Bearer ${accessToken}`,
|
||||||
: moment(subscriptionExpirationDate).isAfter(today)
|
"PayPal-Client-Metadata-Id": trackingId,
|
||||||
? moment(subscriptionExpirationDate)
|
},
|
||||||
: today;
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const updatedExpirationDate = dateToBeAddedTo.add(duration, duration_unit);
|
if (request.data.status === "COMPLETED") {
|
||||||
await setDoc(
|
const user = req.session.user;
|
||||||
doc(db, "users", req.session.user.id),
|
const subscriptionExpirationDate =
|
||||||
{subscriptionExpirationDate: updatedExpirationDate.toISOString(), status: "active"},
|
req.session.user.subscriptionExpirationDate;
|
||||||
{merge: true},
|
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 {
|
try {
|
||||||
await setDoc(
|
await setDoc(
|
||||||
@@ -72,31 +93,40 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
console.error('Failed to insert paypal payment!', err);
|
console.error('Failed to insert paypal payment!', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.type === "corporate") {
|
if (user.type === "corporate") {
|
||||||
const snapshot = await getDocs(collection(db, "groups"));
|
const snapshot = await getDocs(collection(db, "groups"));
|
||||||
const groups: Group[] = (
|
const groups: Group[] = (
|
||||||
snapshot.docs.map((doc) => ({
|
snapshot.docs.map((doc) => ({
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
...doc.data(),
|
...doc.data(),
|
||||||
})) as Group[]
|
})) as Group[]
|
||||||
).filter((x) => x.admin === user.id);
|
).filter((x) => x.admin === user.id);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groups
|
groups
|
||||||
.flatMap((x) => x.participants)
|
.flatMap((x) => x.participants)
|
||||||
.map(
|
.map(
|
||||||
async (x) =>
|
async (x) =>
|
||||||
await setDoc(
|
await setDoc(
|
||||||
doc(db, "users", x),
|
doc(db, "users", x),
|
||||||
{subscriptionExpirationDate: updatedExpirationDate.toISOString(), status: "active"},
|
{
|
||||||
{merge: true},
|
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!",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const accessToken = await getAccessToken();
|
const accessToken = await getAccessToken();
|
||||||
if (!accessToken) return res.status(401).json({ok: false, reason: "Authorization failed!"});
|
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>(
|
const request = await axios.post<OrderResponseBody>(
|
||||||
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders`,
|
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders`,
|
||||||
@@ -34,11 +36,24 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
reference_id: v4(),
|
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",
|
intent: "CAPTURE",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'PayPal-Client-Metadata-Id': trackingId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
55
src/pages/api/paypal/raas.ts
Normal file
55
src/pages/api/paypal/raas.ts
Normal 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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -170,7 +170,9 @@ export default function Tickets() {
|
|||||||
{dateSorting === "asc" && <BsArrowUp />}
|
{dateSorting === "asc" && <BsArrowUp />}
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) 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", {
|
columnHelper.accessor("subject", {
|
||||||
header: "Subject",
|
header: "Subject",
|
||||||
|
|||||||
Reference in New Issue
Block a user