diff --git a/app/phone-calls/api/webhook/incoming-call.ts b/app/phone-calls/api/webhook/incoming-call.ts
index e31b6b5..d7249e2 100644
--- a/app/phone-calls/api/webhook/incoming-call.ts
+++ b/app/phone-calls/api/webhook/incoming-call.ts
@@ -1,3 +1,43 @@
-import type { NextApiRequest, NextApiResponse } from "next";
+import type { BlitzApiRequest, BlitzApiResponse } from "blitz";
+import Twilio from "twilio";
-export default async function incomingCallHandler(req: NextApiRequest, res: NextApiResponse) {}
+import db from "../../../../db";
+
+export default async function incomingCallHandler(req: BlitzApiRequest, res: BlitzApiResponse) {
+ console.log("req.body", req.body);
+
+ const isOutgoingCall = true;
+ if (isOutgoingCall) {
+ const recipient = req.body.To;
+ const organizationId = req.body.From.slice("client:".length).split("__")[0];
+ const phoneNumber = await db.phoneNumber.findFirst({
+ where: { organizationId },
+ select: { number: true },
+ });
+ const twiml = new Twilio.twiml.VoiceResponse();
+ const dial = twiml.dial({
+ answerOnBridge: true,
+ callerId: phoneNumber!.number,
+ });
+ dial.number(recipient);
+ console.log("twiml", twiml.toString());
+
+ res.setHeader("content-type", "text/xml");
+ return res.status(200).send(twiml.toString());
+ }
+
+ res.status(500).end();
+}
+
+const outgoingBody = {
+ AccountSid: "ACa886d066be0832990d1cf43fb1d53362",
+ ApiVersion: "2010-04-01",
+ ApplicationSid: "AP6334c6dd54f5808717b37822de4e4e14",
+ CallSid: "CA3b639875693fd8f563e07937780c9f5f",
+ CallStatus: "ringing",
+ Called: "",
+ Caller: "client:95267d60-3d35-4c36-9905-8543ecb4f174__673b461a-11ba-43a4-89d7-9e29403053d4",
+ Direction: "inbound",
+ From: "client:95267d60-3d35-4c36-9905-8543ecb4f174__673b461a-11ba-43a4-89d7-9e29403053d4",
+ To: "+33613370787",
+};
diff --git a/app/phone-calls/mutations/get-token.ts b/app/phone-calls/mutations/get-token.ts
new file mode 100644
index 0000000..926b9c8
--- /dev/null
+++ b/app/phone-calls/mutations/get-token.ts
@@ -0,0 +1,40 @@
+import { resolver, NotFoundError } from "blitz";
+import Twilio from "twilio";
+
+import db from "db";
+import getCurrentPhoneNumber from "../../phone-numbers/queries/get-current-phone-number";
+
+export default resolver.pipe(resolver.authorize(), async (_ = null, context) => {
+ const phoneNumber = await getCurrentPhoneNumber({}, context);
+ if (!phoneNumber) {
+ throw new NotFoundError();
+ }
+
+ const organization = await db.organization.findFirst({
+ where: { id: phoneNumber.organizationId },
+ });
+ if (
+ !organization ||
+ !organization.twilioAccountSid ||
+ !organization.twilioAuthToken ||
+ !organization.twilioApiKey ||
+ !organization.twilioApiSecret ||
+ !organization.twimlAppSid
+ ) {
+ throw new NotFoundError();
+ }
+
+ const accessToken = new Twilio.jwt.AccessToken(
+ organization.twilioAccountSid,
+ organization.twilioApiKey,
+ organization.twilioApiSecret,
+ { identity: `${context.session.orgId}__${context.session.userId}` },
+ );
+ const grant = new Twilio.jwt.AccessToken.VoiceGrant({
+ outgoingApplicationSid: organization.twimlAppSid,
+ incomingAllow: true,
+ });
+ accessToken.addGrant(grant);
+
+ return accessToken.toJwt();
+});
diff --git a/app/keypad/pages/keypad.tsx b/app/phone-calls/pages/keypad.tsx
similarity index 91%
rename from app/keypad/pages/keypad.tsx
rename to app/phone-calls/pages/keypad.tsx
index 4ac72c0..18d7e20 100644
--- a/app/keypad/pages/keypad.tsx
+++ b/app/phone-calls/pages/keypad.tsx
@@ -1,5 +1,5 @@
import type { BlitzPage } from "blitz";
-import { Routes } from "blitz";
+import { Link, Routes } from "blitz";
import type { FunctionComponent } from "react";
import { useRef } from "react";
import { atom, useAtom } from "jotai";
@@ -59,9 +59,11 @@ const Keypad: BlitzPage = () => {
-
-
-
+
+
+
+
+
diff --git a/app/phone-calls/pages/outgoing-call/[recipient].tsx b/app/phone-calls/pages/outgoing-call/[recipient].tsx
new file mode 100644
index 0000000..b0ba02c
--- /dev/null
+++ b/app/phone-calls/pages/outgoing-call/[recipient].tsx
@@ -0,0 +1,215 @@
+import type { BlitzPage } from "blitz";
+import { Routes, useMutation, useRouter } from "blitz";
+import type { FunctionComponent } from "react";
+import { useEffect, useRef, useState } from "react";
+import { atom, useAtom } from "jotai";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faPhoneAlt as faPhone } from "@fortawesome/pro-solid-svg-icons";
+import { usePress } from "@react-aria/interactions";
+import type { TwilioError } from "@twilio/voice-sdk";
+import { Device, Call } from "@twilio/voice-sdk";
+
+import getToken from "../../mutations/get-token";
+import useRequireOnboarding from "../../../core/hooks/use-require-onboarding";
+
+const OutgoingCall: BlitzPage = () => {
+ const router = useRouter();
+ const recipient = decodeURIComponent(router.params.recipient);
+ const [device, setDevice] = useState(null);
+ const [getTokenMutation] = useMutation(getToken);
+ async function makeCall() {
+ const token = await getTokenMutation();
+ console.log("token", token);
+ const device = new Device(token, { codecPreferences: [Call.Codec.Opus, Call.Codec.PCMU] });
+ setDevice(device);
+
+ const params = { To: recipient };
+ const outgoingConnection = await device.connect({ params });
+ // @ts-ignore
+ window.ddd = outgoingConnection;
+
+ /*$("[id^=dial-]").click(function (event) {
+ console.log("send digit", event.target.innerText);
+ outgoingConnection.sendDigits(event.target.innerText);
+ })*/
+
+ outgoingConnection.on("ringing", () => {
+ console.log("Ringing...");
+ });
+ }
+
+ useEffect(() => {
+ if (!device) {
+ return;
+ }
+
+ device.on("ready", onDeviceReady);
+ device.on("error", onDeviceError);
+ device.on("register", onDeviceRegistered);
+ device.on("unregister", onDeviceUnregistered);
+ device.on("incoming", onDeviceIncoming);
+ // device.audio?.on('deviceChange', updateAllDevices.bind(device));
+
+ return () => {
+ device.off("ready", onDeviceReady);
+ device.off("error", onDeviceError);
+ device.off("register", onDeviceRegistered);
+ device.off("unregister", onDeviceUnregistered);
+ device.off("incoming", onDeviceIncoming);
+ };
+ }, [device]);
+
+ useRequireOnboarding();
+ const phoneNumber = useAtom(phoneNumberAtom)[0];
+
+ return (
+
+
+ {phoneNumber}
+
+
+
+
+
+
+ ABC
+
+
+ DEF
+
+
+
+
+ GHI
+
+
+ JKL
+
+
+ MNO
+
+
+
+
+ PQRS
+
+
+ TUV
+
+
+ WXYZ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ZeroDigit: FunctionComponent = () => {
+ const timeoutRef = useRef | null>(null);
+ const pressDigit = useAtom(pressDigitAtom)[1];
+ const longPressDigit = useAtom(longPressDigitAtom)[1];
+ const { pressProps } = usePress({
+ onPressStart() {
+ pressDigit("0");
+ timeoutRef.current = setTimeout(() => {
+ longPressDigit("+");
+ }, 750);
+ },
+ onPressEnd() {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ },
+ });
+
+ return (
+
+ 0 +
+
+ );
+};
+
+const Row: FunctionComponent = ({ children }) => (
+ {children}
+);
+
+const Digit: FunctionComponent<{ digit: string }> = ({ children, digit }) => {
+ const pressDigit = useAtom(pressDigitAtom)[1];
+ const { pressProps } = usePress({
+ onPress() {
+ pressDigit(digit);
+ },
+ });
+
+ return (
+
+ {digit}
+ {children}
+
+ );
+};
+
+const DigitLetters: FunctionComponent = ({ children }) => {children}
;
+
+const phoneNumberAtom = atom("");
+const pressDigitAtom = atom(null, (get, set, digit: string) => {
+ if (get(phoneNumberAtom).length > 17) {
+ return;
+ }
+
+ set(phoneNumberAtom, (prevState) => prevState + digit);
+});
+const longPressDigitAtom = atom(null, (get, set, replaceWith: string) => {
+ if (get(phoneNumberAtom).length > 17) {
+ return;
+ }
+
+ set(phoneNumberAtom, (prevState) => prevState.slice(0, -1) + replaceWith);
+});
+
+OutgoingCall.authenticate = { redirectTo: Routes.SignIn() };
+
+function onDeviceReady(device: Device) {
+ console.log("device", device);
+}
+
+function onDeviceError(error: TwilioError.TwilioError, call?: Call) {
+ console.log("error", error);
+}
+
+function onDeviceRegistered(device: Device) {
+ console.log("ready to make calls");
+ console.log("device", device);
+}
+
+function onDeviceUnregistered() {}
+
+function onDeviceIncoming(call: Call) {
+ console.log("call", call);
+ console.log("Incoming connection from " + call.parameters.From);
+ let archEnemyPhoneNumber = "+12093373517";
+
+ if (call.parameters.From === archEnemyPhoneNumber) {
+ call.reject();
+ console.log("It's your nemesis. Rejected call.");
+ } else {
+ // accept the incoming connection and start two-way audio
+ call.accept();
+ }
+}
+
+export default OutgoingCall;
diff --git a/app/phone-numbers/queries/get-current-phone-number.ts b/app/phone-numbers/queries/get-current-phone-number.ts
index 601e9ab..1762491 100644
--- a/app/phone-numbers/queries/get-current-phone-number.ts
+++ b/app/phone-numbers/queries/get-current-phone-number.ts
@@ -14,6 +14,7 @@ export default resolver.pipe(
where: { organizationId },
select: {
id: true,
+ organizationId: true,
number: true,
},
});