send message to recipient
This commit is contained in:
parent
29f405290c
commit
5bf885c060
@ -6,8 +6,8 @@ import clsx from "clsx";
|
|||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
className="grid grid-cols-4 bg-[#F7F7F7] border-t border-gray-400 border-opacity-25 py-3 z-10"
|
className="grid grid-cols-4 bg-[#F7F7F7] h-16 border-t border-gray-400 border-opacity-25 py-2 z-10"
|
||||||
style={{ flex: "0 0 50px" }}
|
// className="grid grid-cols-4 border-t border-gray-400 border-opacity-25 py-3 z-10 backdrop-blur"
|
||||||
>
|
>
|
||||||
<FooterLink label="Calls" path="/calls" icon={<IoCall className="w-6 h-6" />} />
|
<FooterLink label="Calls" path="/calls" icon={<IoCall className="w-6 h-6" />} />
|
||||||
<FooterLink label="Keypad" path="/keypad" icon={<IoKeypad className="w-6 h-6" />} />
|
<FooterLink label="Keypad" path="/keypad" icon={<IoKeypad className="w-6 h-6" />} />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useMatches } from "@remix-run/react";
|
import { useMatches } from "@remix-run/react";
|
||||||
|
|
||||||
import type { SessionOrganization, SessionUser } from "~/utils/auth.server";
|
import type { SessionData } from "~/utils/auth.server";
|
||||||
|
|
||||||
export default function useSession() {
|
export default function useSession() {
|
||||||
const matches = useMatches();
|
const matches = useMatches();
|
||||||
@ -9,5 +9,5 @@ export default function useSession() {
|
|||||||
throw new Error("useSession hook called outside _app route");
|
throw new Error("useSession hook called outside _app route");
|
||||||
}
|
}
|
||||||
|
|
||||||
return __appRoute.data as SessionUser & { currentOrganization: SessionOrganization };
|
return __appRoute.data as SessionData;
|
||||||
}
|
}
|
||||||
|
61
app/features/messages/actions/messages.$recipient.tsx
Normal file
61
app/features/messages/actions/messages.$recipient.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { type ActionFunction } from "@remix-run/node";
|
||||||
|
import { json } from "superjson-remix";
|
||||||
|
|
||||||
|
import db from "~/utils/db.server";
|
||||||
|
import { requireLoggedIn } from "~/utils/auth.server";
|
||||||
|
import getTwilioClient, { translateMessageDirection, translateMessageStatus } from "~/utils/twilio.server";
|
||||||
|
|
||||||
|
export type NewMessageActionData = {};
|
||||||
|
|
||||||
|
const action: ActionFunction = async ({ params, request }) => {
|
||||||
|
const user = await requireLoggedIn(request);
|
||||||
|
const organization = user.organizations[0];
|
||||||
|
const phoneNumber = await db.phoneNumber.findUnique({
|
||||||
|
where: { organizationId_isCurrent: { organizationId: user.organizations[0].id, isCurrent: true } },
|
||||||
|
});
|
||||||
|
const recipient = decodeURIComponent(params.recipient ?? "");
|
||||||
|
const formData = Object.fromEntries(await request.formData());
|
||||||
|
|
||||||
|
const { twilioAccountSid, twilioSubAccountSid } = organization;
|
||||||
|
// const twilioClient = getTwilioClient({ twilioSubAccountSid, twilioAccountSid });
|
||||||
|
const twilioClient = getTwilioClient({ twilioSubAccountSid: twilioAccountSid, twilioAccountSid });
|
||||||
|
try {
|
||||||
|
console.log({ twilioAccountSid, twilioSubAccountSid });
|
||||||
|
console.log({
|
||||||
|
body: formData.content.toString(),
|
||||||
|
to: recipient,
|
||||||
|
from: phoneNumber!.number,
|
||||||
|
});
|
||||||
|
const message = await twilioClient.messages.create({
|
||||||
|
body: formData.content.toString(),
|
||||||
|
to: recipient,
|
||||||
|
from: phoneNumber!.number,
|
||||||
|
});
|
||||||
|
await db.message.create({
|
||||||
|
data: {
|
||||||
|
phoneNumberId: phoneNumber!.id,
|
||||||
|
id: message.sid,
|
||||||
|
to: message.to,
|
||||||
|
from: message.from,
|
||||||
|
status: translateMessageStatus(message.status),
|
||||||
|
direction: translateMessageDirection(message.direction),
|
||||||
|
sentAt: new Date(message.dateCreated),
|
||||||
|
content: message.body,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
// TODO: handle twilio error
|
||||||
|
console.log(error.code); // 21211
|
||||||
|
console.log(error.moreInfo); // https://www.twilio.com/docs/errors/21211
|
||||||
|
console.log(JSON.stringify(error));
|
||||||
|
throw error;
|
||||||
|
/*await db.message.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status: MessageStatus.Error /!*errorMessage: "Reason: failed because of"*!/ },
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
|
||||||
|
return json<NewMessageActionData>({});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default action;
|
@ -1,5 +1,5 @@
|
|||||||
import { Suspense, useEffect, useMemo, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useParams } from "@remix-run/react";
|
import { useParams, useTransition } from "@remix-run/react";
|
||||||
import { useLoaderData } from "superjson-remix";
|
import { useLoaderData } from "superjson-remix";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Direction } from "@prisma/client";
|
import { Direction } from "@prisma/client";
|
||||||
@ -7,14 +7,31 @@ import { Direction } from "@prisma/client";
|
|||||||
import NewMessageArea from "./new-message-area";
|
import NewMessageArea from "./new-message-area";
|
||||||
import { formatDate, formatTime } from "~/features/core/helpers/date-formatter";
|
import { formatDate, formatTime } from "~/features/core/helpers/date-formatter";
|
||||||
import { type ConversationLoaderData } from "~/routes/__app/messages.$recipient";
|
import { type ConversationLoaderData } from "~/routes/__app/messages.$recipient";
|
||||||
|
import useSession from "~/features/core/hooks/use-session";
|
||||||
|
|
||||||
export default function Conversation() {
|
export default function Conversation() {
|
||||||
|
const { currentPhoneNumber } = useSession();
|
||||||
const params = useParams<{ recipient: string }>();
|
const params = useParams<{ recipient: string }>();
|
||||||
const recipient = decodeURIComponent(params.recipient ?? "");
|
const recipient = decodeURIComponent(params.recipient ?? "");
|
||||||
const { conversation } = useLoaderData<ConversationLoaderData>();
|
const { conversation } = useLoaderData<ConversationLoaderData>();
|
||||||
const messages = useMemo(() => conversation?.messages ?? [], [conversation?.messages]);
|
const transition = useTransition();
|
||||||
const messagesListRef = useRef<HTMLUListElement>(null);
|
const messagesListRef = useRef<HTMLUListElement>(null);
|
||||||
|
|
||||||
|
const messages = conversation.messages;
|
||||||
|
if (transition.submission) {
|
||||||
|
messages.push({
|
||||||
|
id: "temp",
|
||||||
|
phoneNumberId: currentPhoneNumber.id,
|
||||||
|
from: currentPhoneNumber.number,
|
||||||
|
to: recipient,
|
||||||
|
sentAt: new Date(),
|
||||||
|
direction: Direction.Outbound,
|
||||||
|
|
||||||
|
status: "Queued",
|
||||||
|
content: transition.submission.formData.get("content")!.toString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesListRef.current?.querySelector("li:last-child")?.scrollIntoView();
|
messagesListRef.current?.querySelector("li:last-child")?.scrollIntoView();
|
||||||
}, [messages, messagesListRef]);
|
}, [messages, messagesListRef]);
|
||||||
@ -74,7 +91,7 @@ export default function Conversation() {
|
|||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<NewMessageArea recipient={recipient} />
|
<NewMessageArea />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,68 +1,34 @@
|
|||||||
import type { FunctionComponent } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { Form, useTransition } from "@remix-run/react";
|
||||||
import { IoSend } from "react-icons/io5";
|
import { IoSend } from "react-icons/io5";
|
||||||
import { type Message, Direction, MessageStatus } from "@prisma/client";
|
|
||||||
import useSession from "~/features/core/hooks/use-session";
|
|
||||||
|
|
||||||
type Props = {
|
function NewMessageArea() {
|
||||||
recipient: string;
|
const transition = useTransition();
|
||||||
onSend?: () => void;
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
};
|
const textFieldRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const isSendingMessage = transition.state === "submitting";
|
||||||
|
|
||||||
const NewMessageArea: FunctionComponent<Props> = ({ recipient, onSend }) => {
|
useEffect(() => {
|
||||||
const { currentOrganization, /*hasOngoingSubscription*/ } = useSession();
|
if (isSendingMessage) {
|
||||||
// const phoneNumber = useCurrentPhoneNumber();
|
formRef.current?.reset();
|
||||||
// const sendMessageMutation = useMutation(sendMessage)[0];
|
textFieldRef.current?.focus();
|
||||||
const onSubmit = async () => {
|
|
||||||
/*const id = uuidv4();
|
|
||||||
const message: Message = {
|
|
||||||
id,
|
|
||||||
organizationId: organization!.id,
|
|
||||||
phoneNumberId: phoneNumber!.id,
|
|
||||||
from: phoneNumber!.number,
|
|
||||||
to: recipient,
|
|
||||||
content: hasOngoingSubscription
|
|
||||||
? content
|
|
||||||
: content + "\n\nSent from Shellphone (https://www.shellphone.app)",
|
|
||||||
direction: Direction.Outbound,
|
|
||||||
status: MessageStatus.Queued,
|
|
||||||
sentAt: new Date(),
|
|
||||||
};*/
|
|
||||||
|
|
||||||
/*await setConversationsQueryData(
|
|
||||||
(conversations) => {
|
|
||||||
const nextConversations = { ...conversations };
|
|
||||||
if (!nextConversations[recipient]) {
|
|
||||||
nextConversations[recipient] = {
|
|
||||||
recipient,
|
|
||||||
formattedPhoneNumber: recipient,
|
|
||||||
messages: [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}, [isSendingMessage]);
|
||||||
nextConversations[recipient]!.messages = [...nextConversations[recipient]!.messages, message];
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(nextConversations).sort(
|
|
||||||
([, a], [, b]) =>
|
|
||||||
b.messages[b.messages.length - 1]!.sentAt.getTime() -
|
|
||||||
a.messages[a.messages.length - 1]!.sentAt.getTime(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ refetch: false },
|
|
||||||
);*/
|
|
||||||
// setValue("content", "");
|
|
||||||
// onSend?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<Form
|
||||||
onSubmit={onSubmit}
|
ref={formRef}
|
||||||
className="absolute bottom-0 w-screen backdrop-filter backdrop-blur-xl bg-white bg-opacity-75 border-t flex flex-row h-16 p-2 pr-0"
|
method="post"
|
||||||
|
className="absolute bottom-0 w-screen backdrop-filter backdrop-blur-xl bg-white bg-opacity-75 border-t flex flex-row h-14 mb-16 p-2 pr-0"
|
||||||
|
replace
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={textFieldRef}
|
||||||
name="content"
|
name="content"
|
||||||
className="resize-none flex-1"
|
className="resize-none rounded-full flex-1"
|
||||||
|
style={{
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
}}
|
||||||
autoCapitalize="sentences"
|
autoCapitalize="sentences"
|
||||||
autoCorrect="on"
|
autoCorrect="on"
|
||||||
placeholder="Text message"
|
placeholder="Text message"
|
||||||
@ -73,16 +39,8 @@ const NewMessageArea: FunctionComponent<Props> = ({ recipient, onSend }) => {
|
|||||||
<button type="submit">
|
<button type="submit">
|
||||||
<IoSend className="h-8 w-8 pl-1 pr-2" />
|
<IoSend className="h-8 w-8 pl-1 pr-2" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default NewMessageArea;
|
export default NewMessageArea;
|
||||||
|
|
||||||
function uuidv4() {
|
|
||||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
||||||
const r = (Math.random() * 16) | 0,
|
|
||||||
v = c == "x" ? r : (r & 0x3) | 0x8;
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -5,7 +5,7 @@ import { type SessionData, type SessionOrganization, requireLoggedIn } from "~/u
|
|||||||
import Footer from "~/features/core/components/footer";
|
import Footer from "~/features/core/components/footer";
|
||||||
import db from "~/utils/db.server";
|
import db from "~/utils/db.server";
|
||||||
|
|
||||||
export type AppLoaderData = SessionData
|
export type AppLoaderData = SessionData;
|
||||||
|
|
||||||
export const loader: LoaderFunction = async ({ request }) => {
|
export const loader: LoaderFunction = async ({ request }) => {
|
||||||
const user = await requireLoggedIn(request);
|
const user = await requireLoggedIn(request);
|
||||||
@ -16,21 +16,27 @@ export const loader:LoaderFunction = async ({ request }) => {
|
|||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
select: { role: true },
|
select: { role: true },
|
||||||
},
|
},
|
||||||
|
phoneNumbers: {
|
||||||
|
where: { isCurrent: true },
|
||||||
|
select: { id: true, number: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const currentOrganization: SessionOrganization = {
|
const currentOrganization: SessionOrganization = {
|
||||||
id: organization!.id,
|
id: organization!.id,
|
||||||
twilioAccountSid: organization!.twilioAccountSid,
|
twilioAccountSid: organization!.twilioAccountSid,
|
||||||
|
twilioSubAccountSid: organization!.twilioSubAccountSid,
|
||||||
role: organization!.memberships[0].role,
|
role: organization!.memberships[0].role,
|
||||||
};
|
};
|
||||||
|
const currentPhoneNumber = organization!.phoneNumbers[0];
|
||||||
|
|
||||||
return json<AppLoaderData>({ ...user, currentOrganization });
|
return json<AppLoaderData>({ ...user, currentOrganization, currentPhoneNumber });
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function __App() {
|
export default function __App() {
|
||||||
const hideFooter = false;
|
const hideFooter = false;
|
||||||
const matches = useMatches();
|
const matches = useMatches();
|
||||||
// matches[0].handle
|
// matches[0].handle.hideFooter
|
||||||
// console.log("matches", matches);
|
// console.log("matches", matches);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -55,9 +61,7 @@ export function CatchBoundary() {
|
|||||||
<div className="h-full w-full overflow-hidden fixed bg-gray-100">
|
<div className="h-full w-full overflow-hidden fixed bg-gray-100">
|
||||||
<div className="flex flex-col w-full h-full">
|
<div className="flex flex-col w-full h-full">
|
||||||
<div className="flex flex-col flex-1 w-full overflow-y-auto">
|
<div className="flex flex-col flex-1 w-full overflow-y-auto">
|
||||||
<main className="flex flex-col flex-1 my-0 h-full">
|
<main className="flex flex-col flex-1 my-0 h-full">{caught.status}</main>
|
||||||
{caught.status}
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import type { LoaderFunction, MetaFunction } from "@remix-run/node";
|
import type { LoaderFunction, MetaFunction } from "@remix-run/node";
|
||||||
import { useNavigate, useParams } from "@remix-run/react";
|
import { Link, useNavigate, useParams } from "@remix-run/react";
|
||||||
import { json, useLoaderData } from "superjson-remix";
|
import { json, useLoaderData } from "superjson-remix";
|
||||||
import { IoCall, IoChevronBack, IoInformationCircle } from "react-icons/io5";
|
import { IoCall, IoChevronBack } from "react-icons/io5";
|
||||||
|
import { parsePhoneNumber } from "awesome-phonenumber";
|
||||||
import { type Message, Prisma } from "@prisma/client";
|
import { type Message, Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import Conversation from "~/features/messages/components/conversation";
|
import Conversation from "~/features/messages/components/conversation";
|
||||||
import { getSeoMeta } from "~/utils/seo";
|
import { getSeoMeta } from "~/utils/seo";
|
||||||
import db from "~/utils/db.server";
|
import db from "~/utils/db.server";
|
||||||
import { parsePhoneNumber } from "awesome-phonenumber";
|
|
||||||
import { requireLoggedIn } from "~/utils/auth.server";
|
import { requireLoggedIn } from "~/utils/auth.server";
|
||||||
|
import newMessageAction from "~/features/messages/actions/messages.$recipient";
|
||||||
|
|
||||||
export const meta: MetaFunction = ({ params }) => {
|
export const meta: MetaFunction = ({ params }) => {
|
||||||
const recipient = decodeURIComponent(params.recipient ?? "");
|
const recipient = decodeURIComponent(params.recipient ?? "");
|
||||||
@ -21,6 +22,8 @@ export const meta: MetaFunction = ({ params }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const action = newMessageAction;
|
||||||
|
|
||||||
type ConversationType = {
|
type ConversationType = {
|
||||||
recipient: string;
|
recipient: string;
|
||||||
formattedPhoneNumber: string;
|
formattedPhoneNumber: string;
|
||||||
@ -70,20 +73,21 @@ export default function ConversationPage() {
|
|||||||
const { conversation } = useLoaderData<ConversationLoaderData>();
|
const { conversation } = useLoaderData<ConversationLoaderData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section className="h-full">
|
||||||
<header className="absolute top-0 w-screen h-12 backdrop-filter backdrop-blur-sm bg-white bg-opacity-75 border-b grid grid-cols-3 items-center">
|
<header className="absolute top-0 w-screen h-12 backdrop-filter backdrop-blur-sm bg-white bg-opacity-75 border-b items-center flex justify-between">
|
||||||
<span className="col-start-1 col-span-1 pl-2 cursor-pointer" onClick={() => navigate(-1)}>
|
<span className="pl-2 cursor-pointer" onClick={() => navigate(-1)}>
|
||||||
<IoChevronBack className="h-8 w-8" />
|
<IoChevronBack className="h-6 w-6" />
|
||||||
</span>
|
|
||||||
<strong className="col-span-1">{conversation?.formattedPhoneNumber ?? recipient}</strong>
|
|
||||||
<span className="col-span-1 flex justify-end space-x-4 pr-2">
|
|
||||||
<IoCall className="h-8 w-8" />
|
|
||||||
<IoInformationCircle className="h-8 w-8" />
|
|
||||||
</span>
|
</span>
|
||||||
|
<strong className="absolute right-0 left-0 text-center pointer-events-none">
|
||||||
|
{conversation?.formattedPhoneNumber ?? recipient}
|
||||||
|
</strong>
|
||||||
|
<Link to={`/outgoing-call/${encodeURI(recipient)}`} className="pr-2">
|
||||||
|
<IoCall className="h-6 w-6" />
|
||||||
|
</Link>
|
||||||
</header>
|
</header>
|
||||||
{/*<Suspense fallback={<div className="pt-12">Loading messages with {recipient}</div>}>*/}
|
{/*<Suspense fallback={<div className="pt-12">Loading messages with {recipient}</div>}>*/}
|
||||||
<Conversation />
|
<Conversation />
|
||||||
{/*</Suspense>*/}
|
{/*</Suspense>*/}
|
||||||
</>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { redirect, type Session } from "@remix-run/node";
|
import { redirect, type Session } from "@remix-run/node";
|
||||||
import type { FormStrategyVerifyParams } from "remix-auth-form";
|
import type { FormStrategyVerifyParams } from "remix-auth-form";
|
||||||
import SecurePassword from "secure-password";
|
import SecurePassword from "secure-password";
|
||||||
import type { MembershipRole, Organization, User } from "@prisma/client";
|
import type { MembershipRole, Organization, PhoneNumber, User } from "@prisma/client";
|
||||||
|
|
||||||
import db from "./db.server";
|
import db from "./db.server";
|
||||||
import logger from "./logger.server";
|
import logger from "./logger.server";
|
||||||
@ -12,10 +12,11 @@ import { commitSession, destroySession, getSession } from "./session.server";
|
|||||||
export type SessionOrganization = Pick<Organization, "id" | "twilioSubAccountSid" | "twilioAccountSid"> & {
|
export type SessionOrganization = Pick<Organization, "id" | "twilioSubAccountSid" | "twilioAccountSid"> & {
|
||||||
role: MembershipRole;
|
role: MembershipRole;
|
||||||
};
|
};
|
||||||
|
export type SessionPhoneNumber = Pick<PhoneNumber, "id" | "number">;
|
||||||
export type SessionUser = Omit<User, "hashedPassword"> & {
|
export type SessionUser = Omit<User, "hashedPassword"> & {
|
||||||
organizations: SessionOrganization[];
|
organizations: SessionOrganization[];
|
||||||
};
|
};
|
||||||
export type SessionData = SessionUser & { currentOrganization: SessionOrganization };
|
export type SessionData = SessionUser & { currentOrganization: SessionOrganization; currentPhoneNumber: SessionPhoneNumber };
|
||||||
|
|
||||||
const SP = new SecurePassword();
|
const SP = new SecurePassword();
|
||||||
|
|
||||||
@ -136,10 +137,11 @@ export async function requireLoggedOut(request: Request) {
|
|||||||
|
|
||||||
export async function requireLoggedIn(request: Request) {
|
export async function requireLoggedIn(request: Request) {
|
||||||
const user = await authenticator.isAuthenticated(request);
|
const user = await authenticator.isAuthenticated(request);
|
||||||
|
if (!user) {
|
||||||
const signInUrl = "/sign-in";
|
const signInUrl = "/sign-in";
|
||||||
const redirectTo = buildRedirectTo(new URL(request.url));
|
const redirectTo = buildRedirectTo(new URL(request.url));
|
||||||
const searchParams = new URLSearchParams({ redirectTo });
|
const searchParams = new URLSearchParams({ redirectTo });
|
||||||
if (!user) {
|
|
||||||
throw redirect(`${signInUrl}?${searchParams.toString()}`, {
|
throw redirect(`${signInUrl}?${searchParams.toString()}`, {
|
||||||
headers: { "Set-Cookie": await destroySession(await getSession(request)) },
|
headers: { "Set-Cookie": await destroySession(await getSession(request)) },
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user