get rid of onboarding requirements

This commit is contained in:
m5r 2021-10-16 00:24:28 +02:00
parent c8f707af9c
commit 3cc6f35071
33 changed files with 291 additions and 142 deletions

View File

@ -40,16 +40,9 @@ export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ct
const user = await authenticateUser(email, password); const user = await authenticateUser(email, password);
const organization = user.memberships[0]!.organization; 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({ await ctx.session.$create({
userId: user.id, userId: user.id,
roles: [user.role, user.memberships[0]!.role], roles: [user.role, user.memberships[0]!.role],
hasCompletedOnboarding: hasCompletedOnboarding || undefined,
orgId: organization.id, orgId: organization.id,
}); });

View File

@ -31,6 +31,7 @@ export default resolver.pipe(resolver.zod(Signup), async ({ email, password, ful
userId: user.id, userId: user.id,
roles: [user.role, user.memberships[0]!.role], roles: [user.role, user.memberships[0]!.role],
orgId: user.memberships[0]!.organizationId, orgId: user.memberships[0]!.organizationId,
shouldShowWelcomeMessage: true,
}); });
return user; return user;
}); });

View File

@ -29,7 +29,7 @@ const SignUp: BlitzPage = () => {
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await signupMutation(values); await signupMutation(values);
router.push(Routes.StepOne()); await router.push(Routes.Welcome());
} catch (error: any) { } catch (error: any) {
if (error.code === "P2002" && error.meta?.target?.includes("email")) { if (error.code === "P2002" && error.meta?.target?.includes("email")) {
// This error comes from Prisma // 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) => <BaseLayout title="Sign Up">{page}</BaseLayout>; SignUp.getLayout = (page) => <BaseLayout title="Sign Up">{page}</BaseLayout>;

View File

@ -0,0 +1,28 @@
import type { BlitzPage, GetServerSideProps } from "blitz";
import { getSession, Routes, useRouter } from "blitz";
const Welcome: BlitzPage = () => {
const router = useRouter();
return (
<div>
<p>Thanks for joining Shellphone</p>
<p>Let us know if you need our help</p>
<p>Make sure to set up your phone number</p>
<button onClick={() => router.push(Routes.Messages())}>Open my phone</button>
</div>
);
};
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;

View File

@ -0,0 +1,28 @@
import { Routes, useRouter } from "blitz";
import { IoSettings, IoAlertCircleOutline } from "react-icons/io5";
export default function MissingTwilioCredentials() {
const router = useRouter();
return (
<div className="text-center my-auto">
<IoAlertCircleOutline className="mx-auto h-12 w-12 text-gray-400" aria-hidden="true" />
<h3 className="mt-2 text-sm font-medium text-gray-900">You haven&#39;t set up any phone number yet</h3>
<p className="mt-1 text-sm text-gray-500">
Head over to your settings
<br />
to set up your phone number.
</p>
<div className="mt-6">
<button
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-500 focus:outline-none focus:ring-2 focus:ring-offset-2"
onClick={() => router.push(Routes.PhoneSettings())}
>
<IoSettings className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
Set up my phone number
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,17 @@
import type { FunctionComponent } from "react";
import clsx from "clsx";
type Props = {
className?: string;
title: string;
};
const PageTitle: FunctionComponent<Props> = ({ className, title }) => {
return (
<div className={clsx(className, "flex flex-col space-y-6 p-3")}>
<h2 className="text-3xl font-bold">{title}</h2>
</div>
);
};
export default PageTitle;

View File

@ -9,7 +9,9 @@ export default function useCurrentUser() {
return { return {
user, user,
organization, organization,
hasFilledTwilioCredentials: Boolean(user && organization?.twilioAccountSid && organization?.twilioAuthToken), hasFilledTwilioCredentials: Boolean(
hasCompletedOnboarding: session.hasCompletedOnboarding, organization && organization.twilioAccountSid && organization.twilioAuthToken,
),
hasActiveSubscription: organization && organization.subscriptions.length > 0,
}; };
} }

View File

@ -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());
}
}

View File

@ -32,7 +32,7 @@ function NavLink({ path, label, icon }: NavLinkProps) {
<Link href={path}> <Link href={path}>
<a <a
className={clsx("flex flex-col items-center", { className={clsx("flex flex-col items-center", {
"text-[#007AFF]": isActiveRoute, "text-primary-500": isActiveRoute,
"text-[#959595]": !isActiveRoute, "text-[#959595]": !isActiveRoute,
})} })}
> >

View File

@ -19,7 +19,7 @@ export default function EmptyMessages() {
<div className="mt-6"> <div className="mt-6">
<button <button
type="button" type="button"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-[#007AFF] focus:outline-none focus:ring-2 focus:ring-offset-2" className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-500 focus:outline-none focus:ring-2 focus:ring-offset-2"
onClick={openNewMessageArea} onClick={openNewMessageArea}
> >
<IoCreateOutline className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" /> <IoCreateOutline className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />

View File

@ -4,14 +4,16 @@ import type { BlitzPage } from "blitz";
import { Routes } from "blitz"; import { Routes } from "blitz";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import Layout from "../../core/layouts/layout"; import Layout from "app/core/layouts/layout";
import ConversationsList from "../components/conversations-list"; import ConversationsList from "../components/conversations-list";
import NewMessageButton from "../components/new-message-button"; import NewMessageButton from "../components/new-message-button";
import useRequireOnboarding from "../../core/hooks/use-require-onboarding"; import MissingTwilioCredentials from "app/core/components/missing-twilio-credentials";
import useNotifications from "../../core/hooks/use-notifications"; import useNotifications from "app/core/hooks/use-notifications";
import useCurrentUser from "app/core/hooks/use-current-user";
import PageTitle from "../../core/components/page-title";
const Messages: BlitzPage = () => { const Messages: BlitzPage = () => {
useRequireOnboarding(); const { hasFilledTwilioCredentials } = useCurrentUser();
const { subscription, subscribe } = useNotifications(); const { subscription, subscribe } = useNotifications();
const setIsOpen = useAtom(bottomSheetOpenAtom)[1]; const setIsOpen = useAtom(bottomSheetOpenAtom)[1];
@ -21,11 +23,18 @@ const Messages: BlitzPage = () => {
} }
}, [subscribe, subscription]); }, [subscribe, subscription]);
if (!hasFilledTwilioCredentials) {
return (
<>
<MissingTwilioCredentials />
<PageTitle className="filter blur-sm absolute top-0" title="Messages" />
</>
);
}
return ( return (
<> <>
<div className="flex flex-col space-y-6 p-3"> <PageTitle title="Messages" />
<h2 className="text-3xl font-bold">Messages</h2>
</div>
<section className="flex flex-grow flex-col"> <section className="flex flex-grow flex-col">
<Suspense fallback="Loading..."> <Suspense fallback="Loading...">
<ConversationsList /> <ConversationsList />

View File

@ -5,12 +5,9 @@ import { IoChevronBack, IoInformationCircle, IoCall } from "react-icons/io5";
import Layout from "../../../core/layouts/layout"; import Layout from "../../../core/layouts/layout";
import Conversation from "../../components/conversation"; import Conversation from "../../components/conversation";
import useRequireOnboarding from "../../../core/hooks/use-require-onboarding";
import useConversation from "../../hooks/use-conversation"; import useConversation from "../../hooks/use-conversation";
const ConversationPage: BlitzPage = () => { const ConversationPage: BlitzPage = () => {
useRequireOnboarding();
const router = useRouter(); const router = useRouter();
const recipient = decodeURIComponent(router.params.recipient); const recipient = decodeURIComponent(router.params.recipient);
const pageTitle = `Messages with ${recipient}`; const pageTitle = `Messages with ${recipient}`;

View File

@ -22,12 +22,11 @@ export default resolver.pipe(
where: { id: organizationId }, where: { id: organizationId },
include: { phoneNumbers: true }, include: { phoneNumbers: true },
}); });
if (!organization) { if (!organization || !organization.phoneNumbers[0]) {
throw new NotFoundError(); throw new NotFoundError();
} }
const phoneNumberId = organization.phoneNumbers[0]!.id; const phoneNumberId = organization.phoneNumbers[0].id;
const processingState = await db.processingPhoneNumber.findFirst({ where: { organizationId, phoneNumberId } }); const processingState = await db.processingPhoneNumber.findFirst({ where: { organizationId, phoneNumberId } });
if (processingState && !processingState.hasFetchedMessages) { if (processingState && !processingState.hasFetchedMessages) {
return; return;

View File

@ -1,7 +1,7 @@
import type { FunctionComponent } from "react"; import type { FunctionComponent } from "react";
import { useRef } from "react"; import { useRef } from "react";
import Modal, { ModalTitle } from "../../settings/components/modal"; import Modal, { ModalTitle } from "app/core/components/modal";
type Props = { type Props = {
isHelpModalOpen: boolean; isHelpModalOpen: boolean;

View File

@ -30,7 +30,6 @@ export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({
number: phoneNumber.phoneNumber, number: phoneNumber.phoneNumber,
}, },
}); });
context.session.$setPrivateData({ hasCompletedOnboarding: true });
const mainTwilioClient = twilio(organization.twilioAccountSid, organization.twilioAuthToken); const mainTwilioClient = twilio(organization.twilioAccountSid, organization.twilioAuthToken);
const apiKey = await mainTwilioClient.newKeys.create({ friendlyName: "Shellphone API key" }); const apiKey = await mainTwilioClient.newKeys.create({ friendlyName: "Shellphone API key" });

View File

@ -40,7 +40,6 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId: session.orgId } }); const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId: session.orgId } });
if (phoneNumber) { if (phoneNumber) {
await session.$setPublicData({ hasCompletedOnboarding: true });
return { return {
redirect: { redirect: {
destination: Routes.Messages().pathname, destination: Routes.Messages().pathname,

View File

@ -102,7 +102,6 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ req, res }
const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId: session.orgId } }); const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId: session.orgId } });
if (phoneNumber) { if (phoneNumber) {
await session.$setPublicData({ hasCompletedOnboarding: true });
return { return {
redirect: { redirect: {
destination: Routes.Messages().pathname, destination: Routes.Messages().pathname,

View File

@ -140,7 +140,6 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId: session.orgId } }); const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId: session.orgId } });
if (phoneNumber) { if (phoneNumber) {
await session.$setPublicData({ hasCompletedOnboarding: true });
return { return {
redirect: { redirect: {
destination: Routes.Messages().pathname, destination: Routes.Messages().pathname,

View File

@ -22,7 +22,7 @@ export default function EmptyMessages() {
<p className="mt-1 text-sm text-gray-500">Get started by calling someone you know.</p> <p className="mt-1 text-sm text-gray-500">Get started by calling someone you know.</p>
<div className="mt-6"> <div className="mt-6">
<Link href={Routes.KeypadPage()}> <Link href={Routes.KeypadPage()}>
<a className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-[#007AFF] focus:outline-none focus:ring-2 focus:ring-offset-2"> <a className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-500 focus:outline-none focus:ring-2 focus:ring-offset-2">
<IoKeypad className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" /> <IoKeypad className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
Open keypad Open keypad
</a> </a>

View File

@ -0,0 +1,53 @@
import type { FunctionComponent } from "react";
import { useRef } from "react";
import { Link, Routes, useRouter } from "blitz";
import Modal, { ModalTitle } from "app/core/components/modal";
type Props = {
isOpen: boolean;
closeModal: () => void;
};
const KeypadErrorModal: FunctionComponent<Props> = ({ isOpen, closeModal }) => {
const openSettingsButtonRef = useRef<HTMLButtonElement>(null);
const router = useRouter();
return (
<Modal initialFocus={openSettingsButtonRef} isOpen={isOpen} onClose={closeModal}>
<div className="md:flex md:items-start">
<div className="mt-3 text-center md:mt-0 md:ml-4 md:text-left">
<ModalTitle>Woah, hold on! Set up your &#128026;phone number first</ModalTitle>
<div className="mt-2 text-gray-500">
<p>
First things first. Head over to your{" "}
<Link href={Routes.PhoneSettings()}>
<a className="underline">phone settings</a>
</Link>{" "}
to set up your phone number.
</p>
</div>
</div>
</div>
<div className="mt-5 md:mt-4 md:flex md:flex-row-reverse">
<button
ref={openSettingsButtonRef}
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-primary-500 text-base font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 md:mt-0 md:w-auto md:text-sm"
onClick={() => router.push(Routes.PhoneSettings())}
>
Take me there
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 md:mt-0 md:w-auto md:text-sm"
onClick={closeModal}
>
I got it, thanks!
</button>
</div>
</Modal>
);
};
export default KeypadErrorModal;

View File

@ -1,14 +1,15 @@
import { useCallback, useEffect, useState } from "react"; 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 type { TwilioError } from "@twilio/voice-sdk";
import { Call, Device } from "@twilio/voice-sdk"; import { Call, Device } from "@twilio/voice-sdk";
import getToken from "../mutations/get-token"; import getToken from "../mutations/get-token";
import appLogger from "../../../integrations/logger"; import appLogger from "integrations/logger";
const logger = appLogger.child({ module: "use-device" }); const logger = appLogger.child({ module: "use-device" });
export default function useDevice() { export default function useDevice() {
const router = useRouter();
const [device, setDevice] = useState<Device | null>(null); const [device, setDevice] = useState<Device | null>(null);
const [isDeviceReady, setIsDeviceReady] = useState(() => device?.state === Device.State.Registered); const [isDeviceReady, setIsDeviceReady] = useState(() => device?.state === Device.State.Registered);
const [getTokenMutation] = useMutation(getToken); const [getTokenMutation] = useMutation(getToken);
@ -18,9 +19,17 @@ export default function useDevice() {
return; return;
} }
const token = await getTokenMutation(); try {
device.updateToken(token); const token = await getTokenMutation();
}, [device, getTokenMutation]); device.updateToken(token);
} catch (error) {
if (error instanceof NotFoundError) {
throw router.push(Routes.KeypadPage());
}
throw error;
}
}, [device, getTokenMutation, router]);
useEffect(() => { useEffect(() => {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
@ -31,17 +40,25 @@ export default function useDevice() {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const token = await getTokenMutation(); try {
const device = new Device(token, { const token = await getTokenMutation();
codecPreferences: [Call.Codec.Opus, Call.Codec.PCMU], const device = new Device(token, {
sounds: { codecPreferences: [Call.Codec.Opus, Call.Codec.PCMU],
[Device.SoundName.Disconnect]: undefined, // TODO sounds: {
}, [Device.SoundName.Disconnect]: undefined, // TODO
}); },
device.register(); });
setDevice(device); device.register();
setDevice(device);
} catch (error) {
if (error instanceof NotFoundError) {
throw router.push(Routes.KeypadPage());
}
throw error;
}
})(); })();
}, [getTokenMutation, setDevice]); }, [getTokenMutation, setDevice, router]);
useEffect(() => { useEffect(() => {
if (!device) { if (!device) {

View File

@ -5,9 +5,6 @@ import getPhoneCalls from "../queries/get-phone-calls";
export default function usePhoneCalls() { export default function usePhoneCalls() {
const phoneNumber = useCurrentPhoneNumber(); 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) });
} }

View File

@ -2,18 +2,27 @@ import { Suspense } from "react";
import type { BlitzPage } from "blitz"; import type { BlitzPage } from "blitz";
import { Routes } 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 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 = () => { const PhoneCalls: BlitzPage = () => {
useRequireOnboarding(); const { hasFilledTwilioCredentials } = useCurrentUser();
if (!hasFilledTwilioCredentials) {
return (
<>
<MissingTwilioCredentials />
<PageTitle className="filter blur-sm absolute top-0" title="Calls" />
</>
);
}
return ( return (
<> <>
<div className="flex flex-col space-y-6 py-3 pl-12"> <PageTitle className="pl-12" title="Calls" />
<h2 className="text-3xl font-bold">Calls</h2>
</div>
<section className="flex flex-grow flex-col"> <section className="flex flex-grow flex-col">
<Suspense fallback="Loading..."> <Suspense fallback="Loading...">
<PhoneCallsList /> <PhoneCallsList />

View File

@ -1,4 +1,4 @@
import { Fragment, useRef } from "react"; import { Fragment, useRef, useState } from "react";
import type { BlitzPage } from "blitz"; import type { BlitzPage } from "blitz";
import { Routes, useRouter } from "blitz"; import { Routes, useRouter } from "blitz";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
@ -7,15 +7,17 @@ import { Transition } from "@headlessui/react";
import { IoBackspace, IoCall } from "react-icons/io5"; import { IoBackspace, IoCall } from "react-icons/io5";
import { Direction } from "db"; import { Direction } from "db";
import Layout from "../../core/layouts/layout"; import Layout from "app/core/layouts/layout";
import Keypad from "../components/keypad"; import Keypad from "../components/keypad";
import useRequireOnboarding from "../../core/hooks/use-require-onboarding";
import usePhoneCalls from "../hooks/use-phone-calls"; import usePhoneCalls from "../hooks/use-phone-calls";
import useKeyPress from "../hooks/use-key-press"; 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 = () => { const KeypadPage: BlitzPage = () => {
useRequireOnboarding(); const { hasFilledTwilioCredentials, hasActiveSubscription } = useCurrentUser();
const router = useRouter(); const router = useRouter();
const [isKeypadErrorModalOpen, setIsKeypadErrorModalOpen] = useState(false);
const [phoneCalls] = usePhoneCalls(); const [phoneCalls] = usePhoneCalls();
const [phoneNumber, setPhoneNumber] = useAtom(phoneNumberAtom); const [phoneNumber, setPhoneNumber] = useAtom(phoneNumberAtom);
const removeDigit = useAtom(pressBackspaceAtom)[1]; const removeDigit = useAtom(pressBackspaceAtom)[1];
@ -74,49 +76,61 @@ const KeypadPage: BlitzPage = () => {
}); });
return ( return (
<div className="w-96 h-full flex flex-col justify-around py-5 mx-auto text-center text-black"> <>
<div className="h-16 text-3xl text-gray-700"> <div className="w-96 h-full flex flex-col justify-around py-5 mx-auto text-center text-black">
<span>{phoneNumber}</span> <div className="h-16 text-3xl text-gray-700">
</div> <span>{phoneNumber}</span>
</div>
<Keypad onDigitPressProps={onDigitPressProps} onZeroPressProps={onZeroPressProps}> <Keypad onDigitPressProps={onDigitPressProps} onZeroPressProps={onZeroPressProps}>
<button <button
onClick={async () => { onClick={async () => {
if (phoneNumber === "") { if (!hasFilledTwilioCredentials) {
const lastCall = phoneCalls?.[0]; setIsKeypadErrorModalOpen(true);
if (lastCall) { return;
const lastCallRecipient =
lastCall.direction === Direction.Inbound ? lastCall.from : lastCall.to;
setPhoneNumber(lastCallRecipient);
} }
return; if (!hasActiveSubscription) {
} // TODO
}
await router.push(Routes.OutgoingCall({ recipient: encodeURI(phoneNumber) })); if (phoneNumber === "") {
setPhoneNumber(""); const lastCall = phoneCalls?.[0];
}} if (lastCall) {
className="cursor-pointer select-none col-start-2 h-12 w-12 flex justify-center items-center mx-auto bg-green-800 rounded-full" const lastCallRecipient =
> lastCall.direction === Direction.Inbound ? lastCall.from : lastCall.to;
<IoCall className="w-6 h-6 text-white" /> setPhoneNumber(lastCallRecipient);
</button> }
<Transition return;
as={Fragment} }
show={phoneNumber.length > 0}
enter="transition duration-300 ease-in-out" await router.push(Routes.OutgoingCall({ recipient: encodeURI(phoneNumber) }));
enterFrom="transform scale-95 opacity-0" setPhoneNumber("");
enterTo="transform scale-100 opacity-100" }}
leave="transition duration-100 ease-out" className="cursor-pointer select-none col-start-2 h-12 w-12 flex justify-center items-center mx-auto bg-green-800 rounded-full"
leaveFrom="transform scale-100 opacity-100" >
leaveTo="transform scale-95 opacity-0" <IoCall className="w-6 h-6 text-white" />
> </button>
<div {...onBackspacePress} className="cursor-pointer select-none m-auto">
<IoBackspace className="w-6 h-6" /> <Transition
</div> as={Fragment}
</Transition> show={phoneNumber.length > 0}
</Keypad> enter="transition duration-300 ease-in-out"
</div> 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"
>
<div {...onBackspacePress} className="cursor-pointer select-none m-auto">
<IoBackspace className="w-6 h-6" />
</div>
</Transition>
</Keypad>
</div>
<KeypadErrorModal closeModal={() => setIsKeypadErrorModalOpen(false)} isOpen={isKeypadErrorModalOpen} />
</>
); );
}; };

View File

@ -5,14 +5,12 @@ import type { TwilioError } from "@twilio/voice-sdk";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { IoCall } from "react-icons/io5"; import { IoCall } from "react-icons/io5";
import useRequireOnboarding from "../../../core/hooks/use-require-onboarding";
import useMakeCall from "../../hooks/use-make-call"; import useMakeCall from "../../hooks/use-make-call";
import useDevice from "../../hooks/use-device"; import useDevice from "../../hooks/use-device";
import Keypad from "../../components/keypad"; import Keypad from "../../components/keypad";
const OutgoingCall: BlitzPage = () => { const OutgoingCall: BlitzPage = () => {
useRequireOnboarding();
const [phoneNumber, setPhoneNumber] = useAtom(phoneNumberAtom); const [phoneNumber, setPhoneNumber] = useAtom(phoneNumberAtom);
const router = useRouter(); const router = useRouter();
const recipient = decodeURIComponent(router.params.recipient); const recipient = decodeURIComponent(router.params.recipient);

View File

@ -4,7 +4,7 @@ import clsx from "clsx";
import Button from "../button"; import Button from "../button";
import SettingsSection from "../settings-section"; import SettingsSection from "../settings-section";
import Modal, { ModalTitle } from "../modal"; import Modal, { ModalTitle } from "app/core/components/modal";
import deleteUser from "../../mutations/delete-user"; import deleteUser from "../../mutations/delete-user";
export default function DangerZone() { export default function DangerZone() {

View File

@ -6,6 +6,7 @@ import {
IoLogOutOutline, IoLogOutOutline,
IoNotificationsOutline, IoNotificationsOutline,
IoCardOutline, IoCardOutline,
IoCallOutline,
IoPersonCircleOutline, IoPersonCircleOutline,
} from "react-icons/io5"; } from "react-icons/io5";
@ -15,6 +16,7 @@ import Divider from "./divider";
const subNavigation = [ const subNavigation = [
{ name: "Account", href: Routes.Account(), icon: IoPersonCircleOutline }, { name: "Account", href: Routes.Account(), icon: IoPersonCircleOutline },
{ name: "Phone", href: Routes.PhoneSettings(), icon: IoCallOutline },
{ name: "Billing", href: Routes.Billing(), icon: IoCardOutline }, { name: "Billing", href: Routes.Billing(), icon: IoCardOutline },
{ name: "Notifications", href: Routes.Notifications(), icon: IoNotificationsOutline }, { name: "Notifications", href: Routes.Notifications(), icon: IoNotificationsOutline },
]; ];
@ -36,7 +38,7 @@ const SettingsLayout: FunctionComponent = ({ children }) => {
<main className="flex flex-col flex-grow mx-auto w-full max-w-7xl pb-10 lg:py-12 lg:px-8"> <main className="flex flex-col flex-grow mx-auto w-full max-w-7xl pb-10 lg:py-12 lg:px-8">
<div className="flex flex-col flex-grow lg:grid lg:grid-cols-12 lg:gap-x-5"> <div className="flex flex-col flex-grow lg:grid lg:grid-cols-12 lg:gap-x-5">
<aside className="py-6 px-2 sm:px-6 lg:py-0 lg:px-0 lg:col-span-3"> <aside className="py-6 px-2 sm:px-6 lg:py-0 lg:px-0 lg:col-span-3">
<nav className="h-full flex flex-col"> <nav className="space-y-1 h-full flex flex-col">
{subNavigation.map((item) => { {subNavigation.map((item) => {
const isCurrentPage = item.href.pathname === router.pathname; const isCurrentPage = item.href.pathname === router.pathname;
@ -47,7 +49,7 @@ const SettingsLayout: FunctionComponent = ({ children }) => {
isCurrentPage isCurrentPage
? "bg-gray-50 text-primary-600 hover:bg-white" ? "bg-gray-50 text-primary-600 hover:bg-white"
: "text-gray-900 hover:text-gray-900 hover:bg-gray-50", : "text-gray-900 hover:text-gray-900 hover:bg-gray-50",
"mb-1 group rounded-md px-3 py-2 flex items-center text-sm font-medium", "group rounded-md px-3 py-2 flex items-center text-sm font-medium",
)} )}
aria-current={isCurrentPage ? "page" : undefined} aria-current={isCurrentPage ? "page" : undefined}
> >
@ -66,7 +68,7 @@ const SettingsLayout: FunctionComponent = ({ children }) => {
); );
})} })}
<Divider className="mt-auto mb-1" /> <Divider />
<button <button
onClick={() => logoutMutation()} onClick={() => logoutMutation()}
className="group text-gray-900 hover:text-gray-900 hover:bg-gray-50 rounded-md px-3 py-2 flex items-center text-sm font-medium" className="group text-gray-900 hover:text-gray-900 hover:bg-gray-50 rounded-md px-3 py-2 flex items-center text-sm font-medium"

View File

@ -3,7 +3,6 @@ import { GetServerSideProps, getSession, Routes } from "blitz";
import db, { Subscription, SubscriptionStatus } from "db"; import db, { Subscription, SubscriptionStatus } from "db";
import useSubscription from "../../hooks/use-subscription"; import useSubscription from "../../hooks/use-subscription";
import useRequireOnboarding from "../../../core/hooks/use-require-onboarding";
import usePaymentsHistory from "../../hooks/use-payments-history"; import usePaymentsHistory from "../../hooks/use-payments-history";
import SettingsLayout from "../../components/settings-layout"; import SettingsLayout from "../../components/settings-layout";
import SettingsSection from "../../components/settings-section"; import SettingsSection from "../../components/settings-section";
@ -27,7 +26,6 @@ const Billing: BlitzPage<Props> = (props) => {
- resubscribe (message like "your subscription expired, would you like to renew ?") - resubscribe (message like "your subscription expired, would you like to renew ?")
*/ */
useRequireOnboarding();
const { count: paymentsCount } = usePaymentsHistory(); const { count: paymentsCount } = usePaymentsHistory();
const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription({ const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription({
initialData: props.subscription, initialData: props.subscription,

View File

@ -5,11 +5,8 @@ import SettingsLayout from "../../components/settings-layout";
import ProfileInformations from "../../components/account/profile-informations"; import ProfileInformations from "../../components/account/profile-informations";
import UpdatePassword from "../../components/account/update-password"; import UpdatePassword from "../../components/account/update-password";
import DangerZone from "../../components/account/danger-zone"; import DangerZone from "../../components/account/danger-zone";
import useRequireOnboarding from "../../../core/hooks/use-require-onboarding";
const Account: BlitzPage = () => { const Account: BlitzPage = () => {
useRequireOnboarding();
return ( return (
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<ProfileInformations /> <ProfileInformations />

View File

@ -0,0 +1,14 @@
import type { BlitzPage } from "blitz";
import { Routes } from "blitz";
import SettingsLayout from "../../components/settings-layout";
const PhoneSettings: BlitzPage = () => {
return <div>Coming soon</div>;
};
PhoneSettings.getLayout = (page) => <SettingsLayout>{page}</SettingsLayout>;
PhoneSettings.authenticate = { redirectTo: Routes.SignIn() };
export default PhoneSettings;

View File

@ -1,6 +1,6 @@
import type { Ctx } from "blitz"; import type { Ctx } from "blitz";
import db from "db"; import db, { SubscriptionStatus } from "db";
export default async function getCurrentUser(_ = null, { session }: Ctx) { export default async function getCurrentUser(_ = null, { session }: Ctx) {
if (!session.userId) return null; if (!session.userId) return null;
@ -15,14 +15,10 @@ export default async function getCurrentUser(_ = null, { session }: Ctx) {
memberships: { memberships: {
include: { include: {
organization: { organization: {
select: { include: {
id: true, subscriptions: {
encryptionKey: true, where: { status: SubscriptionStatus.active },
twilioAccountSid: true, },
twilioAuthToken: true,
twilioApiKey: true,
twilioApiSecret: true,
twimlAppSid: true,
}, },
}, },
}, },

View File

@ -15,7 +15,7 @@ declare module "blitz" {
userId: User["id"]; userId: User["id"];
roles: Role[]; roles: Role[];
orgId: Organization["id"]; orgId: Organization["id"];
hasCompletedOnboarding?: true; shouldShowWelcomeMessage?: true;
}; };
} }
} }