store twilio stuff in TwilioAccount table and remodel session data
This commit is contained in:
@ -8,19 +8,14 @@ import getTwilioClient, { translateMessageDirection, translateMessageStatus } fr
|
||||
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 { phoneNumber, twilioAccount } = await requireLoggedIn(request);
|
||||
if (!twilioAccount) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
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 });
|
||||
const twilioClient = getTwilioClient(twilioAccount);
|
||||
try {
|
||||
console.log({ twilioAccountSid, twilioSubAccountSid });
|
||||
console.log({
|
||||
body: formData.content.toString(),
|
||||
to: recipient,
|
||||
|
@ -10,7 +10,7 @@ import { type ConversationLoaderData } from "~/routes/__app/messages.$recipient"
|
||||
import useSession from "~/features/core/hooks/use-session";
|
||||
|
||||
export default function Conversation() {
|
||||
const { currentPhoneNumber } = useSession();
|
||||
const { phoneNumber } = useSession();
|
||||
const params = useParams<{ recipient: string }>();
|
||||
const recipient = decodeURIComponent(params.recipient ?? "");
|
||||
const { conversation } = useLoaderData<ConversationLoaderData>();
|
||||
@ -21,15 +21,15 @@ export default function Conversation() {
|
||||
if (transition.submission) {
|
||||
messages.push({
|
||||
id: "temp",
|
||||
phoneNumberId: currentPhoneNumber.id,
|
||||
from: currentPhoneNumber.number,
|
||||
phoneNumberId: phoneNumber!.id,
|
||||
from: phoneNumber!.number,
|
||||
to: recipient,
|
||||
sentAt: new Date(),
|
||||
direction: Direction.Outbound,
|
||||
|
||||
status: "Queued",
|
||||
content: transition.submission.formData.get("content")!.toString()
|
||||
})
|
||||
content: transition.submission.formData.get("content")!.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -91,7 +91,7 @@ export default function Conversation() {
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<NewMessageArea />
|
||||
<NewMessageArea recipient={recipient} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ export default function NewMessageBottomSheet() {
|
||||
onClose={() => setIsOpen(false)}
|
||||
snapPoints={[0.5]}
|
||||
>
|
||||
<BottomSheet.Container>
|
||||
<BottomSheet.Header>
|
||||
<BottomSheet.Container onViewportBoxUpdate={null}>
|
||||
<BottomSheet.Header onViewportBoxUpdate={null}>
|
||||
<div className="w-full flex items-center justify-center p-4 text-black relative">
|
||||
<span className="font-semibold text-base">New Message</span>
|
||||
|
||||
@ -30,7 +30,7 @@ export default function NewMessageBottomSheet() {
|
||||
</button>
|
||||
</div>
|
||||
</BottomSheet.Header>
|
||||
<BottomSheet.Content>
|
||||
<BottomSheet.Content onViewportBoxUpdate={null}>
|
||||
<main className="flex flex-col h-full overflow-hidden">
|
||||
<div className="flex items-center p-4 border-t border-b">
|
||||
<span className="mr-4 text-[#333]">To:</span>
|
||||
@ -48,7 +48,7 @@ export default function NewMessageBottomSheet() {
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Container>
|
||||
|
||||
<BottomSheet.Backdrop onTap={() => setIsOpen(false)} />
|
||||
<BottomSheet.Backdrop onViewportBoxUpdate={null} onTap={() => setIsOpen(false)} />
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import type { LoaderFunction } from "@remix-run/node";
|
||||
import { json } from "superjson-remix";
|
||||
import { parsePhoneNumber } from "awesome-phonenumber";
|
||||
import { type Message, Prisma, Direction, SubscriptionStatus } from "@prisma/client";
|
||||
import { type Message, Prisma, Direction } from "@prisma/client";
|
||||
|
||||
import db from "~/utils/db.server";
|
||||
import { requireLoggedIn } from "~/utils/auth.server";
|
||||
import { requireLoggedIn, type SessionData } from "~/utils/auth.server";
|
||||
|
||||
export type MessagesLoaderData = {
|
||||
user: { hasPhoneNumber: boolean };
|
||||
@ -18,95 +18,58 @@ type Conversation = {
|
||||
};
|
||||
|
||||
const loader: LoaderFunction = async ({ request }) => {
|
||||
const { id, organizations } = await requireLoggedIn(request);
|
||||
const user = await db.user.findFirst({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
fullName: true,
|
||||
email: true,
|
||||
role: true,
|
||||
memberships: {
|
||||
include: {
|
||||
organization: {
|
||||
include: {
|
||||
subscriptions: {
|
||||
where: {
|
||||
OR: [
|
||||
{ status: { not: SubscriptionStatus.deleted } },
|
||||
{
|
||||
status: SubscriptionStatus.deleted,
|
||||
cancellationEffectiveDate: { gt: new Date() },
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: { lastEventTime: Prisma.SortOrder.desc },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const organization = user!.memberships[0].organization;
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { organizationId_isCurrent: { organizationId: organization.id, isCurrent: true } },
|
||||
select: {
|
||||
id: true,
|
||||
organizationId: true,
|
||||
number: true,
|
||||
},
|
||||
});
|
||||
const conversations = await getConversations();
|
||||
|
||||
const sessionData = await requireLoggedIn(request);
|
||||
return json<MessagesLoaderData>({
|
||||
user: { hasPhoneNumber: Boolean(phoneNumber) },
|
||||
conversations,
|
||||
user: { hasPhoneNumber: Boolean(sessionData.phoneNumber) },
|
||||
conversations: await getConversations(sessionData.phoneNumber),
|
||||
});
|
||||
|
||||
async function getConversations() {
|
||||
const organizationId = organizations[0].id;
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { organizationId_isCurrent: { organizationId, isCurrent: true } },
|
||||
});
|
||||
if (!phoneNumber || phoneNumber.isFetchingMessages) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = await db.message.findMany({
|
||||
where: { phoneNumberId: phoneNumber.id },
|
||||
orderBy: { sentAt: Prisma.SortOrder.desc },
|
||||
});
|
||||
|
||||
let conversations: Record<string, Conversation> = {};
|
||||
for (const message of messages) {
|
||||
let recipient: string;
|
||||
if (message.direction === Direction.Outbound) {
|
||||
recipient = message.to;
|
||||
} else {
|
||||
recipient = message.from;
|
||||
}
|
||||
const formattedPhoneNumber = parsePhoneNumber(recipient).getNumber("international");
|
||||
|
||||
if (!conversations[recipient]) {
|
||||
conversations[recipient] = {
|
||||
recipient,
|
||||
formattedPhoneNumber,
|
||||
lastMessage: message,
|
||||
};
|
||||
}
|
||||
|
||||
if (message.sentAt > conversations[recipient].lastMessage.sentAt) {
|
||||
conversations[recipient].lastMessage = message;
|
||||
}
|
||||
/*conversations[recipient]!.messages.push({
|
||||
...message,
|
||||
content: decrypt(message.content, organization.encryptionKey),
|
||||
});*/
|
||||
}
|
||||
|
||||
return conversations;
|
||||
}
|
||||
};
|
||||
|
||||
export default loader;
|
||||
|
||||
async function getConversations(sessionPhoneNumber: SessionData["phoneNumber"]) {
|
||||
if (!sessionPhoneNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { id: sessionPhoneNumber.id },
|
||||
});
|
||||
if (!phoneNumber || phoneNumber.isFetchingMessages) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = await db.message.findMany({
|
||||
where: { phoneNumberId: phoneNumber.id },
|
||||
orderBy: { sentAt: Prisma.SortOrder.desc },
|
||||
});
|
||||
|
||||
let conversations: Record<string, Conversation> = {};
|
||||
for (const message of messages) {
|
||||
let recipient: string;
|
||||
if (message.direction === Direction.Outbound) {
|
||||
recipient = message.to;
|
||||
} else {
|
||||
recipient = message.from;
|
||||
}
|
||||
const formattedPhoneNumber = parsePhoneNumber(recipient).getNumber("international");
|
||||
|
||||
if (!conversations[recipient]) {
|
||||
conversations[recipient] = {
|
||||
recipient,
|
||||
formattedPhoneNumber,
|
||||
lastMessage: message,
|
||||
};
|
||||
}
|
||||
|
||||
if (message.sentAt > conversations[recipient].lastMessage.sentAt) {
|
||||
conversations[recipient].lastMessage = message;
|
||||
}
|
||||
/*conversations[recipient]!.messages.push({
|
||||
...message,
|
||||
content: decrypt(message.content, organization.encryptionKey),
|
||||
});*/
|
||||
}
|
||||
|
||||
return conversations;
|
||||
}
|
||||
|
@ -19,23 +19,25 @@ const action: ActionFunction = async ({ request }) => {
|
||||
}
|
||||
|
||||
switch (formData._action as Action) {
|
||||
case "deleteUser":
|
||||
return deleteUser(request);
|
||||
case "changePassword":
|
||||
return changePassword(request, formData);
|
||||
case "updateUser":
|
||||
return updateUser(request, formData);
|
||||
default:
|
||||
const errorMessage = `POST /settings with an invalid _action=${formData._action}`;
|
||||
logger.error(errorMessage);
|
||||
return badRequest({ errorMessage });
|
||||
case "deleteUser":
|
||||
return deleteUser(request);
|
||||
case "changePassword":
|
||||
return changePassword(request, formData);
|
||||
case "updateUser":
|
||||
return updateUser(request, formData);
|
||||
default:
|
||||
const errorMessage = `POST /settings with an invalid _action=${formData._action}`;
|
||||
logger.error(errorMessage);
|
||||
return badRequest({ errorMessage });
|
||||
}
|
||||
};
|
||||
|
||||
export default action;
|
||||
|
||||
async function deleteUser(request: Request) {
|
||||
const { id } = await requireLoggedIn(request);
|
||||
const {
|
||||
user: { id },
|
||||
} = await requireLoggedIn(request);
|
||||
|
||||
await db.user.update({
|
||||
where: { id },
|
||||
@ -64,7 +66,9 @@ async function changePassword(request: Request, formData: unknown) {
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = await requireLoggedIn(request);
|
||||
const {
|
||||
user: { id },
|
||||
} = await requireLoggedIn(request);
|
||||
const user = await db.user.findUnique({ where: { id } });
|
||||
const { currentPassword, newPassword } = validation.data;
|
||||
const verificationResult = await verifyPassword(user!.hashedPassword!, currentPassword);
|
||||
@ -99,7 +103,7 @@ async function updateUser(request: Request, formData: unknown) {
|
||||
});
|
||||
}
|
||||
|
||||
const user = await requireLoggedIn(request);
|
||||
const { user } = await requireLoggedIn(request);
|
||||
const { email, fullName } = validation.data;
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
|
@ -5,14 +5,14 @@ import { z } from "zod";
|
||||
import db from "~/utils/db.server";
|
||||
import { type FormError, validate } from "~/utils/validation.server";
|
||||
import { requireLoggedIn } from "~/utils/auth.server";
|
||||
import setTwilioWebhooksQueue from "~/queues/set-twilio-webhooks.server";
|
||||
|
||||
type SetPhoneNumberFailureActionData = { errors: FormError<typeof bodySchema>; submitted?: never };
|
||||
type SetPhoneNumberSuccessfulActionData = { errors?: never; submitted: true };
|
||||
export type SetPhoneNumberActionData = SetPhoneNumberFailureActionData | SetPhoneNumberSuccessfulActionData;
|
||||
|
||||
const action: ActionFunction = async ({ request }) => {
|
||||
const { organizations } = await requireLoggedIn(request);
|
||||
const organization = organizations[0];
|
||||
const { organization } = await requireLoggedIn(request);
|
||||
const formData = Object.fromEntries(await request.formData());
|
||||
const validation = validate(bodySchema, formData);
|
||||
if (validation.errors) {
|
||||
@ -35,6 +35,11 @@ const action: ActionFunction = async ({ request }) => {
|
||||
where: { id: validation.data.phoneNumberSid },
|
||||
data: { isCurrent: true },
|
||||
});
|
||||
await setTwilioWebhooksQueue.add(`set twilio webhooks for phoneNumberId=${validation.data.phoneNumberSid}`, {
|
||||
phoneNumberId: validation.data.phoneNumberSid,
|
||||
organizationId: organization.id,
|
||||
});
|
||||
console.log("queued");
|
||||
|
||||
return json<SetPhoneNumberActionData>({ submitted: true });
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ import Button from "../button";
|
||||
import SettingsSection from "../settings-section";
|
||||
|
||||
const ProfileInformations: FunctionComponent = () => {
|
||||
const user = useSession();
|
||||
const { user } = useSession();
|
||||
const transition = useTransition();
|
||||
const actionData = useActionData<UpdateUserActionData>()?.updateUser;
|
||||
|
||||
|
@ -10,7 +10,7 @@ import type { SetPhoneNumberActionData } from "~/features/settings/actions/phone
|
||||
export default function PhoneNumberForm() {
|
||||
const transition = useTransition();
|
||||
const actionData = useActionData<SetPhoneNumberActionData>();
|
||||
const { currentOrganization } = useSession();
|
||||
const { twilioAccount } = useSession();
|
||||
const availablePhoneNumbers = useLoaderData<PhoneSettingsLoaderData>().phoneNumbers;
|
||||
|
||||
const isSubmitting = transition.state === "submitting";
|
||||
@ -18,8 +18,8 @@ export default function PhoneNumberForm() {
|
||||
const errors = actionData?.errors;
|
||||
const topErrorMessage = errors?.general ?? errors?.phoneNumberSid;
|
||||
const isError = typeof topErrorMessage !== "undefined";
|
||||
const currentPhoneNumber = availablePhoneNumbers.find(phoneNumber => phoneNumber.isCurrent === true);
|
||||
const hasFilledTwilioCredentials = Boolean(currentOrganization.twilioAccountSid)
|
||||
const currentPhoneNumber = availablePhoneNumbers.find((phoneNumber) => phoneNumber.isCurrent === true);
|
||||
const hasFilledTwilioCredentials = twilioAccount !== null;
|
||||
|
||||
if (!hasFilledTwilioCredentials) {
|
||||
return null;
|
||||
|
@ -6,7 +6,7 @@ import SettingsSection from "../settings-section";
|
||||
import useSession from "~/features/core/hooks/use-session";
|
||||
|
||||
export default function TwilioConnect() {
|
||||
const { currentOrganization } = useSession();
|
||||
const { twilioAccount } = useSession();
|
||||
const [isHelpModalOpen, setIsHelpModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@ -20,7 +20,7 @@ export default function TwilioConnect() {
|
||||
Shellphone needs to connect to your Twilio account to securely use your phone numbers.
|
||||
</article>
|
||||
|
||||
{currentOrganization.twilioAccountSid === null ? (
|
||||
{twilioAccount === null ? (
|
||||
<a
|
||||
href="https://www.twilio.com/authorize/CN01675d385a9ee79e6aa58adf54abe3b3"
|
||||
rel="noopener noreferrer"
|
||||
|
Reference in New Issue
Block a user