diff --git a/app/features/core/actions/notifications-subscription.ts b/app/features/core/actions/notifications-subscription.ts
index 1dce1b0..4cd8431 100644
--- a/app/features/core/actions/notifications-subscription.ts
+++ b/app/features/core/actions/notifications-subscription.ts
@@ -61,9 +61,10 @@ async function subscribe(request: Request) {
});
} catch (error: any) {
if (error.code !== "P2002") {
- logger.error(error);
throw error;
}
+
+ logger.warn(`Duplicate insertion of subscription with endpoint=${subscription.endpoint}`);
}
return null;
@@ -80,7 +81,15 @@ async function unsubscribe(request: Request) {
}
const endpoint = validation.data.subscriptionEndpoint;
- await db.notificationSubscription.delete({ where: { endpoint } });
+ try {
+ await db.notificationSubscription.delete({ where: { endpoint } });
+ } catch (error: any) {
+ if (error.code !== "P2025") {
+ throw error;
+ }
+
+ logger.warn(`Could not delete subscription with endpoint=${endpoint} because it has already been deleted`);
+ }
return null;
}
diff --git a/app/features/messages/actions/messages.$recipient.tsx b/app/features/messages/actions/messages.$recipient.tsx
index 13b9333..2bfb0fe 100644
--- a/app/features/messages/actions/messages.$recipient.tsx
+++ b/app/features/messages/actions/messages.$recipient.tsx
@@ -16,11 +16,6 @@ const action: ActionFunction = async ({ params, request }) => {
const formData = Object.fromEntries(await request.formData());
const twilioClient = getTwilioClient(twilioAccount);
try {
- console.log({
- body: formData.content.toString(),
- to: recipient,
- from: phoneNumber!.number,
- });
const message = await twilioClient.messages.create({
body: formData.content.toString(),
to: recipient,
diff --git a/app/features/settings/components/settings/notifications-settings.tsx b/app/features/settings/components/settings/notifications-settings.tsx
new file mode 100644
index 0000000..c6925a3
--- /dev/null
+++ b/app/features/settings/components/settings/notifications-settings.tsx
@@ -0,0 +1,84 @@
+import Toggle from "~/features/settings/components/settings/toggle";
+import useNotifications from "~/features/core/hooks/use-notifications.client";
+import { useEffect, useState } from "react";
+
+export default function NotificationsSettings() {
+ const { subscription, subscribe, unsubscribe } = useNotifications();
+ const [notificationsEnabled, setNotificationsEnabled] = useState(!!subscription);
+ const [errorMessage, setErrorMessage] = useState("");
+ const [isChanging, setIsChanging] = useState(false);
+ const onChange = async (checked: boolean) => {
+ if (isChanging) {
+ return;
+ }
+
+ setIsChanging(true);
+ setNotificationsEnabled(checked);
+ setErrorMessage("");
+ try {
+ if (checked) {
+ await subscribe();
+ } else {
+ await unsubscribe();
+ }
+ } catch (error: any) {
+ console.error(error);
+ setNotificationsEnabled(!checked);
+ if (!checked) {
+ unsubscribe().catch((error) => console.error(error));
+ }
+
+ switch (error.name) {
+ case "NotAllowedError":
+ setErrorMessage(
+ "Your browser is not allowing Shellphone to register push notifications for you. Please allow Shellphone's notifications in your browser's settings if you wish to receive them.",
+ );
+ break;
+ case "TypeError":
+ setErrorMessage("Your browser does not support push notifications yet.");
+ break;
+ }
+ } finally {
+ setIsChanging(false);
+ }
+ };
+ useEffect(() => {
+ setNotificationsEnabled(!!subscription);
+ }, [subscription]);
+
+ return (
+
+ );
+}
+
+export function FallbackNotificationsSettings() {
+ return (
+
+ void 0}
+ title="Enable notifications"
+ error={null}
+ isLoading
+ />
+
+ );
+}
diff --git a/app/features/settings/components/settings/toggle.tsx b/app/features/settings/components/settings/toggle.tsx
new file mode 100644
index 0000000..d691c4b
--- /dev/null
+++ b/app/features/settings/components/settings/toggle.tsx
@@ -0,0 +1,77 @@
+import type { ElementType } from "react";
+import { Switch } from "@headlessui/react";
+import clsx from "clsx";
+import Alert from "~/features/core/components/alert";
+
+type Props = {
+ as?: ElementType;
+ checked: boolean;
+ description?: string;
+ onChange(checked: boolean): void;
+ title: string;
+ error: null | {
+ title: string;
+ message: string;
+ };
+ isLoading?: true;
+};
+
+export default function Toggle({ as, checked, description, onChange, title, error, isLoading }: Props) {
+ return (
+
+
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {error !== null && }
+
+ );
+}
+
+function Loader() {
+ return (
+
+ );
+}
diff --git a/app/routes/__app/settings/notifications.tsx b/app/routes/__app/settings/notifications.tsx
index 4063b99..cfbb6f2 100644
--- a/app/routes/__app/settings/notifications.tsx
+++ b/app/routes/__app/settings/notifications.tsx
@@ -1,17 +1,31 @@
-import { type ElementType, useEffect, useState } from "react";
import type { ActionFunction } from "@remix-run/node";
import { ClientOnly } from "remix-utils";
-import { Switch } from "@headlessui/react";
-import clsx from "clsx";
-
-import useNotifications from "~/features/core/hooks/use-notifications.client";
-import Alert from "~/features/core/components/alert";
import { Form } from "@remix-run/react";
+
import { notify } from "~/utils/web-push.server";
import Button from "~/features/settings/components/button";
+import NotificationsSettings, {
+ FallbackNotificationsSettings,
+} from "~/features/settings/components/settings/notifications-settings";
+import db from "~/utils/db.server";
export const action: ActionFunction = async () => {
- await notify("PN4f11f0c4155dfb5d5ac8bbab2cc23cbc", {
+ const phoneNumber = await db.phoneNumber.findUnique({
+ where: { id: "PN4f11f0c4155dfb5d5ac8bbab2cc23cbc" },
+ select: {
+ organization: {
+ select: {
+ memberships: {
+ select: { notificationSubscription: true },
+ },
+ },
+ },
+ },
+ });
+ const subscriptions = phoneNumber!.organization.memberships.flatMap(
+ (membership) => membership.notificationSubscription,
+ );
+ await notify(subscriptions, {
title: "+33 6 13 37 07 87",
body: "wesh le zin, wesh la zine, copain copine mais si y'a moyen on pine",
actions: [
@@ -25,134 +39,21 @@ export const action: ActionFunction = async () => {
return null;
};
-export default function NotificationsPage() {
- return }>{() => };
-}
-
-function Notifications() {
- const { subscription, subscribe, unsubscribe } = useNotifications();
- const [notificationsEnabled, setNotificationsEnabled] = useState(!!subscription);
- const [errorMessage, setErrorMessage] = useState("");
- const [isChanging, setIsChanging] = useState(false);
- const onChange = async (checked: boolean) => {
- if (isChanging) {
- return;
- }
-
- setIsChanging(true);
- setNotificationsEnabled(checked);
- setErrorMessage("");
- try {
- if (checked) {
- await subscribe();
- } else {
- await unsubscribe();
- }
- } catch (error: any) {
- console.error(error);
- setNotificationsEnabled(!checked);
-
- switch (error.name) {
- case "NotAllowedError":
- setErrorMessage(
- "Your browser is not allowing Shellphone to register push notifications for you. Please allow Shellphone's notifications in your browser's settings if you wish to receive them.",
- );
- break;
- case "TypeError":
- setErrorMessage("Your browser does not support push notifications yet.");
- break;
- }
- } finally {
- setIsChanging(false);
- }
- };
- useEffect(() => {
- setNotificationsEnabled(!!subscription);
- }, [subscription]);
-
+export default function Notifications() {
return (
+
+
Notifications
+ }>{() => }
+
+
-
-
-
Notifications
-
-
- {errorMessage !== "" &&
}
-
);
}
-
-type ToggleProps = {
- as?: ElementType;
- checked: boolean;
- description?: string;
- onChange(checked: boolean): void;
- title: string;
-};
-
-function Toggle({ as, checked, description, onChange, title }: ToggleProps) {
- return (
-
-
-
- {title}
-
- {description && (
-
- {description}
-
- )}
-
-
-
-
-
- );
-}
-
-function Loader() {
- return (
-
- );
-}
diff --git a/app/routes/cache[.]manifest.ts b/app/routes/cache[.]manifest.ts
deleted file mode 100644
index 15fcb80..0000000
--- a/app/routes/cache[.]manifest.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { LoaderFunction } from "@remix-run/node";
-
-const manifest = `CACHE MANIFEST
-
-# Version 1.0000
-
-NETWORK:
-*
-`;
-
-export const loader: LoaderFunction = async () => {
- const headers = new Headers({
- "Content-Type": "text/cache-manifest",
- "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
- Pragma: "no-cache",
- Expires: "Thu, 01 Jan 1970 00:00:01 GMT",
- });
-
- return new Response(manifest, { headers });
-};
diff --git a/app/utils/web-push.server.ts b/app/utils/web-push.server.ts
index c0012bb..98cffd7 100644
--- a/app/utils/web-push.server.ts
+++ b/app/utils/web-push.server.ts
@@ -1,37 +1,24 @@
import webpush, { type PushSubscription, WebPushError } from "web-push";
+import type { NotificationSubscription } from "@prisma/client";
import serverConfig from "~/config/config.server";
import db from "~/utils/db.server";
import logger from "~/utils/logger.server";
export type NotificationPayload = NotificationOptions & {
- title: string;
- body: string;
+ title: string; // max 50 characters
+ body: string; // max 150 characters
};
-export async function notify(phoneNumberId: string, payload: NotificationPayload) {
+export async function notify(subscriptions: NotificationSubscription[], payload: NotificationPayload) {
webpush.setVapidDetails("mailto:mokht@rmi.al", serverConfig.webPush.publicKey, serverConfig.webPush.privateKey);
-
- const phoneNumber = await db.phoneNumber.findUnique({
- where: { id: phoneNumberId },
- select: {
- organization: {
- select: {
- memberships: {
- select: { notificationSubscription: true },
- },
- },
- },
- },
+ const title = truncate(payload.title, 50);
+ const body = truncate(payload.body, 150);
+ const _payload = JSON.stringify({
+ ...payload,
+ title,
+ body,
});
- if (!phoneNumber) {
- // TODO
- return;
- }
-
- const subscriptions = phoneNumber.organization.memberships.flatMap(
- (membership) => membership.notificationSubscription,
- );
await Promise.all(
subscriptions.map(async (subscription) => {
@@ -44,7 +31,7 @@ export async function notify(phoneNumberId: string, payload: NotificationPayload
};
try {
- await webpush.sendNotification(webPushSubscription, JSON.stringify(payload));
+ await webpush.sendNotification(webPushSubscription, _payload);
} catch (error: any) {
logger.error(error);
if (error instanceof WebPushError) {
@@ -55,3 +42,11 @@ export async function notify(phoneNumberId: string, payload: NotificationPayload
}),
);
}
+
+function truncate(str: string, maxLength: number) {
+ if (str.length <= maxLength) {
+ return str;
+ }
+
+ return str.substring(0, maxLength - 1) + "\u2026";
+}