From f1702180b7ba90c3636b6a5e5f25e0995db15000 Mon Sep 17 00:00:00 2001 From: m5r Date: Sat, 11 Jun 2022 19:29:58 +0200 Subject: [PATCH] make phone calls --- .../service-worker-update-notifier.tsx | 4 +++ app/features/phone-calls/hooks/use-device.ts | 31 ++++++++++++------- .../phone-calls/hooks/use-make-call.ts | 1 + .../phone-calls/loaders/twilio-token.ts | 12 +++---- app/routes/__app/outgoing-call.$recipient.tsx | 8 ++++- app/routes/{webhooks => webhook}/call.ts | 9 +++--- app/routes/{webhooks => webhook}/message.ts | 0 app/service-worker/cache-utils.ts | 4 +++ app/utils/twilio.server.ts | 4 +-- 9 files changed, 49 insertions(+), 24 deletions(-) rename app/routes/{webhooks => webhook}/call.ts (92%) rename app/routes/{webhooks => webhook}/message.ts (100%) diff --git a/app/features/core/components/service-worker-update-notifier.tsx b/app/features/core/components/service-worker-update-notifier.tsx index f23a4b3..1e46746 100644 --- a/app/features/core/components/service-worker-update-notifier.tsx +++ b/app/features/core/components/service-worker-update-notifier.tsx @@ -4,6 +4,10 @@ import { IoDownloadOutline } from "react-icons/io5"; export default function ServiceWorkerUpdateNotifier() { const [hasUpdate, setHasUpdate] = useState(false); useEffect(() => { + if (!("serviceWorker" in navigator)) { + return; + } + (async () => { const registration = await navigator.serviceWorker.getRegistration(); if (!registration) { diff --git a/app/features/phone-calls/hooks/use-device.ts b/app/features/phone-calls/hooks/use-device.ts index c95621f..8a34023 100644 --- a/app/features/phone-calls/hooks/use-device.ts +++ b/app/features/phone-calls/hooks/use-device.ts @@ -1,26 +1,26 @@ import { useEffect, useState } from "react"; -import { type TwilioError, Call, Device } from "@twilio/voice-sdk"; import { useFetcher } from "@remix-run/react"; +import { type TwilioError, Call, Device } from "@twilio/voice-sdk"; +import { useAtom, atom } from "jotai"; import type { TwilioTokenLoaderData } from "~/features/phone-calls/loaders/twilio-token"; export default function useDevice() { const jwt = useDeviceToken(); - const [device, setDevice] = useState(null); - const [isDeviceReady, setIsDeviceReady] = useState(() => device?.state === Device.State.Registered); + const [device, setDevice] = useAtom(deviceAtom); + const [isDeviceReady, setIsDeviceReady] = useState(device?.state === Device.State.Registered); useEffect(() => { + // init token jwt.refresh(); }, []); useEffect(() => { - if (jwt.token && device?.state === Device.State.Registered && device?.token !== jwt.token) { - device.updateToken(jwt.token); + // init device + if (!jwt.token) { + return; } - }, [jwt.token, device]); - - useEffect(() => { - if (!jwt.token || device?.state === Device.State.Registered) { + if (device && device.state !== Device.State.Unregistered) { return; } @@ -30,9 +30,16 @@ export default function useDevice() { [Device.SoundName.Disconnect]: undefined, // TODO }, }); - newDevice.register(); // TODO throwing an error + newDevice.register(); setDevice(newDevice); - }, [device?.state, jwt.token, setDevice]); + }, [device, jwt.token]); + + useEffect(() => { + // refresh token + if (jwt.token && device?.state === Device.State.Registered && device?.token !== jwt.token) { + device.updateToken(jwt.token); + } + }, [device, jwt.token]); useEffect(() => { if (!device) { @@ -100,6 +107,8 @@ export default function useDevice() { } } +const deviceAtom = atom(null); + function useDeviceToken() { const fetcher = useFetcher(); diff --git a/app/features/phone-calls/hooks/use-make-call.ts b/app/features/phone-calls/hooks/use-make-call.ts index d98be58..3ca81c4 100644 --- a/app/features/phone-calls/hooks/use-make-call.ts +++ b/app/features/phone-calls/hooks/use-make-call.ts @@ -32,6 +32,7 @@ export default function useMakeCall({ recipient, onHangUp }: Params) { const makeCall = useCallback( async function makeCall() { + console.log({ device, isDeviceReady }); if (!device || !isDeviceReady) { console.warn("device is not ready yet, can't make the call"); return; diff --git a/app/features/phone-calls/loaders/twilio-token.ts b/app/features/phone-calls/loaders/twilio-token.ts index 85fd339..988e85e 100644 --- a/app/features/phone-calls/loaders/twilio-token.ts +++ b/app/features/phone-calls/loaders/twilio-token.ts @@ -2,7 +2,7 @@ import { type LoaderFunction } from "@remix-run/node"; import Twilio from "twilio"; import { refreshSessionData, requireLoggedIn } from "~/utils/auth.server"; -import { encrypt } from "~/utils/encryption"; +import { decrypt, encrypt } from "~/utils/encryption"; import db from "~/utils/db.server"; import { commitSession } from "~/utils/session.server"; import getTwilioClient from "~/utils/twilio.server"; @@ -10,7 +10,7 @@ import getTwilioClient from "~/utils/twilio.server"; export type TwilioTokenLoaderData = string; const loader: LoaderFunction = async ({ request }) => { - const { user, organization, twilio } = await requireLoggedIn(request); + const { user, twilio } = await requireLoggedIn(request); if (!twilio) { throw new Error("unreachable"); } @@ -39,15 +39,15 @@ const loader: LoaderFunction = async ({ request }) => { shouldRefreshSession = true; const apiKey = await twilioClient.newKeys.create({ friendlyName: "Shellphone" }); apiKeySid = apiKey.sid; - apiKeySecret = apiKey.secret; + apiKeySecret = encrypt(apiKey.secret); await db.twilioAccount.update({ where: { accountSid: twilioAccount.accountSid }, - data: { apiKeySid: apiKey.sid, apiKeySecret: encrypt(apiKey.secret) }, + data: { apiKeySid, apiKeySecret }, }); } - const accessToken = new Twilio.jwt.AccessToken(twilioAccount.accountSid, apiKeySid, apiKeySecret, { - identity: `${organization.id}__${user.id}`, + const accessToken = new Twilio.jwt.AccessToken(twilioAccount.accountSid, apiKeySid, decrypt(apiKeySecret), { + identity: `${twilio.accountSid}__${user.id}`, ttl: 3600, }); const grant = new Twilio.jwt.AccessToken.VoiceGrant({ diff --git a/app/routes/__app/outgoing-call.$recipient.tsx b/app/routes/__app/outgoing-call.$recipient.tsx index 1bbe1d7..5f7a574 100644 --- a/app/routes/__app/outgoing-call.$recipient.tsx +++ b/app/routes/__app/outgoing-call.$recipient.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; import type { MetaFunction } from "@remix-run/node"; import { useParams } from "@remix-run/react"; import { IoCall } from "react-icons/io5"; @@ -38,6 +38,12 @@ export default function OutgoingCallPage() { [call, pressDigit], ); + useEffect(() => { + if (isDeviceReady) { + call.makeCall(); + } + }, [call, isDeviceReady]); + return (
diff --git a/app/routes/webhooks/call.ts b/app/routes/webhook/call.ts similarity index 92% rename from app/routes/webhooks/call.ts rename to app/routes/webhook/call.ts index 6ddf9c7..02efc01 100644 --- a/app/routes/webhooks/call.ts +++ b/app/routes/webhook/call.ts @@ -15,14 +15,14 @@ export const action: ActionFunction = async ({ request }) => { return badRequest("Invalid header X-Twilio-Signature"); } - const body: Body = await request.json(); + const body: Body = Object.fromEntries(await request.formData()) as any; const isOutgoingCall = body.From.startsWith("client:"); if (isOutgoingCall) { const recipient = body.To; - const organizationId = body.From.slice("client:".length).split("__")[0]; + const accountSid = body.From.slice("client:".length).split("__")[0]; try { - const twilioAccount = await db.twilioAccount.findUnique({ where: { organizationId } }); + const twilioAccount = await db.twilioAccount.findUnique({ where: { accountSid } }); if (!twilioAccount) { // this shouldn't be happening return new Response(null, { status: 402 }); @@ -57,7 +57,8 @@ export const action: ActionFunction = async ({ request }) => { if (phoneNumber?.twilioAccount.organization.subscriptions.length === 0) { // decline the outgoing call because // the organization is on the free plan - return new Response(null, { status: 402 }); + console.log("no active subscription"); // TODO: uncomment the line below + // return new Response(null, { status: 402 }); } const encryptedAuthToken = phoneNumber?.twilioAccount.authToken; diff --git a/app/routes/webhooks/message.ts b/app/routes/webhook/message.ts similarity index 100% rename from app/routes/webhooks/message.ts rename to app/routes/webhook/message.ts diff --git a/app/service-worker/cache-utils.ts b/app/service-worker/cache-utils.ts index 11e0a01..4b16584 100644 --- a/app/service-worker/cache-utils.ts +++ b/app/service-worker/cache-utils.ts @@ -59,6 +59,10 @@ const lastTimeRevalidated: Record = {}; export function fetchLoaderData(event: FetchEvent): Promise { const url = new URL(event.request.url); + if (url.pathname === "/outgoing-call/twilio-token") { + return fetch(event.request); + } + const path = url.pathname + url.search; return caches.match(event.request, { cacheName: DATA_CACHE }).then((cachedResponse) => { diff --git a/app/utils/twilio.server.ts b/app/utils/twilio.server.ts index 6361129..e610d07 100644 --- a/app/utils/twilio.server.ts +++ b/app/utils/twilio.server.ts @@ -17,9 +17,9 @@ export default function getTwilioClient({ return twilio(accountSid, decrypt(authToken)); } -export const smsUrl = `https://${serverConfig.app.baseUrl}/webhook/message`; +export const smsUrl = `${serverConfig.app.baseUrl}/webhook/message`; -export const voiceUrl = `https://${serverConfig.app.baseUrl}/webhook/call`; +export const voiceUrl = `${serverConfig.app.baseUrl}/webhook/call`; export function getTwiMLName() { switch (serverConfig.app.baseUrl) {