diff --git a/src/components/billing/billing-plans.tsx b/src/components/billing/billing-plans.tsx index 1420da4..391c31b 100644 --- a/src/components/billing/billing-plans.tsx +++ b/src/components/billing/billing-plans.tsx @@ -26,7 +26,7 @@ type Form = { }; const BillingPlans: FunctionComponent = ({ activePlanId = FREE.id }) => { - const { userProfile } = useUser(); + const { customer } = useUser(); const { subscribe, changePlan } = useSubscription(); const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState( false, @@ -78,8 +78,8 @@ const BillingPlans: FunctionComponent = ({ activePlanId = FREE.id }) => { return; } - const email = userProfile!.email!; - const userId = userProfile!.id; + const email = customer!.email!; + const userId = customer!.id; const selectedPlanId = selectedPlan.id; const isMovingToPaidPlan = diff --git a/src/components/connected-layout.tsx b/src/components/connected-layout.tsx new file mode 100644 index 0000000..4df5c33 --- /dev/null +++ b/src/components/connected-layout.tsx @@ -0,0 +1,158 @@ +import type { FunctionComponent } from "react"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import { useAtom } from "jotai"; + +import { conversationsAtom, customerAtom, customerPhoneNumberAtom, messagesAtom, phoneCallsAtom } from "../state"; +import supabase from "../supabase/client"; +import type { Customer } from "../database/customer"; +import { PhoneNumber } from "../database/phone-number"; +import { Message } from "../database/message"; +import { PhoneCall } from "../database/phone-call"; + +type Props = {} + +const ConnectedLayout: FunctionComponent = ({ + children, +}) => { + useRequireOnboarding(); + const { isInitialized } = useInitializeState(); + + if (!isInitialized) { + return ( + <>Loading... + ); + } + + return ( + <> + {children} + + ); +}; + +export default ConnectedLayout; + +function useRequireOnboarding() { + const router = useRouter(); + const [customer] = useAtom(customerAtom); + + useEffect(() => { + (async () => { + if (!customer) { + // still loading + return; + } + + if (!customer.accountSid || !customer.authToken) { + return router.push("/welcome/step-two"); + } + + const phoneNumberResponse = await supabase + .from("phone-number") + .select("*") + .eq("customerId", customer.id) + .single(); + if (phoneNumberResponse.error) { + return router.push("/welcome/step-three"); + } + })(); + }, [customer, router]); +} + +function useInitializeState() { + useInitializeCustomer(); + useInitializeMessages(); + useInitializePhoneCalls(); + + const customer = useAtom(customerAtom)[0]; + const messages = useAtom(messagesAtom)[0]; + const phoneCalls = useAtom(phoneCallsAtom)[0]; + + return { + isInitialized: customer !== null && messages !== null && phoneCalls !== null, + }; +} + +function useInitializeCustomer() { + const router = useRouter(); + const setCustomer = useAtom(customerAtom)[1]; + const setCustomerPhoneNumber = useAtom(customerPhoneNumberAtom)[1]; + + useEffect(() => { + (async () => { + const redirectTo = `/auth/sign-in?redirectTo=${router.pathname}`; + // TODO: also redirect when no cookie + try { + await supabase.auth.refreshSession(); + } catch (error) { + console.error("session error", error); + return router.push(redirectTo); + } + const user = supabase.auth.user(); + if (!user) { + return router.push(redirectTo); + } + + const customerId = user.id; + const customerResponse = await supabase + .from("customer") + .select("*") + .eq("id", customerId) + .single(); + if (customerResponse.error) throw customerResponse.error; + + const customer = customerResponse.data; + setCustomer(customer); + + const customerPhoneNumberResponse = await supabase + .from("phone-number") + .select("*") + .eq("customerId", customerId) + .single(); + if (customerPhoneNumberResponse.error) throw customerPhoneNumberResponse.error; + setCustomerPhoneNumber(customerPhoneNumberResponse.data); + })(); + }, []); +} + +function useInitializeMessages() { + const customer = useAtom(customerAtom)[0]; + const setMessages = useAtom(messagesAtom)[1]; + + useEffect(() => { + (async () => { + if (!customer) { + return; + } + + const messagesResponse = await supabase + .from("message") + .select("*") + .eq("customerId", customer.id); + if (messagesResponse.error) throw messagesResponse.error; + setMessages(messagesResponse.data); + })(); + }, [customer, setMessages]); +} + + +function useInitializePhoneCalls() { + const customer = useAtom(customerAtom)[0]; + const setPhoneCalls = useAtom(phoneCallsAtom)[1]; + + useEffect(() => { + (async () => { + if (!customer) { + return; + } + + const phoneCallsResponse = await supabase + .from("phone-call") + .select("*") + .eq("customerId", customer.id); + if (phoneCallsResponse.error) throw phoneCallsResponse.error; + setPhoneCalls(phoneCallsResponse.data); + })(); + }, [customer, setPhoneCalls]); +} diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 3d28056..43b3ffd 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -8,7 +8,7 @@ import Avatar from "../avatar"; import useUser from "../../hooks/use-user"; export default function Header() { - const { userProfile } = useUser(); + const { customer } = useUser(); return (
diff --git a/src/components/settings/profile-informations.tsx b/src/components/settings/profile-informations.tsx index be99986..0b7ad80 100644 --- a/src/components/settings/profile-informations.tsx +++ b/src/components/settings/profile-informations.tsx @@ -30,9 +30,9 @@ const ProfileInformations: FunctionComponent = () => { const [errorMessage, setErrorMessage] = useState(""); useEffect(() => { - setValue("name", user.userProfile?.user_metadata.name ?? ""); - setValue("email", user.userProfile?.email ?? ""); - }, [setValue, user.userProfile]); + setValue("name", user.customer?.name ?? ""); + setValue("email", user.customer?.email ?? ""); + }, [setValue, user.customer]); const onSubmit = handleSubmit(async ({ name, email }) => { if (isSubmitting) { diff --git a/src/database/customer.ts b/src/database/customer.ts index f44e8a8..9dcda11 100644 --- a/src/database/customer.ts +++ b/src/database/customer.ts @@ -32,8 +32,6 @@ export async function createCustomer({ id, email, name }: CreateCustomerParams): if (error) throw error; - console.log("data", data); - return data![0]; } diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts index 64a0914..7c2788f 100644 --- a/src/hooks/use-auth.ts +++ b/src/hooks/use-auth.ts @@ -15,6 +15,7 @@ export default function useAuth() { useEffect(() => { const { data } = supabase.auth.onAuthStateChange(async (event, session) => { + console.log("event", event); if (["SIGNED_IN", "SIGNED_OUT"].includes(event)) { await axios.post("/api/auth/session", { event, session }); diff --git a/src/hooks/use-conversation.ts b/src/hooks/use-conversation.ts index 9d1994c..d87dbec 100644 --- a/src/hooks/use-conversation.ts +++ b/src/hooks/use-conversation.ts @@ -1,34 +1,39 @@ import { useMutation, useQuery, useQueryClient } from "react-query"; import axios from "axios"; +import { useAtom } from "jotai"; + import type { Message } from "../database/message"; import useUser from "./use-user"; +import { conversationsAtom, customerAtom, customerPhoneNumberAtom } from "../state"; +import { useEffect } from "react"; -type UseConversationParams = { - initialData?: Message[]; - recipient: string; -} - -export default function useConversation({ - initialData, - recipient, -}: UseConversationParams) { - const user = useUser(); +export default function useConversation(recipient: string) { + const customer = useAtom(customerAtom)[0]; + const customerPhoneNumber = useAtom(customerPhoneNumberAtom)[0]; const getConversationUrl = `/api/conversation/${encodeURIComponent(recipient)}`; const fetcher = async () => { const { data } = await axios.get(getConversationUrl); return data; }; const queryClient = useQueryClient(); - const getConversationQuery = useQuery( + const [conversations] = useAtom(conversationsAtom); + const getConversationQuery = useQuery( getConversationUrl, fetcher, { - initialData, + initialData: null, refetchInterval: false, refetchOnWindowFocus: false, }, ); + useEffect(() => { + const conversation = conversations[recipient]; + if (getConversationQuery.data?.length === 0) { + queryClient.setQueryData(getConversationUrl, conversation); + } + }, [queryClient, getConversationQuery.data, conversations, recipient, getConversationUrl]); + const sendMessage = useMutation( (sms: Pick) => axios.post(`/api/conversation/${sms.to}/send-message`, sms, { withCredentials: true }), { @@ -41,8 +46,8 @@ export default function useConversation({ ...previousMessages, { id: "", // TODO: somehow generate an id - from: "", // TODO: get user's phone number - customerId: user.userProfile!.id, + from: customerPhoneNumber!.phoneNumber, + customerId: customer!.id, sentAt: new Date().toISOString(), direction: "outbound", status: "queued", @@ -54,12 +59,6 @@ export default function useConversation({ return { previousMessages }; }, - onError: (error, variables, context) => { - if (context?.previousMessages) { - queryClient.setQueryData(getConversationUrl, context.previousMessages); - } - }, - onSettled: () => queryClient.invalidateQueries(getConversationUrl), }, ); diff --git a/src/hooks/use-user.ts b/src/hooks/use-user.ts index d4d202a..bcfeff0 100644 --- a/src/hooks/use-user.ts +++ b/src/hooks/use-user.ts @@ -1,11 +1,12 @@ -import { useContext } from "react"; import { useRouter } from "next/router"; import axios from "axios"; import type { User, UserAttributes } from "@supabase/supabase-js"; -import { SessionContext } from "../session-context"; import appLogger from "../../lib/logger"; import supabase from "../supabase/client"; +import { useAtom } from "jotai"; +import { customerAtom } from "../state"; +import { Customer } from "../database/customer"; const logger = appLogger.child({ module: "useUser" }); @@ -16,28 +17,27 @@ type UseUser = { | { isLoading: true; error: null; - userProfile: null; + customer: null; } | { isLoading: false; error: Error; - userProfile: User | null; + customer: Customer | null; } | { isLoading: false; error: null; - userProfile: User; + customer: Customer; } ); export default function useUser(): UseUser { - const session = useContext(SessionContext); + const [customer] = useAtom(customerAtom); const router = useRouter(); return { - isLoading: session.state.user === null, - userProfile: session.state.user, - error: session.state.error, + isLoading: customer === null, + customer, async deleteUser() { await axios.post("/api/user/delete-user", null, { withCredentials: true, diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index d22604a..53cf407 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -5,7 +5,6 @@ import { QueryClient, QueryClientProvider } from "react-query"; import { Hydrate } from "react-query/hydration"; import { pageTitle } from "./_document"; -import { SessionProvider } from "../session-context"; import "../fonts.css"; import "../tailwind.css"; @@ -21,16 +20,14 @@ const NextApp = (props: AppProps) => { return ( - - - - {pageTitle} - - - + + + {pageTitle} + + ); diff --git a/src/pages/api/conversation/[recipient]/index.ts b/src/pages/api/conversation/[recipient].ts similarity index 82% rename from src/pages/api/conversation/[recipient]/index.ts rename to src/pages/api/conversation/[recipient].ts index 99f1a01..3c4777e 100644 --- a/src/pages/api/conversation/[recipient]/index.ts +++ b/src/pages/api/conversation/[recipient].ts @@ -1,9 +1,9 @@ import Joi from "joi"; -import { withApiAuthRequired } from "../../../../../lib/session-helpers"; -import { findConversation } from "../../../../database/message"; -import type { ApiError } from "../../_types"; -import appLogger from "../../../../../lib/logger"; +import { withApiAuthRequired } from "../../../../lib/session-helpers"; +import { findConversation } from "../../../database/message"; +import type { ApiError } from "../_types"; +import appLogger from "../../../../lib/logger"; const logger = appLogger.child({ route: "/api/conversation" }); diff --git a/src/pages/calls.tsx b/src/pages/calls.tsx index 1829db1..a0de876 100644 --- a/src/pages/calls.tsx +++ b/src/pages/calls.tsx @@ -1,56 +1,37 @@ -import type { InferGetServerSidePropsType, NextPage } from "next"; +import type { NextPage } from "next"; -import { withPageOnboardingRequired } from "../../lib/session-helpers"; -import { findCustomerPhoneCalls } from "../database/phone-call"; -import useUser from "../hooks/use-user"; import Layout from "../components/layout"; +import ConnectedLayout from "../components/connected-layout"; +import { useAtom } from "jotai"; +import { phoneCallsAtom } from "../state"; -type Props = InferGetServerSidePropsType; +type Props = {}; const pageTitle = "Calls"; -const Calls: NextPage = ({ phoneCalls }) => { - const { userProfile } = useUser(); - - console.log("userProfile", userProfile); - - if (!userProfile) { - return Loading...; - } +const Calls: NextPage = () => { + const phoneCalls = useAtom(phoneCallsAtom)[0] ?? []; return ( - -
-

Calls page

-
    - {phoneCalls.map((phoneCall) => { - const recipient = phoneCall.direction === "outbound" ? phoneCall.to : phoneCall.from; - return ( -
  • -
    {recipient}
    -
    {new Date(phoneCall.createdAt).toLocaleString("fr-FR")}
    -
  • - ) - })} -
-
-
+ + +
+

Calls page

+
    + {phoneCalls.map((phoneCall) => { + const recipient = phoneCall.direction === "outbound" ? phoneCall.to : phoneCall.from; + return ( +
  • +
    {recipient}
    +
    {new Date(phoneCall.createdAt).toLocaleString("fr-FR")}
    +
  • + ) + })} +
+
+
+
); }; -export const getServerSideProps = withPageOnboardingRequired( - async ({ res }, user) => { - res.setHeader( - "Cache-Control", - "private, s-maxage=15, stale-while-revalidate=59", - ); - - const phoneCalls = await findCustomerPhoneCalls(user.id); - - return { - props: { phoneCalls }, - }; - }, -); - export default Calls; diff --git a/src/pages/keypad.tsx b/src/pages/keypad.tsx index 0d09dae..5d0a6aa 100644 --- a/src/pages/keypad.tsx +++ b/src/pages/keypad.tsx @@ -7,60 +7,58 @@ import { faBackspace, faPhoneAlt as faPhone } from "@fortawesome/pro-solid-svg-i import { withPageOnboardingRequired } from "../../lib/session-helpers"; import Layout from "../components/layout"; import useUser from "../hooks/use-user"; +import ConnectedLayout from "../components/connected-layout"; -type Props = InferGetServerSidePropsType; +type Props = {}; const pageTitle = "Keypad"; const Keypad: NextPage = () => { - const { userProfile } = useUser(); const phoneNumber = useAtom(phoneNumberAtom)[0]; const pressBackspace = useAtom(pressBackspaceAtom)[1]; - if (!userProfile) { - return Loading...; - } - return ( - -
-
- {phoneNumber} -
+ + +
+
+ {phoneNumber} +
-
- - - ABC - DEF - - - GHI - JKL - MNO - - - PQRS - TUV - WXYZ - - - - - - - -
- -
-
- -
-
-
-
-
+
+ + + ABC + DEF + + + GHI + JKL + MNO + + + PQRS + TUV + WXYZ + + + + + + + +
+ +
+
+ +
+
+
+
+
+ ); }; @@ -118,13 +116,4 @@ const pressBackspaceAtom = atom( }, ); -export const getServerSideProps = withPageOnboardingRequired(({ res }) => { - res.setHeader( - "Cache-Control", - "private, s-maxage=15, stale-while-revalidate=59", - ); - - return { props: {} }; -}); - export default Keypad; diff --git a/src/pages/messages/[recipient].tsx b/src/pages/messages/[recipient].tsx index 573a731..d5da3b2 100644 --- a/src/pages/messages/[recipient].tsx +++ b/src/pages/messages/[recipient].tsx @@ -10,31 +10,33 @@ import { import clsx from "clsx"; import { useForm } from "react-hook-form"; -import { withPageOnboardingRequired } from "../../../lib/session-helpers"; import type { Message } from "../../database/message"; -import { findConversation } from "../../database/message"; import supabase from "../../supabase/client"; import useUser from "../../hooks/use-user"; import useConversation from "../../hooks/use-conversation"; import Layout from "../../components/layout"; +import ConnectedLayout from "../../components/connected-layout"; -type Props = { - recipient: string; - conversation: Message[]; -} +type Props = {} type Form = { content: string; } const Messages: NextPage = (props) => { - const { userProfile } = useUser(); + const { customer } = useUser(); const router = useRouter(); const recipient = router.query.recipient as string; - const { conversation, error, refetch, sendMessage } = useConversation({ - initialData: props.conversation, - recipient, - }); + useEffect(() => { + if (!router.isReady) { + return; + } + + if (!recipient || Array.isArray(recipient)) { + router.push("/messages"); + } + }, [recipient, router]); + const { conversation, error, refetch, sendMessage } = useConversation(recipient); const formRef = useRef(null); const { register, @@ -58,12 +60,12 @@ const Messages: NextPage = (props) => { }); useEffect(() => { - if (!userProfile) { + if (!customer) { return; } const subscription = supabase - .from(`sms:customerId=eq.${userProfile.id}`) + .from(`sms:customerId=eq.${customer.id}`) .on("INSERT", (payload) => { const message = payload.new; if ([message.from, message.to].includes(recipient)) { @@ -73,117 +75,99 @@ const Messages: NextPage = (props) => { .subscribe(); return () => void subscription.unsubscribe(); - }, [userProfile, recipient, refetch]); + }, [customer, recipient, refetch]); useEffect(() => { if (formRef.current) { formRef.current.scrollIntoView(); } - }, []); - - if (!userProfile) { - return ( - - Loading... - - ); - } + }, [conversation]); if (error) { console.error("error", error); return ( - - Oops, something unexpected happened. Please try reloading the page. - + + + Oops, something unexpected happened. Please try reloading the page. + + ); } - console.log("conversation", conversation); + if (!conversation) { + return ( + + + Loading... + + + ); + } return ( - -
- - - - - {recipient} - - - - - -
-
-
    - {conversation!.map((message, index) => { - const isOutbound = message.direction === "outbound"; - const nextMessage = conversation![index + 1]; - const previousMessage = conversation![index - 1]; - const isSameNext = message.from === nextMessage?.from; - const isSamePrevious = message.from === previousMessage?.from; - const differenceInMinutes = previousMessage ? (new Date(message.sentAt).getTime() - new Date(previousMessage.sentAt).getTime()) / 1000 / 60 : 0; - const isTooLate = differenceInMinutes > 15; - return ( -
  • - { - (!isSamePrevious || isTooLate) && ( -
    - {new Date(message.sentAt).toLocaleDateString("fr-FR", { weekday: "long", day: "2-digit", month: "short" })} - {new Date(message.sentAt).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })} -
    - ) - } + + +
    + + + + + {recipient} + + + + + +
    +
    +
      + {conversation.map((message, index) => { + const isOutbound = message.direction === "outbound"; + const nextMessage = conversation![index + 1]; + const previousMessage = conversation![index - 1]; + const isSameNext = message.from === nextMessage?.from; + const isSamePrevious = message.from === previousMessage?.from; + const differenceInMinutes = previousMessage ? (new Date(message.sentAt).getTime() - new Date(previousMessage.sentAt).getTime()) / 1000 / 60 : 0; + const isTooLate = differenceInMinutes > 15; + console.log("message.from === previousMessage?.from", message.from, previousMessage?.from); + return ( +
    • + { + (!isSamePrevious || isTooLate) && ( +
      + {new Date(message.sentAt).toLocaleDateString("fr-FR", { weekday: "long", day: "2-digit", month: "short" })} + {new Date(message.sentAt).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })} +
      + ) + } -
      - - {message.content} - -
      -
    • - ); - })} -
    -
    -
    - - - -
    + + {message.content} + +
+ + ); + })} + + +
+ + + +
+ ); }; -export const getServerSideProps = withPageOnboardingRequired( - async (context, user) => { - const recipient = context.params?.recipient; - if (!recipient || Array.isArray(recipient)) { - return { - redirect: { - destination: "/messages", - permanent: false, - }, - }; - } - - const conversation = await findConversation(user.id, recipient); - - return { - props: { - recipient, - conversation, - }, - }; - }, -); - export default Messages; diff --git a/src/pages/messages/index.tsx b/src/pages/messages/index.tsx index a1e559b..0fcf6d0 100644 --- a/src/pages/messages/index.tsx +++ b/src/pages/messages/index.tsx @@ -8,85 +8,44 @@ import { findCustomer } from "../../database/customer"; import { decrypt } from "../../database/_encryption"; import useUser from "../../hooks/use-user"; import Layout from "../../components/layout"; +import ConnectedLayout from "../../components/connected-layout"; +import { conversationsAtom } from "../../state"; +import { useAtom } from "jotai"; -type Props = InferGetServerSidePropsType; +type Props = {}; const pageTitle = "Messages"; -const Messages: NextPage = ({ conversations }) => { - const { userProfile } = useUser(); - - if (!userProfile) { - return Loading...; - } +const Messages: NextPage = () => { + const [conversations] = useAtom(conversationsAtom); return ( - -
-

Messages page

- -
-
+ + +
+

Messages page

+ +
+
+
); }; -type Recipient = string; - -export const getServerSideProps = withPageOnboardingRequired( - async (context, user) => { - context.res.setHeader( - "Cache-Control", - "private, s-maxage=15, stale-while-revalidate=59", - ); - - const [customer, messages] = await Promise.all([ - findCustomer(user.id), - findCustomerMessages(user.id), - ]); - - let conversations: Record = {}; - for (const message of messages) { - let recipient: string; - if (message.direction === "outbound") { - recipient = message.to; - } else { - recipient = message.from; - } - - if ( - !conversations[recipient] || - message.sentAt > conversations[recipient].sentAt - ) { - conversations[recipient] = { - ...message, - content: decrypt(message.content, customer.encryptionKey), // TODO: should probably decrypt on the phone - }; - } - } - conversations = Object.fromEntries( - Object.entries(conversations).sort(([,a], [,b]) => b.sentAt.localeCompare(a.sentAt)) - ); - - return { - props: { conversations }, - }; - }, -); - export default Messages; diff --git a/src/pages/settings/account.tsx b/src/pages/settings/account.tsx index f09994d..a701108 100644 --- a/src/pages/settings/account.tsx +++ b/src/pages/settings/account.tsx @@ -9,14 +9,11 @@ import Divider from "../../components/divider"; import UpdatePassword from "../../components/settings/update-password"; import DangerZone from "../../components/settings/danger-zone"; import { withPageOnboardingRequired } from "../../../lib/session-helpers"; +import ConnectedLayout from "../../components/connected-layout"; const Account: NextPage = () => { const user = useUser(); - if (user.isLoading) { - return Loading...; - } - if (user.error !== null) { return ( @@ -32,23 +29,25 @@ const Account: NextPage = () => { } return ( - -
- + + +
+ -
- +
+ +
+ + + +
+ +
+ +
- - - -
- -
- - -
-
+ +
); }; diff --git a/src/pages/settings/billing.tsx b/src/pages/settings/billing.tsx index 8a3e9ca..3e2b47e 100644 --- a/src/pages/settings/billing.tsx +++ b/src/pages/settings/billing.tsx @@ -14,6 +14,7 @@ import type { Subscription } from "../../database/subscriptions"; import { findUserSubscription } from "../../database/subscriptions"; import appLogger from "../../../lib/logger"; +import ConnectedLayout from "../../components/connected-layout"; const logger = appLogger.child({ page: "/account/settings/billing" }); @@ -31,51 +32,53 @@ const Billing: NextPage = ({ subscription }) => { const { cancelSubscription, updatePaymentMethod } = useSubscription(); return ( - -
- {subscription ? ( - <> - - - updatePaymentMethod({ - updateUrl: subscription.updateUrl, - }) - } - text="Update payment method on Paddle" - /> - + + +
+ {subscription ? ( + <> + + + updatePaymentMethod({ + updateUrl: subscription.updateUrl, + }) + } + text="Update payment method on Paddle" + /> + -
- -
+
+ +
+ + + + +
+ +
+ + + + cancelSubscription({ + cancelUrl: subscription.cancelUrl, + }) + } + text="Cancel subscription on Paddle" + /> + + + ) : ( - + - -
- -
- - - - cancelSubscription({ - cancelUrl: subscription.cancelUrl, - }) - } - text="Cancel subscription on Paddle" - /> - - - ) : ( - - - - )} -
-
+ )} +
+
+ ); }; diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx index 595514c..1cc1fb5 100644 --- a/src/pages/settings/index.tsx +++ b/src/pages/settings/index.tsx @@ -6,8 +6,9 @@ import Layout from "../../components/layout"; import { withPageOnboardingRequired } from "../../../lib/session-helpers"; import appLogger from "../../../lib/logger"; +import ConnectedLayout from "../../components/connected-layout"; -type Props = InferGetServerSidePropsType; +type Props = {}; const logger = appLogger.child({ page: "/account/settings" }); @@ -28,36 +29,27 @@ const navigation = [ const Settings: NextPage = (props) => { return ( - -
- -
-
+ + +
+ +
+
+
); }; -export const getServerSideProps = withPageOnboardingRequired( - async ({ res }) => { - res.setHeader( - "Cache-Control", - "private, s-maxage=15, stale-while-revalidate=59", - ); - - return { props: {} }; - }, -); - export default Settings; diff --git a/src/session-context.tsx b/src/session-context.tsx deleted file mode 100644 index 2a8eb3c..0000000 --- a/src/session-context.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import type { Dispatch, ReactNode, Reducer, ReducerAction } from "react"; -import { createContext, useEffect, useReducer } from "react"; -import type { User } from "@supabase/supabase-js"; -import supabase from "./supabase/client"; - -type Context = { - state: SessionState; - dispatch: Dispatch>; -}; - -export const SessionContext = createContext(null as any); - -type ProviderProps = { - children: ReactNode; - user?: User | null; -}; - -function getInitialState(initialUser: User | null | undefined): SessionState { - if (!initialUser) { - return { - state: "LOADING", - user: null, - error: null, - }; - } - - return { - state: "SUCCESS", - user: initialUser, - error: null, - }; -} - -export function SessionProvider({ children, user }: ProviderProps) { - const [state, dispatch] = useReducer( - sessionReducer, - getInitialState(user), - ); - - useEffect(() => { - supabase.auth.onAuthStateChange((event, session) => { - console.log("event", event); - if (["SIGNED_IN", "USER_UPDATED"].includes(event)) { - dispatch({ - type: "SET_SESSION", - user: session!.user!, - }); - } - }); - - if (state.user === null) { - dispatch({ - type: "SET_SESSION", - user: supabase.auth.user()!, - }); - } - }, []); - - return ( - - {children} - - ); -} - -type SessionState = - | { - state: "LOADING"; - user: null; - error: null; - } - | { - state: "SUCCESS"; - user: User; - error: null; - } - | { - state: "ERROR"; - user: User | null; - error: Error; - }; - -type Action = - | { type: "SET_SESSION"; user: User } - | { type: "THROW_ERROR"; error: Error }; - -const sessionReducer: Reducer = (state, action) => { - switch (action.type) { - case "SET_SESSION": - return { - ...state, - state: "SUCCESS", - user: action.user, - error: null, - }; - case "THROW_ERROR": - return { - ...state, - state: "ERROR", - error: action.error, - }; - default: - throw new Error("unreachable"); - } -}; diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..ae04a7a --- /dev/null +++ b/src/state.ts @@ -0,0 +1,50 @@ +import { atom } from "jotai"; +import type { Message } from "./database/message"; +import type { Customer } from "./database/customer"; +import type { PhoneCall } from "./database/phone-call"; +import type { PhoneNumber } from "./database/phone-number"; +import { decrypt } from "./database/_encryption"; + +type Recipient = string; + +export const customerAtom = atom(null); +export const customerPhoneNumberAtom = atom(null); + +export const messagesAtom = atom(null); +export const conversationsAtom = atom>( + (get) => { + const messages = get(messagesAtom); + const customer = get(customerAtom); + if (!customer || !messages) { + return {}; + } + + let conversations: Record = {}; + for (const message of messages) { + let recipient: string; + if (message.direction === "outbound") { + recipient = message.to; + } else { + recipient = message.from; + } + + if (!conversations[recipient]) { + conversations[recipient] = []; + } + + conversations[recipient].push({ + ...message, + content: decrypt(message.content, customer.encryptionKey), + }); + + conversations[recipient].sort((a, b) => a.sentAt.localeCompare(b.sentAt)); + } + conversations = Object.fromEntries( + Object.entries(conversations).sort(([,a], [,b]) => b[b.length - 1].sentAt.localeCompare(a[a.length - 1].sentAt)) + ); + + return conversations; + }, +); + +export const phoneCallsAtom = atom(null);