diff --git a/app/auth/mutations/login.ts b/app/auth/mutations/login.ts index 9a09a7d..5afb76d 100644 --- a/app/auth/mutations/login.ts +++ b/app/auth/mutations/login.ts @@ -40,16 +40,9 @@ export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ct const user = await authenticateUser(email, password); const organization = user.memberships[0]!.organization; - const hasCompletedOnboarding = - Boolean(organization.twilioAccountSid) && - Boolean(organization.twilioAuthToken) && - Boolean(organization.twilioApiKey) && - Boolean(organization.twilioApiSecret) && - Boolean(organization.phoneNumbers.length > 1); await ctx.session.$create({ userId: user.id, roles: [user.role, user.memberships[0]!.role], - hasCompletedOnboarding: hasCompletedOnboarding || undefined, orgId: organization.id, }); diff --git a/app/auth/mutations/signup.ts b/app/auth/mutations/signup.ts index de100a1..11a3f8e 100644 --- a/app/auth/mutations/signup.ts +++ b/app/auth/mutations/signup.ts @@ -31,6 +31,7 @@ export default resolver.pipe(resolver.zod(Signup), async ({ email, password, ful userId: user.id, roles: [user.role, user.memberships[0]!.role], orgId: user.memberships[0]!.organizationId, + shouldShowWelcomeMessage: true, }); return user; }); diff --git a/app/auth/pages/sign-up.tsx b/app/auth/pages/sign-up.tsx index 1494629..449371f 100644 --- a/app/auth/pages/sign-up.tsx +++ b/app/auth/pages/sign-up.tsx @@ -29,7 +29,7 @@ const SignUp: BlitzPage = () => { onSubmit={async (values) => { try { await signupMutation(values); - router.push(Routes.StepOne()); + await router.push(Routes.Welcome()); } catch (error: any) { if (error.code === "P2002" && error.meta?.target?.includes("email")) { // This error comes from Prisma @@ -47,7 +47,13 @@ const SignUp: BlitzPage = () => { ); }; -SignUp.redirectAuthenticatedTo = Routes.StepOne(); +SignUp.redirectAuthenticatedTo = ({ session }) => { + if (session.shouldShowWelcomeMessage) { + return Routes.Welcome(); + } + + return Routes.Messages(); +}; SignUp.getLayout = (page) => {page}; diff --git a/app/auth/pages/welcome.tsx b/app/auth/pages/welcome.tsx new file mode 100644 index 0000000..218a9c5 --- /dev/null +++ b/app/auth/pages/welcome.tsx @@ -0,0 +1,28 @@ +import type { BlitzPage, GetServerSideProps } from "blitz"; +import { getSession, Routes, useRouter } from "blitz"; + +const Welcome: BlitzPage = () => { + const router = useRouter(); + + return ( +
+

Thanks for joining Shellphone

+

Let us know if you need our help

+

Make sure to set up your phone number

+ +
+ ); +}; + +Welcome.authenticate = { redirectTo: Routes.SignIn() }; + +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const session = await getSession(req, res); + await session.$setPublicData({ shouldShowWelcomeMessage: undefined }); + + return { + props: {}, + }; +}; + +export default Welcome; diff --git a/app/core/components/missing-twilio-credentials.tsx b/app/core/components/missing-twilio-credentials.tsx new file mode 100644 index 0000000..06f81b0 --- /dev/null +++ b/app/core/components/missing-twilio-credentials.tsx @@ -0,0 +1,28 @@ +import { Routes, useRouter } from "blitz"; +import { IoSettings, IoAlertCircleOutline } from "react-icons/io5"; + +export default function MissingTwilioCredentials() { + const router = useRouter(); + + return ( +
+
+ ); +} diff --git a/app/settings/components/modal.tsx b/app/core/components/modal.tsx similarity index 100% rename from app/settings/components/modal.tsx rename to app/core/components/modal.tsx diff --git a/app/core/components/page-title.tsx b/app/core/components/page-title.tsx new file mode 100644 index 0000000..79e3552 --- /dev/null +++ b/app/core/components/page-title.tsx @@ -0,0 +1,17 @@ +import type { FunctionComponent } from "react"; +import clsx from "clsx"; + +type Props = { + className?: string; + title: string; +}; + +const PageTitle: FunctionComponent = ({ className, title }) => { + return ( +
+

{title}

+
+ ); +}; + +export default PageTitle; diff --git a/app/core/hooks/use-current-user.ts b/app/core/hooks/use-current-user.ts index e480f21..7331a6c 100644 --- a/app/core/hooks/use-current-user.ts +++ b/app/core/hooks/use-current-user.ts @@ -9,7 +9,9 @@ export default function useCurrentUser() { return { user, organization, - hasFilledTwilioCredentials: Boolean(user && organization?.twilioAccountSid && organization?.twilioAuthToken), - hasCompletedOnboarding: session.hasCompletedOnboarding, + hasFilledTwilioCredentials: Boolean( + organization && organization.twilioAccountSid && organization.twilioAuthToken, + ), + hasActiveSubscription: organization && organization.subscriptions.length > 0, }; } diff --git a/app/core/hooks/use-require-onboarding.ts b/app/core/hooks/use-require-onboarding.ts deleted file mode 100644 index 8e75d40..0000000 --- a/app/core/hooks/use-require-onboarding.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Routes, useRouter } from "blitz"; - -import useCurrentUser from "./use-current-user"; -import useCurrentPhoneNumber from "./use-current-phone-number"; - -export default function useRequireOnboarding() { - const router = useRouter(); - const { hasFilledTwilioCredentials, hasCompletedOnboarding } = useCurrentUser(); - const phoneNumber = useCurrentPhoneNumber(); - - if (hasCompletedOnboarding) { - return; - } - - if (!hasFilledTwilioCredentials) { - throw router.push(Routes.StepTwo()); - } - - if (!phoneNumber) { - throw router.push(Routes.StepThree()); - } -} diff --git a/app/core/layouts/layout/footer.tsx b/app/core/layouts/layout/footer.tsx index 555801f..b2ce2ed 100644 --- a/app/core/layouts/layout/footer.tsx +++ b/app/core/layouts/layout/footer.tsx @@ -32,7 +32,7 @@ function NavLink({ path, label, icon }: NavLinkProps) { diff --git a/app/messages/components/empty-messages.tsx b/app/messages/components/empty-messages.tsx index 06e6cae..9bcb89d 100644 --- a/app/messages/components/empty-messages.tsx +++ b/app/messages/components/empty-messages.tsx @@ -19,7 +19,7 @@ export default function EmptyMessages() {
+ +
+ + ); +}; + +export default KeypadErrorModal; diff --git a/app/phone-calls/hooks/use-device.tsx b/app/phone-calls/hooks/use-device.tsx index c69cfba..03424e4 100644 --- a/app/phone-calls/hooks/use-device.tsx +++ b/app/phone-calls/hooks/use-device.tsx @@ -1,14 +1,15 @@ import { useCallback, useEffect, useState } from "react"; -import { useMutation } from "blitz"; +import { NotFoundError, Routes, useMutation, useRouter } from "blitz"; import type { TwilioError } from "@twilio/voice-sdk"; import { Call, Device } from "@twilio/voice-sdk"; import getToken from "../mutations/get-token"; -import appLogger from "../../../integrations/logger"; +import appLogger from "integrations/logger"; const logger = appLogger.child({ module: "use-device" }); export default function useDevice() { + const router = useRouter(); const [device, setDevice] = useState(null); const [isDeviceReady, setIsDeviceReady] = useState(() => device?.state === Device.State.Registered); const [getTokenMutation] = useMutation(getToken); @@ -18,9 +19,17 @@ export default function useDevice() { return; } - const token = await getTokenMutation(); - device.updateToken(token); - }, [device, getTokenMutation]); + try { + const token = await getTokenMutation(); + device.updateToken(token); + } catch (error) { + if (error instanceof NotFoundError) { + throw router.push(Routes.KeypadPage()); + } + + throw error; + } + }, [device, getTokenMutation, router]); useEffect(() => { const intervalId = setInterval(() => { @@ -31,17 +40,25 @@ export default function useDevice() { useEffect(() => { (async () => { - const token = await getTokenMutation(); - const device = new Device(token, { - codecPreferences: [Call.Codec.Opus, Call.Codec.PCMU], - sounds: { - [Device.SoundName.Disconnect]: undefined, // TODO - }, - }); - device.register(); - setDevice(device); + try { + const token = await getTokenMutation(); + const device = new Device(token, { + codecPreferences: [Call.Codec.Opus, Call.Codec.PCMU], + sounds: { + [Device.SoundName.Disconnect]: undefined, // TODO + }, + }); + device.register(); + setDevice(device); + } catch (error) { + if (error instanceof NotFoundError) { + throw router.push(Routes.KeypadPage()); + } + + throw error; + } })(); - }, [getTokenMutation, setDevice]); + }, [getTokenMutation, setDevice, router]); useEffect(() => { if (!device) { diff --git a/app/phone-calls/hooks/use-phone-calls.ts b/app/phone-calls/hooks/use-phone-calls.ts index 57c3f43..21684d9 100644 --- a/app/phone-calls/hooks/use-phone-calls.ts +++ b/app/phone-calls/hooks/use-phone-calls.ts @@ -5,9 +5,6 @@ import getPhoneCalls from "../queries/get-phone-calls"; export default function usePhoneCalls() { const phoneNumber = useCurrentPhoneNumber(); - if (!phoneNumber) { - throw new NotFoundError(); - } - return useQuery(getPhoneCalls, { phoneNumberId: phoneNumber.id }); + return useQuery(getPhoneCalls, { phoneNumberId: phoneNumber?.id as string }, { enabled: Boolean(phoneNumber) }); } diff --git a/app/phone-calls/pages/calls.tsx b/app/phone-calls/pages/calls.tsx index adbf8d0..e382682 100644 --- a/app/phone-calls/pages/calls.tsx +++ b/app/phone-calls/pages/calls.tsx @@ -2,18 +2,27 @@ import { Suspense } from "react"; import type { BlitzPage } from "blitz"; import { Routes } from "blitz"; -import Layout from "../../core/layouts/layout"; +import Layout from "app/core/layouts/layout"; import PhoneCallsList from "../components/phone-calls-list"; -import useRequireOnboarding from "../../core/hooks/use-require-onboarding"; +import MissingTwilioCredentials from "app/core/components/missing-twilio-credentials"; +import useCurrentUser from "app/core/hooks/use-current-user"; +import PageTitle from "../../core/components/page-title"; const PhoneCalls: BlitzPage = () => { - useRequireOnboarding(); + const { hasFilledTwilioCredentials } = useCurrentUser(); + + if (!hasFilledTwilioCredentials) { + return ( + <> + + + + ); + } return ( <> -
-

Calls

-
+
diff --git a/app/phone-calls/pages/keypad.tsx b/app/phone-calls/pages/keypad.tsx index 6cac0f4..57fab7b 100644 --- a/app/phone-calls/pages/keypad.tsx +++ b/app/phone-calls/pages/keypad.tsx @@ -1,4 +1,4 @@ -import { Fragment, useRef } from "react"; +import { Fragment, useRef, useState } from "react"; import type { BlitzPage } from "blitz"; import { Routes, useRouter } from "blitz"; import { atom, useAtom } from "jotai"; @@ -7,15 +7,17 @@ import { Transition } from "@headlessui/react"; import { IoBackspace, IoCall } from "react-icons/io5"; import { Direction } from "db"; -import Layout from "../../core/layouts/layout"; +import Layout from "app/core/layouts/layout"; import Keypad from "../components/keypad"; -import useRequireOnboarding from "../../core/hooks/use-require-onboarding"; import usePhoneCalls from "../hooks/use-phone-calls"; import useKeyPress from "../hooks/use-key-press"; +import useCurrentUser from "app/core/hooks/use-current-user"; +import KeypadErrorModal from "../components/keypad-error-modal"; const KeypadPage: BlitzPage = () => { - useRequireOnboarding(); + const { hasFilledTwilioCredentials, hasActiveSubscription } = useCurrentUser(); const router = useRouter(); + const [isKeypadErrorModalOpen, setIsKeypadErrorModalOpen] = useState(false); const [phoneCalls] = usePhoneCalls(); const [phoneNumber, setPhoneNumber] = useAtom(phoneNumberAtom); const removeDigit = useAtom(pressBackspaceAtom)[1]; @@ -74,49 +76,61 @@ const KeypadPage: BlitzPage = () => { }); return ( -
-
- {phoneNumber} -
+ <> +
+
+ {phoneNumber} +
- - + if (phoneNumber === "") { + const lastCall = phoneCalls?.[0]; + if (lastCall) { + const lastCallRecipient = + lastCall.direction === Direction.Inbound ? lastCall.from : lastCall.to; + setPhoneNumber(lastCallRecipient); + } - 0} - enter="transition duration-300 ease-in-out" - enterFrom="transform scale-95 opacity-0" - enterTo="transform scale-100 opacity-100" - leave="transition duration-100 ease-out" - leaveFrom="transform scale-100 opacity-100" - leaveTo="transform scale-95 opacity-0" - > -
- -
-
-
-
+ return; + } + + await router.push(Routes.OutgoingCall({ recipient: encodeURI(phoneNumber) })); + setPhoneNumber(""); + }} + className="cursor-pointer select-none col-start-2 h-12 w-12 flex justify-center items-center mx-auto bg-green-800 rounded-full" + > + + + + 0} + enter="transition duration-300 ease-in-out" + enterFrom="transform scale-95 opacity-0" + enterTo="transform scale-100 opacity-100" + leave="transition duration-100 ease-out" + leaveFrom="transform scale-100 opacity-100" + leaveTo="transform scale-95 opacity-0" + > +
+ +
+
+ +
+ setIsKeypadErrorModalOpen(false)} isOpen={isKeypadErrorModalOpen} /> + ); }; diff --git a/app/phone-calls/pages/outgoing-call/[recipient].tsx b/app/phone-calls/pages/outgoing-call/[recipient].tsx index e202c88..55639da 100644 --- a/app/phone-calls/pages/outgoing-call/[recipient].tsx +++ b/app/phone-calls/pages/outgoing-call/[recipient].tsx @@ -5,14 +5,12 @@ import type { TwilioError } from "@twilio/voice-sdk"; import { atom, useAtom } from "jotai"; import { IoCall } from "react-icons/io5"; -import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; import useMakeCall from "../../hooks/use-make-call"; import useDevice from "../../hooks/use-device"; import Keypad from "../../components/keypad"; const OutgoingCall: BlitzPage = () => { - useRequireOnboarding(); const [phoneNumber, setPhoneNumber] = useAtom(phoneNumberAtom); const router = useRouter(); const recipient = decodeURIComponent(router.params.recipient); diff --git a/app/settings/components/account/danger-zone.tsx b/app/settings/components/account/danger-zone.tsx index f29c848..c5f2ead 100644 --- a/app/settings/components/account/danger-zone.tsx +++ b/app/settings/components/account/danger-zone.tsx @@ -4,7 +4,7 @@ import clsx from "clsx"; import Button from "../button"; import SettingsSection from "../settings-section"; -import Modal, { ModalTitle } from "../modal"; +import Modal, { ModalTitle } from "app/core/components/modal"; import deleteUser from "../../mutations/delete-user"; export default function DangerZone() { diff --git a/app/settings/components/settings-layout.tsx b/app/settings/components/settings-layout.tsx index 23b81b4..9e0f58d 100644 --- a/app/settings/components/settings-layout.tsx +++ b/app/settings/components/settings-layout.tsx @@ -6,6 +6,7 @@ import { IoLogOutOutline, IoNotificationsOutline, IoCardOutline, + IoCallOutline, IoPersonCircleOutline, } from "react-icons/io5"; @@ -15,6 +16,7 @@ import Divider from "./divider"; const subNavigation = [ { name: "Account", href: Routes.Account(), icon: IoPersonCircleOutline }, + { name: "Phone", href: Routes.PhoneSettings(), icon: IoCallOutline }, { name: "Billing", href: Routes.Billing(), icon: IoCardOutline }, { name: "Notifications", href: Routes.Notifications(), icon: IoNotificationsOutline }, ]; @@ -36,7 +38,7 @@ const SettingsLayout: FunctionComponent = ({ children }) => {