From 2afd3554b31377cae5b810f1421e9e1c4e9fa8d5 Mon Sep 17 00:00:00 2001 From: m5r Date: Sat, 16 Oct 2021 01:25:13 +0200 Subject: [PATCH] set your twilio things from settings --- app/core/hooks/use-current-user.ts | 12 +- app/messages/pages/messages.tsx | 7 +- .../components/onboarding-layout.tsx | 104 ------------ app/onboarding/pages/welcome/step-one.tsx | 54 ------ app/onboarding/pages/welcome/step-three.tsx | 145 ----------------- app/onboarding/pages/welcome/step-two.tsx | 154 ------------------ .../components/keypad-error-modal.tsx | 4 +- app/phone-calls/pages/calls.tsx | 4 +- app/phone-calls/pages/keypad.tsx | 4 +- .../api/queue/set-twilio-webhooks.ts | 4 +- .../api/queue/subscription-created.ts | 2 +- .../components/phone}/help-modal.tsx | 6 +- .../components/phone/phone-number-form.tsx | 76 +++++++++ .../components/phone/twilio-api-form.tsx | 90 ++++++++++ .../mutations/set-phone-number.ts | 4 +- .../mutations/set-twilio-api-fields.ts | 16 +- app/settings/pages/settings/phone.tsx | 18 +- .../queries/get-available-phone-numbers.ts | 21 +++ 18 files changed, 242 insertions(+), 483 deletions(-) delete mode 100644 app/onboarding/components/onboarding-layout.tsx delete mode 100644 app/onboarding/pages/welcome/step-one.tsx delete mode 100644 app/onboarding/pages/welcome/step-three.tsx delete mode 100644 app/onboarding/pages/welcome/step-two.tsx rename app/{onboarding => settings}/api/queue/set-twilio-webhooks.ts (96%) rename app/{onboarding/components => settings/components/phone}/help-modal.tsx (81%) create mode 100644 app/settings/components/phone/phone-number-form.tsx create mode 100644 app/settings/components/phone/twilio-api-form.tsx rename app/{onboarding => settings}/mutations/set-phone-number.ts (93%) rename app/{onboarding => settings}/mutations/set-twilio-api-fields.ts (64%) create mode 100644 app/settings/queries/get-available-phone-numbers.ts diff --git a/app/core/hooks/use-current-user.ts b/app/core/hooks/use-current-user.ts index 7331a6c..201eacd 100644 --- a/app/core/hooks/use-current-user.ts +++ b/app/core/hooks/use-current-user.ts @@ -1,17 +1,21 @@ import { useSession, useQuery } from "blitz"; import getCurrentUser from "../../users/queries/get-current-user"; +import getCurrentPhoneNumber from "../../phone-numbers/queries/get-current-phone-number"; export default function useCurrentUser() { const session = useSession(); - const [user] = useQuery(getCurrentUser, null, { enabled: Boolean(session.userId) }); + const [user, userQuery] = useQuery(getCurrentUser, null, { enabled: Boolean(session.userId) }); const organization = user?.memberships[0]!.organization; + const hasFilledTwilioCredentials = Boolean(organization?.twilioAccountSid && organization?.twilioAuthToken); + const [phoneNumber] = useQuery(getCurrentPhoneNumber, {}, { enabled: hasFilledTwilioCredentials }); + return { user, organization, - hasFilledTwilioCredentials: Boolean( - organization && organization.twilioAccountSid && organization.twilioAuthToken, - ), + hasFilledTwilioCredentials, + hasPhoneNumber: Boolean(phoneNumber), hasActiveSubscription: organization && organization.subscriptions.length > 0, + refetch: userQuery.refetch, }; } diff --git a/app/messages/pages/messages.tsx b/app/messages/pages/messages.tsx index 470ad3d..a4825c4 100644 --- a/app/messages/pages/messages.tsx +++ b/app/messages/pages/messages.tsx @@ -1,7 +1,6 @@ import { Suspense, useEffect } from "react"; -import dynamic from "next/dynamic"; import type { BlitzPage } from "blitz"; -import { Routes } from "blitz"; +import { Routes, dynamic } from "blitz"; import { atom, useAtom } from "jotai"; import Layout from "app/core/layouts/layout"; @@ -13,7 +12,7 @@ import useCurrentUser from "app/core/hooks/use-current-user"; import PageTitle from "../../core/components/page-title"; const Messages: BlitzPage = () => { - const { hasFilledTwilioCredentials } = useCurrentUser(); + const { hasFilledTwilioCredentials, hasPhoneNumber } = useCurrentUser(); const { subscription, subscribe } = useNotifications(); const setIsOpen = useAtom(bottomSheetOpenAtom)[1]; @@ -23,7 +22,7 @@ const Messages: BlitzPage = () => { } }, [subscribe, subscription]); - if (!hasFilledTwilioCredentials) { + if (!hasFilledTwilioCredentials || !hasPhoneNumber) { return ( <> diff --git a/app/onboarding/components/onboarding-layout.tsx b/app/onboarding/components/onboarding-layout.tsx deleted file mode 100644 index 02aa848..0000000 --- a/app/onboarding/components/onboarding-layout.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import type { FunctionComponent } from "react"; -import { Link } from "blitz"; -import { IoCheckmark } from "react-icons/io5"; -import clsx from "clsx"; - -type StepLink = { - href: string; - label: string; -}; - -type Props = { - currentStep: 1 | 2 | 3; - previous?: StepLink; - next?: StepLink; -}; - -const steps = ["Welcome", "Twilio Credentials", "Pick a plan"] as const; - -const OnboardingLayout: FunctionComponent = ({ children, currentStep, previous, next }) => { - return ( -
-
- {/* This element is to trick the browser into centering the modal contents. */} - -
-

- {steps[currentStep - 1]} -

- -
{children}
- - -
-
-
- ); -}; - -export default OnboardingLayout; diff --git a/app/onboarding/pages/welcome/step-one.tsx b/app/onboarding/pages/welcome/step-one.tsx deleted file mode 100644 index bf125a5..0000000 --- a/app/onboarding/pages/welcome/step-one.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { BlitzPage, GetServerSideProps } from "blitz"; -import { getSession, Routes } from "blitz"; - -import OnboardingLayout from "../../components/onboarding-layout"; -import useCurrentUser from "../../../core/hooks/use-current-user"; -import db from "../../../../db"; - -const StepOne: BlitzPage = () => { - useCurrentUser(); // preload for step two - - return ( -
-

Welcome to Shellphone

- - We'll help you connect your Twilio phone number to our service and set up your virtual phone! - -
- ); -}; - -StepOne.getLayout = (page) => ( - - {page} - -); - -StepOne.authenticate = { redirectTo: Routes.SignIn() }; - -export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const session = await getSession(req, res); - if (!session.userId) { - await session.$revoke(); - return { - redirect: { - destination: Routes.LandingPage().pathname, - permanent: false, - }, - }; - } - - const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId: session.orgId } }); - if (phoneNumber) { - return { - redirect: { - destination: Routes.Messages().pathname, - permanent: false, - }, - }; - } - - return { props: {} }; -}; - -export default StepOne; diff --git a/app/onboarding/pages/welcome/step-three.tsx b/app/onboarding/pages/welcome/step-three.tsx deleted file mode 100644 index b38cc12..0000000 --- a/app/onboarding/pages/welcome/step-three.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import type { BlitzPage, GetServerSideProps } from "blitz"; -import { Routes, getSession, useRouter, useMutation } from "blitz"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import clsx from "clsx"; -import twilio from "twilio"; - -import db from "../../../../db"; -import OnboardingLayout from "../../components/onboarding-layout"; -import setPhoneNumber from "../../mutations/set-phone-number"; - -type PhoneNumber = { - phoneNumber: string; - sid: string; -}; - -type Props = { - availablePhoneNumbers: PhoneNumber[]; -}; - -type Form = { - phoneNumberSid: string; -}; - -const StepThree: BlitzPage = ({ availablePhoneNumbers }) => { - const { - register, - handleSubmit, - setValue, - formState: { isSubmitting }, - } = useForm
(); - const router = useRouter(); - const [setPhoneNumberMutation] = useMutation(setPhoneNumber); - - useEffect(() => { - if (availablePhoneNumbers[0]) { - setValue("phoneNumberSid", availablePhoneNumbers[0].sid); - } - }); - - const onSubmit = handleSubmit(async ({ phoneNumberSid }) => { - if (isSubmitting) { - return; - } - - await setPhoneNumberMutation({ phoneNumberSid }); - await router.push(Routes.Messages()); - }); - - return ( -
- - - - - - -
- ); -}; - -StepThree.getLayout = (page) => ( - - {page} - -); - -StepThree.authenticate = { redirectTo: Routes.SignIn() }; - -export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const session = await getSession(req, res); - if (!session.userId) { - await session.$revoke(); - return { - redirect: { - destination: Routes.LandingPage().pathname, - permanent: false, - }, - }; - } - - const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId: session.orgId } }); - if (phoneNumber) { - return { - redirect: { - destination: Routes.Messages().pathname, - permanent: false, - }, - }; - } - - const organization = await db.organization.findFirst({ where: { id: session.orgId } }); - if (!organization) { - return { - redirect: { - destination: Routes.StepOne().pathname, - permanent: false, - }, - }; - } - - if (!organization.twilioAccountSid || !organization.twilioAuthToken) { - return { - redirect: { - destination: Routes.StepTwo().pathname, - permanent: false, - }, - }; - } - - const incomingPhoneNumbers = await twilio( - organization.twilioAccountSid, - organization.twilioAuthToken, - ).incomingPhoneNumbers.list(); - const phoneNumbers = incomingPhoneNumbers.map(({ phoneNumber, sid }) => ({ phoneNumber, sid })); - - return { - props: { - availablePhoneNumbers: phoneNumbers, - }, - }; -}; - -export default StepThree; diff --git a/app/onboarding/pages/welcome/step-two.tsx b/app/onboarding/pages/welcome/step-two.tsx deleted file mode 100644 index f606dd4..0000000 --- a/app/onboarding/pages/welcome/step-two.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import type { FunctionComponent } from "react"; -import { Suspense, useEffect, useState } from "react"; -import type { BlitzPage, GetServerSideProps } from "blitz"; -import { getSession, Routes, useMutation, useRouter } from "blitz"; -import clsx from "clsx"; -import { useForm } from "react-hook-form"; -import { IoHelpCircle } from "react-icons/io5"; - -import db from "db"; -import setTwilioApiFields from "../../mutations/set-twilio-api-fields"; -import OnboardingLayout from "../../components/onboarding-layout"; -import HelpModal from "../../components/help-modal"; -import useCurrentUser from "../../../core/hooks/use-current-user"; - -type Form = { - twilioAccountSid: string; - twilioAuthToken: string; -}; - -const StepTwo: BlitzPage = () => { - const { - register, - handleSubmit, - setValue, - formState: { isSubmitting }, - } = useForm
(); - const router = useRouter(); - const { organization } = useCurrentUser(); - const [setTwilioApiFieldsMutation] = useMutation(setTwilioApiFields); - const [isHelpModalOpen, setIsHelpModalOpen] = useState(false); - - useEffect(() => { - setValue("twilioAuthToken", organization?.twilioAuthToken ?? ""); - setValue("twilioAccountSid", organization?.twilioAccountSid ?? ""); - }, [setValue, organization?.twilioAuthToken, organization?.twilioAccountSid]); - - const onSubmit = handleSubmit(async ({ twilioAccountSid, twilioAuthToken }) => { - if (isSubmitting) { - return; - } - - await setTwilioApiFieldsMutation({ - twilioAccountSid, - twilioAuthToken, - }); - - await router.push(Routes.StepThree()); - }); - - return ( - <> -
- - -
- Shellphone needs some informations about your Twilio account to securely use your phone numbers. -
- -
- - -
-
- - -
- - - -
- - setIsHelpModalOpen(false)} isHelpModalOpen={isHelpModalOpen} /> - - ); -}; - -StepTwo.getLayout = (page) => { - return ( - - {page} - - ); -}; - -const StepTwoLayout: FunctionComponent = ({ children }) => { - const { organization } = useCurrentUser(); - const initialAuthToken = organization?.twilioAuthToken ?? ""; - const initialAccountSid = organization?.twilioAccountSid ?? ""; - const hasTwilioCredentials = initialAccountSid.length > 0 && initialAuthToken.length > 0; - - return ( - - {children} - - ); -}; - -StepTwo.authenticate = { redirectTo: Routes.SignIn() }; - -export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const session = await getSession(req, res); - if (!session.userId) { - await session.$revoke(); - return { - redirect: { - destination: Routes.LandingPage().pathname, - permanent: false, - }, - }; - } - - const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId: session.orgId } }); - if (phoneNumber) { - return { - redirect: { - destination: Routes.Messages().pathname, - permanent: false, - }, - }; - } - - return { props: {} }; -}; - -export default StepTwo; diff --git a/app/phone-calls/components/keypad-error-modal.tsx b/app/phone-calls/components/keypad-error-modal.tsx index dd9a0c8..cb7ae32 100644 --- a/app/phone-calls/components/keypad-error-modal.tsx +++ b/app/phone-calls/components/keypad-error-modal.tsx @@ -33,14 +33,14 @@ const KeypadErrorModal: FunctionComponent = ({ isOpen, closeModal }) => { diff --git a/app/settings/components/phone/phone-number-form.tsx b/app/settings/components/phone/phone-number-form.tsx new file mode 100644 index 0000000..7ee33f5 --- /dev/null +++ b/app/settings/components/phone/phone-number-form.tsx @@ -0,0 +1,76 @@ +import { useEffect } from "react"; +import { useMutation, useQuery } from "blitz"; +import { useForm } from "react-hook-form"; + +import setPhoneNumber from "../../mutations/set-phone-number"; +import getAvailablePhoneNumbers from "../../queries/get-available-phone-numbers"; +import useCurrentUser from "app/core/hooks/use-current-user"; +import useUserPhoneNumber from "app/core/hooks/use-current-phone-number"; +import Button from "../button"; +import SettingsSection from "../settings-section"; + +type Form = { + phoneNumberSid: string; +}; + +export default function PhoneNumberForm() { + const { hasFilledTwilioCredentials } = useCurrentUser(); + const currentPhoneNumber = useUserPhoneNumber(); + const { + register, + handleSubmit, + setValue, + formState: { isSubmitting }, + } = useForm
(); + const [setPhoneNumberMutation] = useMutation(setPhoneNumber); + const [availablePhoneNumbers] = useQuery(getAvailablePhoneNumbers, {}, { enabled: hasFilledTwilioCredentials }); + + useEffect(() => { + if (currentPhoneNumber) { + setValue("phoneNumberSid", currentPhoneNumber.id); + } + }, [currentPhoneNumber]); + + const onSubmit = handleSubmit(async ({ phoneNumberSid }) => { + if (isSubmitting) { + return; + } + + await setPhoneNumberMutation({ phoneNumberSid }); + }); + + if (!hasFilledTwilioCredentials) { + return null; + } + + return ( + + + + + } + > + + + +
+ ); +} diff --git a/app/settings/components/phone/twilio-api-form.tsx b/app/settings/components/phone/twilio-api-form.tsx new file mode 100644 index 0000000..d26c09b --- /dev/null +++ b/app/settings/components/phone/twilio-api-form.tsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; +import { useMutation } from "blitz"; +import { useForm } from "react-hook-form"; +import { IoHelpCircle } from "react-icons/io5"; + +import setTwilioApiFields from "../../mutations/set-twilio-api-fields"; +import useCurrentUser from "app/core/hooks/use-current-user"; +import HelpModal from "./help-modal"; +import Button from "../button"; +import SettingsSection from "../settings-section"; + +type Form = { + twilioAccountSid: string; + twilioAuthToken: string; +}; + +export default function TwilioApiForm() { + const { + register, + handleSubmit, + setValue, + formState: { isSubmitting }, + } = useForm
(); + const { organization, refetch } = useCurrentUser(); + const [setTwilioApiFieldsMutation] = useMutation(setTwilioApiFields); + const [isHelpModalOpen, setIsHelpModalOpen] = useState(false); + + useEffect(() => { + setValue("twilioAuthToken", organization?.twilioAuthToken ?? ""); + setValue("twilioAccountSid", organization?.twilioAccountSid ?? ""); + }, [setValue, organization?.twilioAuthToken, organization?.twilioAccountSid]); + + const onSubmit = handleSubmit(async ({ twilioAccountSid, twilioAuthToken }) => { + if (isSubmitting) { + return; + } + + await setTwilioApiFieldsMutation({ twilioAccountSid, twilioAuthToken }); + await refetch(); + }); + + return ( + <> + + + + + } + > + +
+ Shellphone needs some informations about your Twilio account to securely use your phone numbers. +
+ +
+ + +
+
+ + +
+
+
+ + setIsHelpModalOpen(false)} isHelpModalOpen={isHelpModalOpen} /> + + ); +} diff --git a/app/onboarding/mutations/set-phone-number.ts b/app/settings/mutations/set-phone-number.ts similarity index 93% rename from app/onboarding/mutations/set-phone-number.ts rename to app/settings/mutations/set-phone-number.ts index 0f1167c..4b04df9 100644 --- a/app/onboarding/mutations/set-phone-number.ts +++ b/app/settings/mutations/set-phone-number.ts @@ -2,8 +2,8 @@ import { resolver } from "blitz"; import { z } from "zod"; import twilio from "twilio"; -import db from "../../../db"; -import getCurrentUser from "../../users/queries/get-current-user"; +import db from "db"; +import getCurrentUser from "app/users/queries/get-current-user"; import setTwilioWebhooks from "../api/queue/set-twilio-webhooks"; const Body = z.object({ diff --git a/app/onboarding/mutations/set-twilio-api-fields.ts b/app/settings/mutations/set-twilio-api-fields.ts similarity index 64% rename from app/onboarding/mutations/set-twilio-api-fields.ts rename to app/settings/mutations/set-twilio-api-fields.ts index 3bd4b31..26d6fe5 100644 --- a/app/onboarding/mutations/set-twilio-api-fields.ts +++ b/app/settings/mutations/set-twilio-api-fields.ts @@ -1,8 +1,8 @@ import { resolver } from "blitz"; import { z } from "zod"; -import db from "../../../db"; -import getCurrentUser from "../../users/queries/get-current-user"; +import db from "db"; +import getCurrentUser from "app/users/queries/get-current-user"; const Body = z.object({ twilioAccountSid: z.string(), @@ -26,5 +26,17 @@ export default resolver.pipe( twilioAuthToken: twilioAuthToken, }, }); + + const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId } }); + if (phoneNumber) { + await db.phoneNumber.delete({ + where: { + organizationId_id: { + organizationId, + id: phoneNumber.id, + }, + }, + }); + } }, ); diff --git a/app/settings/pages/settings/phone.tsx b/app/settings/pages/settings/phone.tsx index 4a9f8c9..7ebdce1 100644 --- a/app/settings/pages/settings/phone.tsx +++ b/app/settings/pages/settings/phone.tsx @@ -1,12 +1,26 @@ +import { Suspense } from "react"; import type { BlitzPage } from "blitz"; -import { Routes } from "blitz"; +import { Routes, dynamic } from "blitz"; import SettingsLayout from "../../components/settings-layout"; +import PhoneNumberForm from "../../components/phone/phone-number-form"; const PhoneSettings: BlitzPage = () => { - return
Coming soon
; + return ( +
+ + + + +
+ ); }; +const TwilioApiForm = dynamic(() => import("../../components/phone/twilio-api-form"), { + ssr: false, + loading: () => null, +}); + PhoneSettings.getLayout = (page) => {page}; PhoneSettings.authenticate = { redirectTo: Routes.SignIn() }; diff --git a/app/settings/queries/get-available-phone-numbers.ts b/app/settings/queries/get-available-phone-numbers.ts new file mode 100644 index 0000000..e35c094 --- /dev/null +++ b/app/settings/queries/get-available-phone-numbers.ts @@ -0,0 +1,21 @@ +import { NotFoundError, resolver } from "blitz"; + +import db from "db"; +import twilio from "twilio"; + +export default resolver.pipe(resolver.authorize(), async (_ = null, { session }) => { + if (!session.orgId) { + throw new NotFoundError(); + } + + const organization = await db.organization.findFirst({ where: { id: session.orgId } }); + if (!organization || !organization.twilioAccountSid || !organization.twilioAuthToken) { + throw new NotFoundError(); + } + + const incomingPhoneNumbers = await twilio( + organization.twilioAccountSid, + organization.twilioAuthToken, + ).incomingPhoneNumbers.list(); + return incomingPhoneNumbers.map(({ phoneNumber, sid }) => ({ phoneNumber, sid })); +});