diff --git a/app/phone-calls/hooks/use-device.tsx b/app/phone-calls/hooks/use-device.tsx index e3a0bd9..117010c 100644 --- a/app/phone-calls/hooks/use-device.tsx +++ b/app/phone-calls/hooks/use-device.tsx @@ -1,16 +1,16 @@ -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useMutation } from "blitz"; import type { TwilioError } from "@twilio/voice-sdk"; import { Call, Device } from "@twilio/voice-sdk"; -import { atom, useAtom } from "jotai"; -import getToken, { ttl } from "../mutations/get-token"; +import getToken from "../mutations/get-token"; import appLogger from "../../../integrations/logger"; const logger = appLogger.child({ module: "use-device" }); export default function useDevice() { - const [device, setDevice] = useAtom(deviceAtom); + const [device, setDevice] = useState(null); + const [isDeviceReady, setIsDeviceReady] = useState(() => device?.state === Device.State.Registered); const [getTokenMutation] = useMutation(getToken); const refreshToken = useCallback(async () => { if (!device) { @@ -21,16 +21,13 @@ export default function useDevice() { const token = await getTokenMutation(); device.updateToken(token); }, [device, getTokenMutation]); - const isDeviceReady = useMemo(() => device?.state === Device.State.Registered, [device]); useEffect(() => { - if (!isDeviceReady) { - return; - } - - const intervalId = setInterval(refreshToken, (ttl - 30) * 1000); + const intervalId = setInterval(() => { + return refreshToken(); + }, (3600 - 30) * 1000); return () => clearInterval(intervalId); - }, [isDeviceReady, refreshToken]); + }, [refreshToken]); useEffect(() => { (async () => { @@ -51,31 +48,39 @@ export default function useDevice() { return; } - console.log("ok"); - // @ts-ignore - window.device = device; + device.on("registered", onDeviceRegistered); + device.on("unregistered", onDeviceUnregistered); device.on("error", onDeviceError); device.on("incoming", onDeviceIncoming); return () => { + device.off("registered", onDeviceRegistered); + device.off("unregistered", onDeviceUnregistered); device.off("error", onDeviceError); device.off("incoming", onDeviceIncoming); }; }, [device]); - // @ts-ignore - window.refreshToken = refreshToken; - return { device, isDeviceReady, refreshToken, }; + function onDeviceRegistered() { + setIsDeviceReady(true); + } + + function onDeviceUnregistered() { + setIsDeviceReady(false); + } + function onDeviceError(error: TwilioError.TwilioError, call?: Call) { - // TODO gracefully handle errors: possibly hang up the call, redirect to keypad - console.error("device error", JSON.parse(JSON.stringify(error))); - alert(error.code); + // we might have to change this if we instantiate the device on every page to receive calls + setDevice(() => { + // hack to trigger the error boundary + throw error; + }); } function onDeviceIncoming(call: Call) { @@ -94,8 +99,6 @@ export default function useDevice() { } } -const deviceAtom = atom(null); - let e = { message: "ConnectionError (53000): Raised whenever a signaling connection error occurs that is not covered by a more specific error code.", diff --git a/app/phone-calls/hooks/use-make-call.ts b/app/phone-calls/hooks/use-make-call.ts index 84e7c91..b14a8b5 100644 --- a/app/phone-calls/hooks/use-make-call.ts +++ b/app/phone-calls/hooks/use-make-call.ts @@ -1,9 +1,13 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useRouter, Routes } from "blitz"; import { Call } from "@twilio/voice-sdk"; import useDevice from "./use-device"; +import appLogger from "../../../integrations/logger"; + +const logger = appLogger.child({ module: "use-make-call" }); + type Params = { recipient: string; onHangUp?: () => void; @@ -15,69 +19,85 @@ export default function useMakeCall({ recipient, onHangUp }: Params) { const { device, isDeviceReady } = useDevice(); const router = useRouter(); + const endCall = useCallback( + function endCall() { + outgoingConnection?.off("cancel", endCall); + outgoingConnection?.off("disconnect", endCall); + outgoingConnection?.disconnect(); + + setState("call_ending"); + setTimeout(() => { + setState("call_ended"); + setTimeout(() => router.replace(Routes.KeypadPage()), 100); + }, 150); + }, + [outgoingConnection, router], + ); + + const makeCall = useCallback( + async function makeCall() { + if (!device || !isDeviceReady) { + logger.warn("device is not ready yet, can't make the call"); + return; + } + + if (state !== "initial") { + return; + } + + if (device.isBusy) { + logger.error("device is busy, this shouldn't happen"); + return; + } + + setState("calling"); + + const params = { To: recipient }; + const outgoingConnection = await device.connect({ params }); + setOutgoingConnection(outgoingConnection); + + outgoingConnection.on("error", (error) => { + outgoingConnection.off("cancel", endCall); + outgoingConnection.off("disconnect", endCall); + setState(() => { + // hack to trigger the error boundary + throw error; + }); + }); + outgoingConnection.once("accept", (call: Call) => setState("call_in_progress")); + outgoingConnection.on("cancel", endCall); + outgoingConnection.on("disconnect", endCall); + }, + [device, isDeviceReady, recipient, state], + ); + + const sendDigits = useCallback( + function sendDigits(digits: string) { + return outgoingConnection?.sendDigits(digits); + }, + [outgoingConnection], + ); + + const hangUp = useCallback( + function hangUp() { + setState("call_ending"); + outgoingConnection?.disconnect(); + device?.disconnectAll(); + device?.destroy(); + onHangUp?.(); + router.replace(Routes.KeypadPage()); + outgoingConnection?.off("cancel", endCall); + outgoingConnection?.off("disconnect", endCall); + }, + [device, endCall, onHangUp, outgoingConnection, router], + ); + return { makeCall, sendDigits, hangUp, state, }; - - async function makeCall() { - if (!device || !isDeviceReady) { - console.error("device is not ready yet, can't make the call"); - return; - } - - if (state !== "initial") { - return; - } - - setState("calling"); - - const params = { To: recipient }; - const outgoingConnection = await device.connect({ params }); - setOutgoingConnection(outgoingConnection); - // @ts-ignore - window.ddd = outgoingConnection; - - outgoingConnection.on("error", (error) => { - outgoingConnection.off("cancel", endCall); - outgoingConnection.off("disconnect", endCall); - setState(() => { - // hack to trigger the error boundary - throw error; - }); - }); - outgoingConnection.once("accept", (call: Call) => setState("call_in_progress")); - outgoingConnection.on("cancel", endCall); - outgoingConnection.on("disconnect", endCall); - } - - function endCall() { - outgoingConnection?.off("cancel", endCall); - outgoingConnection?.off("disconnect", endCall); - outgoingConnection?.disconnect(); - - setState("call_ending"); - setTimeout(() => { - setState("call_ended"); - setTimeout(() => router.replace(Routes.KeypadPage()), 100); - }, 150); - } - - function sendDigits(digits: string) { - return outgoingConnection?.sendDigits(digits); - } - - function hangUp() { - setState("call_ending"); - outgoingConnection?.disconnect(); - device?.disconnectAll(); - onHangUp?.(); - router.replace(Routes.KeypadPage()); - outgoingConnection?.off("cancel", endCall); - outgoingConnection?.off("disconnect", endCall); - } } type State = "initial" | "ready" | "calling" | "call_in_progress" | "call_ending" | "call_ended"; diff --git a/app/phone-calls/mutations/get-token.ts b/app/phone-calls/mutations/get-token.ts index cd0d1e2..0b458b6 100644 --- a/app/phone-calls/mutations/get-token.ts +++ b/app/phone-calls/mutations/get-token.ts @@ -4,8 +4,6 @@ import Twilio from "twilio"; import db from "db"; import getCurrentPhoneNumber from "../../phone-numbers/queries/get-current-phone-number"; -export const ttl = 3600; - export default resolver.pipe(resolver.authorize(), async (_ = null, context) => { const phoneNumber = await getCurrentPhoneNumber({}, context); if (!phoneNumber) { @@ -30,7 +28,7 @@ export default resolver.pipe(resolver.authorize(), async (_ = null, context) => organization.twilioAccountSid, organization.twilioApiKey, organization.twilioApiSecret, - { identity: `${context.session.orgId}__${context.session.userId}` }, + { identity: `${context.session.orgId}__${context.session.userId}`, ttl: 3600 }, ); const grant = new Twilio.jwt.AccessToken.VoiceGrant({ outgoingApplicationSid: organization.twimlAppSid, diff --git a/app/phone-calls/pages/outgoing-call/[recipient].tsx b/app/phone-calls/pages/outgoing-call/[recipient].tsx index be705ab..d6aa6eb 100644 --- a/app/phone-calls/pages/outgoing-call/[recipient].tsx +++ b/app/phone-calls/pages/outgoing-call/[recipient].tsx @@ -16,10 +16,10 @@ const OutgoingCall: BlitzPage = () => { useRequireOnboarding(); const [phoneNumber, setPhoneNumber] = useAtom(phoneNumberAtom); const router = useRouter(); - const { isDeviceReady } = useDevice(); const recipient = decodeURIComponent(router.params.recipient); const onHangUp = useCallback(() => setPhoneNumber(""), [setPhoneNumber]); const call = useMakeCall({ recipient, onHangUp }); + const { isDeviceReady } = useDevice(); const pressDigit = useAtom(pressDigitAtom)[1]; const onDigitPressProps = useCallback( (digit: string) => ({ @@ -36,7 +36,7 @@ const OutgoingCall: BlitzPage = () => { if (isDeviceReady) { call.makeCall(); } - }, [isDeviceReady]); + }, [call, isDeviceReady]); return (