send notification when sms arrives
This commit is contained in:
parent
1489f97c14
commit
fef4c03458
@ -33,7 +33,7 @@ export default async function ddd(req: BlitzApiRequest, res: BlitzApiResponse) {
|
||||
// console.log(JSON.stringify(Object.keys(error)));
|
||||
}*/
|
||||
/*const ddd = await twilio(accountSid, authToken).messages.create({
|
||||
body: "content",
|
||||
body: "cccccasdasd",
|
||||
to: "+33757592025",
|
||||
from: "+33757592722",
|
||||
});*/
|
||||
|
66
app/core/hooks/use-notifications.ts
Normal file
66
app/core/hooks/use-notifications.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { getConfig, useMutation } from "blitz";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import setNotificationSubscription from "../mutations/set-notification-subscription";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
export default function useNotifications() {
|
||||
const isServiceWorkerSupported = "serviceWorker" in navigator;
|
||||
const [subscription, setSubscription] = useState<PushSubscription | null>(null);
|
||||
const [setNotificationSubscriptionMutation] = useMutation(setNotificationSubscription);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!isServiceWorkerSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
setSubscription(subscription);
|
||||
})();
|
||||
}, [isServiceWorkerSupported]);
|
||||
|
||||
async function subscribe() {
|
||||
if (!isServiceWorkerSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(publicRuntimeConfig.webPush.publicKey),
|
||||
});
|
||||
setSubscription(subscription);
|
||||
await setNotificationSubscriptionMutation({ subscription: subscription.toJSON() as any }); // TODO remove as any
|
||||
}
|
||||
|
||||
async function unsubscribe() {
|
||||
if (!isServiceWorkerSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
return subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
return {
|
||||
isServiceWorkerSupported,
|
||||
subscription,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
};
|
||||
}
|
||||
|
||||
function urlBase64ToUint8Array(base64String: string) {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/");
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
}
|
38
app/core/mutations/set-notification-subscription.ts
Normal file
38
app/core/mutations/set-notification-subscription.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { resolver } from "blitz";
|
||||
import { z } from "zod";
|
||||
|
||||
import db from "../../../db";
|
||||
import appLogger from "../../../integrations/logger";
|
||||
|
||||
const logger = appLogger.child({ mutation: "set-notification-subscription" });
|
||||
|
||||
const Body = z.object({
|
||||
subscription: z.object({
|
||||
endpoint: z.string(),
|
||||
expirationTime: z.number().nullable(),
|
||||
keys: z.object({
|
||||
p256dh: z.string(),
|
||||
auth: z.string(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({ subscription }, context) => {
|
||||
const customerId = context.session.userId;
|
||||
try {
|
||||
await db.notificationSubscription.create({
|
||||
data: {
|
||||
customerId,
|
||||
endpoint: subscription.endpoint,
|
||||
expirationTime: subscription.expirationTime,
|
||||
keys_p256dh: subscription.keys.p256dh,
|
||||
keys_auth: subscription.keys.auth,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code !== "P2002") {
|
||||
logger.error(error);
|
||||
// we might want to `throw error`;
|
||||
}
|
||||
}
|
||||
});
|
59
app/messages/api/queue/notify-incoming-message.ts
Normal file
59
app/messages/api/queue/notify-incoming-message.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { getConfig } from "blitz";
|
||||
import { Queue } from "quirrel/blitz";
|
||||
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
|
||||
import twilio from "twilio";
|
||||
import webpush, { PushSubscription, WebPushError } from "web-push";
|
||||
|
||||
import db from "../../../../db";
|
||||
import appLogger from "../../../../integrations/logger";
|
||||
|
||||
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
|
||||
const logger = appLogger.child({ queue: "notify-incoming-message" });
|
||||
|
||||
type Payload = {
|
||||
customerId: string;
|
||||
messageSid: MessageInstance["sid"];
|
||||
};
|
||||
|
||||
const notifyIncomingMessageQueue = Queue<Payload>(
|
||||
"api/queue/notify-incoming-message",
|
||||
async ({ messageSid, customerId }) => {
|
||||
webpush.setVapidDetails(
|
||||
"mailto:mokht@rmi.al",
|
||||
publicRuntimeConfig.webPush.publicKey,
|
||||
serverRuntimeConfig.webPush.privateKey,
|
||||
);
|
||||
|
||||
const customer = await db.customer.findFirst({ where: { id: customerId } });
|
||||
if (!customer || !customer.accountSid || !customer.authToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await twilio(customer.accountSid, customer.authToken).messages.get(messageSid).fetch();
|
||||
const notification = { message: `${message.from} - ${message.body}` };
|
||||
const subscriptions = await db.notificationSubscription.findMany({ where: { customerId: customer.id } });
|
||||
await Promise.all(
|
||||
subscriptions.map(async (subscription) => {
|
||||
const webPushSubscription: PushSubscription = {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: subscription.keys_p256dh,
|
||||
auth: subscription.keys_auth,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await webpush.sendNotification(webPushSubscription, JSON.stringify(notification));
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (error instanceof WebPushError) {
|
||||
// subscription most likely expired
|
||||
await db.notificationSubscription.delete({ where: { id: subscription.id } });
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default notifyIncomingMessageQueue;
|
@ -6,6 +6,7 @@ import type { ApiError } from "../../../api/_types";
|
||||
import appLogger from "../../../../integrations/logger";
|
||||
import db from "../../../../db";
|
||||
import insertIncomingMessageQueue from "../queue/insert-incoming-message";
|
||||
import notifyIncomingMessageQueue from "../queue/notify-incoming-message";
|
||||
|
||||
const logger = appLogger.child({ route: "/api/webhook/incoming-message" });
|
||||
const { serverRuntimeConfig } = getConfig();
|
||||
@ -70,16 +71,24 @@ export default async function incomingMessageHandler(req: BlitzApiRequest, res:
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: send notification
|
||||
|
||||
const messageSid = body.MessageSid;
|
||||
await insertIncomingMessageQueue.enqueue(
|
||||
{
|
||||
messageSid,
|
||||
customerId: customer.id,
|
||||
},
|
||||
{ id: messageSid },
|
||||
);
|
||||
const customerId = customer.id;
|
||||
await Promise.all([
|
||||
notifyIncomingMessageQueue.enqueue(
|
||||
{
|
||||
messageSid,
|
||||
customerId,
|
||||
},
|
||||
{ id: `notify-${messageSid}` },
|
||||
),
|
||||
insertIncomingMessageQueue.enqueue(
|
||||
{
|
||||
messageSid,
|
||||
customerId,
|
||||
},
|
||||
{ id: `insert-${messageSid}` },
|
||||
),
|
||||
]);
|
||||
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
|
@ -48,7 +48,7 @@ export default resolver.pipe(resolver.zod(Body), resolver.authorize(), async ({
|
||||
content,
|
||||
},
|
||||
{
|
||||
id: message.id,
|
||||
id: `insert-${message.id}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Suspense } from "react";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import type { BlitzPage } from "blitz";
|
||||
import { Routes } from "blitz";
|
||||
import { useAtom } from "jotai";
|
||||
@ -8,11 +8,19 @@ import ConversationsList from "../components/conversations-list";
|
||||
import NewMessageButton from "../components/new-message-button";
|
||||
import NewMessageBottomSheet, { bottomSheetOpenAtom } from "../components/new-message-bottom-sheet";
|
||||
import useRequireOnboarding from "../../core/hooks/use-require-onboarding";
|
||||
import useNotifications from "../../core/hooks/use-notifications";
|
||||
|
||||
const Messages: BlitzPage = () => {
|
||||
useRequireOnboarding();
|
||||
const { subscription, subscribe } = useNotifications();
|
||||
const setIsOpen = useAtom(bottomSheetOpenAtom)[1];
|
||||
|
||||
useEffect(() => {
|
||||
if (!subscription) {
|
||||
subscribe();
|
||||
}
|
||||
}, [subscription?.endpoint]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col space-y-6 p-6">
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { BlitzConfig, sessionMiddleware, simpleRolesIsAuthorized } from "blitz";
|
||||
|
||||
const withPWA = require("next-pwa");
|
||||
|
||||
const config: BlitzConfig = {
|
||||
middleware: [
|
||||
sessionMiddleware({
|
||||
@ -24,7 +26,15 @@ const config: BlitzConfig = {
|
||||
},
|
||||
masterEncryptionKey: process.env.MASTER_ENCRYPTION_KEY,
|
||||
app: {
|
||||
baseUrl: process.env.QUIRREL_BASE_URL,
|
||||
baseUrl: process.env.APP_BASE_URL,
|
||||
},
|
||||
webPush: {
|
||||
privateKey: process.env.WEB_PUSH_VAPID_PRIVATE_KEY,
|
||||
},
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
webPush: {
|
||||
publicKey: process.env.WEB_PUSH_VAPID_PUBLIC_KEY,
|
||||
},
|
||||
},
|
||||
/* Uncomment this to customize the webpack config
|
||||
@ -36,4 +46,11 @@ const config: BlitzConfig = {
|
||||
},
|
||||
*/
|
||||
};
|
||||
module.exports = config;
|
||||
|
||||
module.exports = withPWA({
|
||||
...config,
|
||||
pwa: {
|
||||
dest: "public",
|
||||
disable: process.env.NODE_ENV !== "production",
|
||||
},
|
||||
});
|
||||
|
@ -0,0 +1,16 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "NotificationSubscription" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL,
|
||||
"endpoint" TEXT NOT NULL,
|
||||
"expirationTime" INTEGER,
|
||||
"keys_p256dh" TEXT NOT NULL,
|
||||
"keys_auth" TEXT NOT NULL,
|
||||
"customerId" TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "NotificationSubscription" ADD FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[endpoint]` on the table `NotificationSubscription` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "NotificationSubscription.endpoint_unique" ON "NotificationSubscription"("endpoint");
|
@ -77,10 +77,11 @@ model Customer {
|
||||
paddleCustomerId String?
|
||||
paddleSubscriptionId String?
|
||||
|
||||
user User @relation(fields: [id], references: [id])
|
||||
messages Message[]
|
||||
phoneCalls PhoneCall[]
|
||||
phoneNumbers PhoneNumber[]
|
||||
user User @relation(fields: [id], references: [id])
|
||||
messages Message[]
|
||||
phoneCalls PhoneCall[]
|
||||
phoneNumbers PhoneNumber[]
|
||||
notificationSubscriptions NotificationSubscription[]
|
||||
}
|
||||
|
||||
model Message {
|
||||
@ -152,3 +153,16 @@ model PhoneNumber {
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
customerId String
|
||||
}
|
||||
|
||||
model NotificationSubscription {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now()) @db.Timestamptz
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz
|
||||
endpoint String @unique
|
||||
expirationTime Int?
|
||||
keys_p256dh String
|
||||
keys_auth String
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
customerId String
|
||||
}
|
||||
|
1545
package-lock.json
generated
1545
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -56,6 +56,7 @@
|
||||
"concurrently": "6.2.0",
|
||||
"got": "11.8.2",
|
||||
"jotai": "1.2.2",
|
||||
"next-pwa": "5.2.24",
|
||||
"pino": "6.13.0",
|
||||
"pino-pretty": "5.1.2",
|
||||
"postcss": "8.3.6",
|
||||
@ -68,12 +69,14 @@
|
||||
"react-use-gesture": "9.1.3",
|
||||
"tailwindcss": "2.2.7",
|
||||
"twilio": "3.66.1",
|
||||
"web-push": "3.4.5",
|
||||
"zod": "3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pino": "6.3.11",
|
||||
"@types/preview-email": "2.0.1",
|
||||
"@types/react": "17.0.15",
|
||||
"@types/web-push": "3.3.2",
|
||||
"eslint": "7.32.0",
|
||||
"husky": "6.0.0",
|
||||
"lint-staged": "10.5.4",
|
||||
|
40
worker/index.js
Normal file
40
worker/index.js
Normal file
@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
|
||||
self.addEventListener("push", function (event) {
|
||||
console.log("event.data.text()", event.data.text());
|
||||
const data = JSON.parse(event.data.text());
|
||||
event.waitUntil(
|
||||
registration.showNotification(data.title, {
|
||||
body: data.message,
|
||||
icon: "/icons/android-chrome-192x192.png",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("notificationclick", function (event) {
|
||||
event.notification.close();
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: "window", includeUncontrolled: true }).then(function (clientList) {
|
||||
if (clientList.length > 0) {
|
||||
let client = clientList[0];
|
||||
for (let i = 0; i < clientList.length; i++) {
|
||||
if (clientList[i].focused) {
|
||||
client = clientList[i];
|
||||
}
|
||||
}
|
||||
return client.focus();
|
||||
}
|
||||
return clients.openWindow("/");
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// self.addEventListener('pushsubscriptionchange', function(event) {
|
||||
// event.waitUntil(
|
||||
// Promise.all([
|
||||
// Promise.resolve(event.oldSubscription ? deleteSubscription(event.oldSubscription) : true),
|
||||
// Promise.resolve(event.newSubscription ? event.newSubscription : subscribePush(registration))
|
||||
// .then(function(sub) { return saveSubscription(sub) })
|
||||
// ])
|
||||
// )
|
||||
// })
|
Loading…
Reference in New Issue
Block a user