push notifications but installable to home screen yet
@ -21,3 +21,7 @@ PADDLE_VENDOR_ID=TODO
|
|||||||
PADDLE_API_KEY=TODO
|
PADDLE_API_KEY=TODO
|
||||||
|
|
||||||
TWILIO_AUTH_TOKEN=TODO
|
TWILIO_AUTH_TOKEN=TODO
|
||||||
|
|
||||||
|
# npx web-push generate-vapid-keys
|
||||||
|
WEB_PUSH_VAPID_PUBLIC_KEY=TODO
|
||||||
|
WEB_PUSH_VAPID_PRIVATE_KEY=TODO
|
||||||
|
1
.gitignore
vendored
@ -3,6 +3,7 @@ node_modules
|
|||||||
/.cache
|
/.cache
|
||||||
/server/build
|
/server/build
|
||||||
/public/build
|
/public/build
|
||||||
|
/public/entry.worker.js
|
||||||
/build
|
/build
|
||||||
server.js
|
server.js
|
||||||
/app/styles/tailwind.css
|
/app/styles/tailwind.css
|
||||||
|
@ -28,6 +28,14 @@ invariant(
|
|||||||
typeof process.env.MASTER_ENCRYPTION_KEY === "string",
|
typeof process.env.MASTER_ENCRYPTION_KEY === "string",
|
||||||
`Please define the "MASTER_ENCRYPTION_KEY" environment variable`,
|
`Please define the "MASTER_ENCRYPTION_KEY" environment variable`,
|
||||||
);
|
);
|
||||||
|
invariant(
|
||||||
|
typeof process.env.WEB_PUSH_VAPID_PRIVATE_KEY === "string",
|
||||||
|
`Please define the "WEB_PUSH_VAPID_PRIVATE_KEY" environment variable`,
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
typeof process.env.WEB_PUSH_VAPID_PUBLIC_KEY === "string",
|
||||||
|
`Please define the "WEB_PUSH_VAPID_PUBLIC_KEY" environment variable`,
|
||||||
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
app: {
|
app: {
|
||||||
@ -49,4 +57,8 @@ export default {
|
|||||||
twilio: {
|
twilio: {
|
||||||
authToken: process.env.TWILIO_AUTH_TOKEN,
|
authToken: process.env.TWILIO_AUTH_TOKEN,
|
||||||
},
|
},
|
||||||
|
webPush: {
|
||||||
|
privateKey: process.env.WEB_PUSH_VAPID_PRIVATE_KEY,
|
||||||
|
publicKey: process.env.WEB_PUSH_VAPID_PUBLIC_KEY,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -2,3 +2,14 @@ import { hydrate } from "react-dom";
|
|||||||
import { RemixBrowser } from "@remix-run/react";
|
import { RemixBrowser } from "@remix-run/react";
|
||||||
|
|
||||||
hydrate(<RemixBrowser />, document);
|
hydrate(<RemixBrowser />, document);
|
||||||
|
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
window.addEventListener("load", async () => {
|
||||||
|
try {
|
||||||
|
await navigator.serviceWorker.register("/entry.worker.js");
|
||||||
|
await navigator.serviceWorker.ready;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Service worker registration failed", error, (error as Error).name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
47
app/entry.worker.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/// <reference lib="WebWorker" />
|
||||||
|
|
||||||
|
import type { NotificationPayload } from "~/utils/web-push.server";
|
||||||
|
import { addBadge, removeBadge } from "~/utils/pwa.client";
|
||||||
|
|
||||||
|
declare let self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
|
const defaultOptions: NotificationOptions = {
|
||||||
|
icon: "/icons/android-chrome-192x192.png",
|
||||||
|
badge: "/icons/android-chrome-48x48.png",
|
||||||
|
dir: "auto",
|
||||||
|
image: undefined,
|
||||||
|
silent: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.addEventListener("push", (event) => {
|
||||||
|
const { title, ...payload }: NotificationPayload = JSON.parse(event?.data!.text());
|
||||||
|
const options = Object.assign({}, defaultOptions, payload);
|
||||||
|
event.waitUntil(async () => {
|
||||||
|
await self.registration.showNotification(title, options);
|
||||||
|
await addBadge(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("notificationclick", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
console.log("On notification click: ", event.notification.tag);
|
||||||
|
// Android doesn’t close the notification when you click on it
|
||||||
|
// See: http://crbug.com/463146
|
||||||
|
event.notification.close();
|
||||||
|
await removeBadge();
|
||||||
|
|
||||||
|
if (event.action === "reply") {
|
||||||
|
const recipient = encodeURIComponent(event.notification.data.recipient);
|
||||||
|
return self.clients.openWindow?.(`/messages/${recipient}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.action === "answer") {
|
||||||
|
const recipient = encodeURIComponent(event.notification.data.recipient);
|
||||||
|
return self.clients.openWindow?.(`/incoming-call/${recipient}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.clients.openWindow?.("/");
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
});
|
104
app/features/core/actions/notifications-subscription.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { type ActionFunction } from "@remix-run/node";
|
||||||
|
import { badRequest, notFound } from "remix-utils";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import db from "~/utils/db.server";
|
||||||
|
import logger from "~/utils/logger.server";
|
||||||
|
import { validate } from "~/utils/validation.server";
|
||||||
|
import { requireLoggedIn } from "~/utils/auth.server";
|
||||||
|
|
||||||
|
const action: ActionFunction = async ({ request }) => {
|
||||||
|
const formData = await request.clone().formData();
|
||||||
|
const action = formData.get("_action");
|
||||||
|
if (!action) {
|
||||||
|
const errorMessage = "POST /notifications-subscription without any _action";
|
||||||
|
logger.error(errorMessage);
|
||||||
|
return badRequest({ errorMessage });
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action as Action) {
|
||||||
|
case "subscribe":
|
||||||
|
return subscribe(request);
|
||||||
|
case "unsubscribe":
|
||||||
|
return unsubscribe(request);
|
||||||
|
default:
|
||||||
|
const errorMessage = `POST /notifications-subscription with an invalid _action=${action}`;
|
||||||
|
logger.error(errorMessage);
|
||||||
|
return badRequest({ errorMessage });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default action;
|
||||||
|
|
||||||
|
async function subscribe(request: Request) {
|
||||||
|
const { organization } = await requireLoggedIn(request);
|
||||||
|
const formData = await request.formData();
|
||||||
|
const body = {
|
||||||
|
subscription: JSON.parse(formData.get("subscription")?.toString() ?? "{}"),
|
||||||
|
};
|
||||||
|
const validation = validate(validations.subscribe, body);
|
||||||
|
if (validation.errors) {
|
||||||
|
return badRequest(validation.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { subscription } = validation.data;
|
||||||
|
const membership = await db.membership.findFirst({
|
||||||
|
where: { id: organization.membershipId },
|
||||||
|
});
|
||||||
|
if (!membership) {
|
||||||
|
return notFound("Phone number not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.notificationSubscription.create({
|
||||||
|
data: {
|
||||||
|
membershipId: membership.id,
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
expirationTime: subscription.expirationTime,
|
||||||
|
keys_p256dh: subscription.keys.p256dh,
|
||||||
|
keys_auth: subscription.keys.auth,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code !== "P2002") {
|
||||||
|
logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unsubscribe(request: Request) {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const body = {
|
||||||
|
subscriptionEndpoint: formData.get("subscriptionEndpoint"),
|
||||||
|
};
|
||||||
|
const validation = validate(validations.unsubscribe, body);
|
||||||
|
if (validation.errors) {
|
||||||
|
return badRequest(validation.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = validation.data.subscriptionEndpoint;
|
||||||
|
await db.notificationSubscription.delete({ where: { endpoint } });
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = "subscribe" | "unsubscribe";
|
||||||
|
|
||||||
|
const validations = {
|
||||||
|
subscribe: z.object({
|
||||||
|
subscription: z.object({
|
||||||
|
endpoint: z.string(),
|
||||||
|
expirationTime: z.number().nullable(),
|
||||||
|
keys: z.object({
|
||||||
|
p256dh: z.string(),
|
||||||
|
auth: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
unsubscribe: z.object({
|
||||||
|
subscriptionEndpoint: z.string(),
|
||||||
|
}),
|
||||||
|
} as const;
|
4
app/features/core/components/footer.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.footer-ios {
|
||||||
|
margin-bottom: var(--safe-area-bottom);
|
||||||
|
padding-bottom: var(--safe-area-bottom);
|
||||||
|
}
|
@ -6,7 +6,7 @@ import clsx from "clsx";
|
|||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
className="grid grid-cols-4 bg-[#F7F7F7] h-16 border-t border-gray-400 border-opacity-25 py-2 z-10"
|
className="footer-ios grid grid-cols-4 bg-[#F7F7F7] h-16 border-t border-gray-400 border-opacity-25 py-2 z-10"
|
||||||
// className="grid grid-cols-4 border-t border-gray-400 border-opacity-25 py-3 z-10 backdrop-blur"
|
// className="grid grid-cols-4 border-t border-gray-400 border-opacity-25 py-3 z-10 backdrop-blur"
|
||||||
>
|
>
|
||||||
<FooterLink label="Calls" path="/calls" icon={<IoCall className="w-6 h-6" />} />
|
<FooterLink label="Calls" path="/calls" icon={<IoCall className="w-6 h-6" />} />
|
||||||
|
13
app/features/core/hooks/use-app-loader-data.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useMatches } from "@remix-run/react";
|
||||||
|
|
||||||
|
import type { AppLoaderData } from "~/routes/__app";
|
||||||
|
|
||||||
|
export default function useAppLoaderData() {
|
||||||
|
const matches = useMatches();
|
||||||
|
const __appRoute = matches.find((match) => match.id === "routes/__app");
|
||||||
|
if (!__appRoute) {
|
||||||
|
throw new Error("useSession hook called outside _app route");
|
||||||
|
}
|
||||||
|
|
||||||
|
return __appRoute.data as AppLoaderData;
|
||||||
|
}
|
90
app/features/core/hooks/use-notifications.client.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useFetcher } from "@remix-run/react";
|
||||||
|
|
||||||
|
import useAppLoaderData from "~/features/core/hooks/use-app-loader-data";
|
||||||
|
|
||||||
|
export default function useNotifications() {
|
||||||
|
const isServiceWorkerSupported = useMemo(() => "serviceWorker" in navigator, []);
|
||||||
|
const [subscription, setSubscription] = useState<PushSubscription | null>(null);
|
||||||
|
const { webPushPublicKey } = useAppLoaderData().config;
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
const subscribeToNotifications = (subscription: PushSubscriptionJSON) => {
|
||||||
|
fetcher.submit(
|
||||||
|
{
|
||||||
|
_action: "subscribe",
|
||||||
|
subscription: JSON.stringify(subscription),
|
||||||
|
},
|
||||||
|
{ method: "post", action: "/notifications-subscription" },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const unsubscribeFromNotifications = (subscriptionEndpoint: PushSubscription["endpoint"]) => {
|
||||||
|
fetcher.submit(
|
||||||
|
{
|
||||||
|
_action: "unsubscribe",
|
||||||
|
subscriptionEndpoint,
|
||||||
|
},
|
||||||
|
{ method: "post", action: "/notifications-subscription" },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 || subscription !== null || fetcher.state !== "idle") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const newSubscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(webPushPublicKey),
|
||||||
|
});
|
||||||
|
setSubscription(newSubscription);
|
||||||
|
subscribeToNotifications(newSubscription.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unsubscribe() {
|
||||||
|
if (!isServiceWorkerSupported || !subscription || fetcher.state !== "idle") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription
|
||||||
|
.unsubscribe()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Unsubscribed from notifications");
|
||||||
|
setSubscription(null);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Failed to unsubscribe from notifications", error));
|
||||||
|
unsubscribeFromNotifications(subscription.endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -1,13 +1,5 @@
|
|||||||
import { useMatches } from "@remix-run/react";
|
import useAppLoaderData from "~/features/core/hooks/use-app-loader-data";
|
||||||
|
|
||||||
import type { SessionData } from "~/utils/auth.server";
|
|
||||||
|
|
||||||
export default function useSession() {
|
export default function useSession() {
|
||||||
const matches = useMatches();
|
return useAppLoaderData().sessionData;
|
||||||
const __appRoute = matches.find((match) => match.id === "routes/__app");
|
|
||||||
if (!__appRoute) {
|
|
||||||
throw new Error("useSession hook called outside _app route");
|
|
||||||
}
|
|
||||||
|
|
||||||
return __appRoute.data as SessionData;
|
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ const action: ActionFunction = async ({ params, request }) => {
|
|||||||
phoneNumberId: phoneNumber!.id,
|
phoneNumberId: phoneNumber!.id,
|
||||||
id: message.sid,
|
id: message.sid,
|
||||||
to: message.to,
|
to: message.to,
|
||||||
|
recipient: message.to,
|
||||||
from: message.from,
|
from: message.from,
|
||||||
status: translateMessageStatus(message.status),
|
status: translateMessageStatus(message.status),
|
||||||
direction: translateMessageDirection(message.direction),
|
direction: translateMessageDirection(message.direction),
|
||||||
|
54
app/root.tsx
@ -1,4 +1,4 @@
|
|||||||
import type { FunctionComponent, ReactNode } from "react";
|
import { type FunctionComponent, type PropsWithChildren } from "react";
|
||||||
import type { LinksFunction } from "@remix-run/node";
|
import type { LinksFunction } from "@remix-run/node";
|
||||||
import { Link, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useCatch } from "@remix-run/react";
|
import { Link, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useCatch } from "@remix-run/react";
|
||||||
|
|
||||||
@ -6,35 +6,7 @@ import Logo from "~/features/core/components/logo";
|
|||||||
|
|
||||||
import styles from "./styles/tailwind.css";
|
import styles from "./styles/tailwind.css";
|
||||||
|
|
||||||
export const links: LinksFunction = () => [
|
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];
|
||||||
{ rel: "stylesheet", href: styles },
|
|
||||||
{
|
|
||||||
rel: "icon",
|
|
||||||
href: "/favicon.ico",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rel: "apple-touch-icon",
|
|
||||||
sizes: "180x180",
|
|
||||||
href: "/apple-touch-icon.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rel: "icon",
|
|
||||||
type: "image/png",
|
|
||||||
sizes: "32x32",
|
|
||||||
href: "/favicon-32x32.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rel: "icon",
|
|
||||||
type: "image/png",
|
|
||||||
sizes: "16x16",
|
|
||||||
href: "/favicon-16x16.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rel: "mask-icon",
|
|
||||||
href: "/safari-pinned-tab.svg",
|
|
||||||
color: "#663399",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@ -92,11 +64,29 @@ export function CatchBoundary() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Document: FunctionComponent<{ children: ReactNode }> = ({ children }) => (
|
const Document: FunctionComponent<PropsWithChildren<{}>> = ({ children }) => (
|
||||||
<html lang="en" className="h-full">
|
<html lang="en" className="h-full">
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||||
|
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Shellphone" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="application-name" content="Shellphone" />
|
||||||
|
<meta name="theme-color" content="#F4F4F5" />
|
||||||
|
|
||||||
|
<meta name="msapplication-navbutton-color" content="#F4F4F5" />
|
||||||
|
<meta name="msapplication-starturl" content="/messages" />
|
||||||
|
<meta name="msapplication-TileColor" content="#F4F4F5" />
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
|
||||||
|
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#F4F4F5" />
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
|
@ -1,15 +1,31 @@
|
|||||||
import { type LoaderFunction, json } from "@remix-run/node";
|
import { type LinksFunction, type LoaderFunction, json } from "@remix-run/node";
|
||||||
import { Outlet, useCatch, useMatches } from "@remix-run/react";
|
import { Outlet, useCatch, useMatches } from "@remix-run/react";
|
||||||
|
|
||||||
|
import serverConfig from "~/config/config.server";
|
||||||
import { type SessionData, requireLoggedIn } from "~/utils/auth.server";
|
import { type SessionData, requireLoggedIn } from "~/utils/auth.server";
|
||||||
import Footer from "~/features/core/components/footer";
|
import Footer from "~/features/core/components/footer";
|
||||||
|
import footerStyles from "~/features/core/components/footer.css";
|
||||||
|
import appStyles from "~/styles/app.css";
|
||||||
|
|
||||||
export type AppLoaderData = SessionData;
|
export const links: LinksFunction = () => [
|
||||||
|
{ rel: "stylesheet", href: appStyles },
|
||||||
|
{ rel: "stylesheet", href: footerStyles },
|
||||||
|
];
|
||||||
|
|
||||||
|
export type AppLoaderData = {
|
||||||
|
sessionData: SessionData;
|
||||||
|
config: { webPushPublicKey: string };
|
||||||
|
};
|
||||||
|
|
||||||
export const loader: LoaderFunction = async ({ request }) => {
|
export const loader: LoaderFunction = async ({ request }) => {
|
||||||
const sessionData = await requireLoggedIn(request);
|
const sessionData = await requireLoggedIn(request);
|
||||||
|
|
||||||
return json<AppLoaderData>(sessionData);
|
return json<AppLoaderData>({
|
||||||
|
sessionData,
|
||||||
|
config: {
|
||||||
|
webPushPublicKey: serverConfig.webPush.publicKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function __App() {
|
export default function __App() {
|
||||||
@ -24,7 +40,7 @@ export default function __App() {
|
|||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{!hideFooter ? <Footer /> : null}
|
{hideFooter ? null : <Footer />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
3
app/routes/__app/notifications-subscription.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import notificationsSubscriptionAction from "~/features/core/actions/notifications-subscription";
|
||||||
|
|
||||||
|
export const action = notificationsSubscriptionAction;
|
@ -8,6 +8,7 @@ import {
|
|||||||
IoCardOutline,
|
IoCardOutline,
|
||||||
IoCallOutline,
|
IoCallOutline,
|
||||||
IoPersonCircleOutline,
|
IoPersonCircleOutline,
|
||||||
|
IoHelpBuoyOutline,
|
||||||
} from "react-icons/io5";
|
} from "react-icons/io5";
|
||||||
|
|
||||||
import Divider from "~/features/settings/components/divider";
|
import Divider from "~/features/settings/components/divider";
|
||||||
@ -18,6 +19,7 @@ const subNavigation = [
|
|||||||
{ name: "Phone", to: "/settings/phone", icon: IoCallOutline },
|
{ name: "Phone", to: "/settings/phone", icon: IoCallOutline },
|
||||||
{ name: "Billing", to: "/settings/billing", icon: IoCardOutline },
|
{ name: "Billing", to: "/settings/billing", icon: IoCardOutline },
|
||||||
{ name: "Notifications", to: "/settings/notifications", icon: IoNotificationsOutline },
|
{ name: "Notifications", to: "/settings/notifications", icon: IoNotificationsOutline },
|
||||||
|
{ name: "Support", to: "/settings/support", icon: IoHelpBuoyOutline },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const meta: MetaFunction = () => ({
|
export const meta: MetaFunction = () => ({
|
||||||
@ -90,4 +92,4 @@ export default function SettingsLayout() {
|
|||||||
</main>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
@ -15,7 +15,7 @@ function useSubscription() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function Billing() {
|
export default function Billing() {
|
||||||
const { count: paymentsCount } = usePaymentsHistory();
|
const { count: paymentsCount } = usePaymentsHistory();
|
||||||
const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription();
|
const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription();
|
||||||
|
|
||||||
@ -64,5 +64,3 @@ const plansName: Record<number, string> = {
|
|||||||
727544: "Yearly",
|
727544: "Yearly",
|
||||||
727540: "Monthly",
|
727540: "Monthly",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Billing;
|
|
||||||
|
@ -1,7 +1,158 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export const action: ActionFunction = async () => {
|
||||||
|
await notify("PN4f11f0c4155dfb5d5ac8bbab2cc23cbc", {
|
||||||
|
title: "+33 6 13 37 07 87",
|
||||||
|
body: "wesh le zin, wesh la zine, copain copine mais si y'a moyen on pine",
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: "reply",
|
||||||
|
title: "Reply",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: { recipient: "+33613370787" },
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NotificationsPage() {
|
||||||
|
return <ClientOnly fallback={<Loader />}>{() => <Notifications />}</ClientOnly>;
|
||||||
|
}
|
||||||
|
|
||||||
function Notifications() {
|
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]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>Coming soon</div>
|
<section className="pt-6 divide-y divide-gray-200">
|
||||||
|
<section>
|
||||||
|
<Form method="post">
|
||||||
|
<Button variant="default" type="submit">
|
||||||
|
send it!!!
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</section>
|
||||||
|
<div className="px-4 sm:px-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg leading-6 font-medium text-gray-900">Notifications</h2>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-2 divide-y divide-gray-200">
|
||||||
|
<Toggle
|
||||||
|
as="li"
|
||||||
|
checked={notificationsEnabled}
|
||||||
|
description="Get notified on this device when you receive a message or a phone call"
|
||||||
|
onChange={onChange}
|
||||||
|
title="Enable notifications"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
{errorMessage !== "" && <Alert title="Browser error" message={errorMessage} variant="error" />}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Notifications;
|
type ToggleProps = {
|
||||||
|
as?: ElementType;
|
||||||
|
checked: boolean;
|
||||||
|
description?: string;
|
||||||
|
onChange(checked: boolean): void;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Toggle({ as, checked, description, onChange, title }: ToggleProps) {
|
||||||
|
return (
|
||||||
|
<Switch.Group as={as} className="py-4 flex items-center justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Switch.Label as="p" className="text-sm font-medium text-gray-900" passive>
|
||||||
|
{title}
|
||||||
|
</Switch.Label>
|
||||||
|
{description && (
|
||||||
|
<Switch.Description as="span" className="text-sm text-gray-500">
|
||||||
|
{description}
|
||||||
|
</Switch.Description>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
className={clsx(
|
||||||
|
checked ? "bg-primary-500" : "bg-gray-200",
|
||||||
|
"ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={clsx(
|
||||||
|
checked ? "translate-x-5" : "translate-x-0",
|
||||||
|
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</Switch.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Loader() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className="animate-spin mx-auto h-5 w-5 text-primary-700"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@ export const loader = settingsPhoneLoader;
|
|||||||
|
|
||||||
export const action = settingsPhoneAction;
|
export const action = settingsPhoneAction;
|
||||||
|
|
||||||
function PhoneSettings() {
|
export default function PhoneSettings() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
<TwilioConnect />
|
<TwilioConnect />
|
||||||
@ -15,5 +15,3 @@ function PhoneSettings() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PhoneSettings;
|
|
||||||
|
9
app/routes/__app/settings/support.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function SupportPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<a className="underline" href="mailto:support@shellphone.app">
|
||||||
|
Email us
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
20
app/routes/cache[.]manifest.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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 });
|
||||||
|
};
|
10
app/styles/app.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
:root {
|
||||||
|
--safe-area-top: env(safe-area-inset-top);
|
||||||
|
--safe-area-right: env(safe-area-inset-right);
|
||||||
|
--safe-area-bottom: env(safe-area-inset-bottom); /* THIS ONE GETS US THE HOME BAR HEIGHT */
|
||||||
|
--safe-area-left: env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||||
|
}
|
@ -13,7 +13,7 @@ type SessionTwilioAccount = Pick<
|
|||||||
TwilioAccount,
|
TwilioAccount,
|
||||||
"accountSid" | "subAccountSid" | "subAccountAuthToken" | "apiKeySid" | "apiKeySecret" | "twimlAppSid"
|
"accountSid" | "subAccountSid" | "subAccountAuthToken" | "apiKeySid" | "apiKeySecret" | "twimlAppSid"
|
||||||
>;
|
>;
|
||||||
type SessionOrganization = Pick<Organization, "id"> & { role: MembershipRole };
|
type SessionOrganization = Pick<Organization, "id"> & { role: MembershipRole; membershipId: string };
|
||||||
type SessionPhoneNumber = Pick<PhoneNumber, "id" | "number">;
|
type SessionPhoneNumber = Pick<PhoneNumber, "id" | "number">;
|
||||||
export type SessionUser = Pick<User, "id" | "role" | "email" | "fullName">;
|
export type SessionUser = Pick<User, "id" | "role" | "email" | "fullName">;
|
||||||
export type SessionData = {
|
export type SessionData = {
|
||||||
@ -190,6 +190,7 @@ async function buildSessionData(id: string): Promise<SessionData> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
role: true,
|
role: true,
|
||||||
|
id: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -203,6 +204,7 @@ async function buildSessionData(id: string): Promise<SessionData> {
|
|||||||
const organizations = memberships.map((membership) => ({
|
const organizations = memberships.map((membership) => ({
|
||||||
...membership.organization,
|
...membership.organization,
|
||||||
role: membership.role,
|
role: membership.role,
|
||||||
|
membershipId: membership.id,
|
||||||
}));
|
}));
|
||||||
const { twilioAccount, ...organization } = organizations[0];
|
const { twilioAccount, ...organization } = organizations[0];
|
||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({
|
||||||
|
70
app/utils/pwa.client.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
type ResponseObject = {
|
||||||
|
status: "success" | "bad";
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// use case: prevent making phone calls / queue messages when offline
|
||||||
|
export async function checkConnectivity(online: () => void, offline: () => void): Promise<ResponseObject> {
|
||||||
|
try {
|
||||||
|
if (navigator.onLine) {
|
||||||
|
online();
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
message: "Connected to the internet",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
offline();
|
||||||
|
return {
|
||||||
|
status: "bad",
|
||||||
|
message: "No internet connection available",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.debug(err);
|
||||||
|
throw new Error("Unable to check network connectivity!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use case: display unread messages + missed phone calls count
|
||||||
|
export async function addBadge(numberCount: number): Promise<ResponseObject> {
|
||||||
|
try {
|
||||||
|
//@ts-ignore
|
||||||
|
if (navigator.setAppBadge) {
|
||||||
|
//@ts-ignore
|
||||||
|
await navigator.setAppBadge(numberCount);
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
message: "Badge successfully added",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: "bad",
|
||||||
|
message: "Badging API not supported",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.debug(err);
|
||||||
|
throw new Error("Error adding badge!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function removeBadge(): Promise<ResponseObject> {
|
||||||
|
try {
|
||||||
|
//@ts-ignore
|
||||||
|
if (navigator.clearAppBadge) {
|
||||||
|
//@ts-ignore
|
||||||
|
await navigator.clearAppBadge();
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
message: "Cleared badges",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: "bad",
|
||||||
|
message: "Badging API not supported in this browser!",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.debug(error);
|
||||||
|
throw new Error("Error removing badge!");
|
||||||
|
}
|
||||||
|
}
|
@ -4,5 +4,5 @@ export const { getSeo, getSeoMeta, getSeoLinks } = initSeo({
|
|||||||
title: "",
|
title: "",
|
||||||
titleTemplate: "%s | Shellphone",
|
titleTemplate: "%s | Shellphone",
|
||||||
description: "",
|
description: "",
|
||||||
defaultTitle: "Shellphone",
|
defaultTitle: "Shellphone: Your Personal Cloud Phone",
|
||||||
});
|
});
|
||||||
|
@ -15,7 +15,7 @@ export default function getTwilioClient({
|
|||||||
throw new Error("unreachable");
|
throw new Error("unreachable");
|
||||||
}
|
}
|
||||||
|
|
||||||
return twilio(subAccountSid, subAccountAuthToken ?? serverConfig.twilio.authToken, {
|
return twilio(subAccountSid, serverConfig.twilio.authToken, {
|
||||||
accountSid,
|
accountSid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
57
app/utils/web-push.server.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import webpush, { type PushSubscription, WebPushError } from "web-push";
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function notify(phoneNumberId: string, 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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!phoneNumber) {
|
||||||
|
// TODO
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptions = phoneNumber.organization.memberships.flatMap(
|
||||||
|
(membership) => membership.notificationSubscription,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(payload));
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(error);
|
||||||
|
if (error instanceof WebPushError) {
|
||||||
|
// subscription most likely expired or has been revoked
|
||||||
|
await db.notificationSubscription.delete({ where: { id: subscription.id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
387
package-lock.json
generated
@ -47,6 +47,7 @@
|
|||||||
"tiny-invariant": "1.2.0",
|
"tiny-invariant": "1.2.0",
|
||||||
"tslog": "3.3.3",
|
"tslog": "3.3.3",
|
||||||
"twilio": "3.77.0",
|
"twilio": "3.77.0",
|
||||||
|
"web-push": "3.5.0",
|
||||||
"zod": "3.16.0"
|
"zod": "3.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -66,6 +67,7 @@
|
|||||||
"@types/react": "18.0.9",
|
"@types/react": "18.0.9",
|
||||||
"@types/react-dom": "18.0.4",
|
"@types/react-dom": "18.0.4",
|
||||||
"@types/secure-password": "3.1.1",
|
"@types/secure-password": "3.1.1",
|
||||||
|
"@types/web-push": "3.3.2",
|
||||||
"@vitejs/plugin-react": "1.3.2",
|
"@vitejs/plugin-react": "1.3.2",
|
||||||
"c8": "7.11.2",
|
"c8": "7.11.2",
|
||||||
"cypress": "9.6.1",
|
"cypress": "9.6.1",
|
||||||
@ -4459,6 +4461,15 @@
|
|||||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
|
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/web-push": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/yauzl": {
|
"node_modules/@types/yauzl": {
|
||||||
"version": "2.9.2",
|
"version": "2.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
|
||||||
@ -5108,6 +5119,17 @@
|
|||||||
"safer-buffer": "~2.1.0"
|
"safer-buffer": "~2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1.js": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^4.0.0",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"minimalistic-assert": "^1.0.0",
|
||||||
|
"safer-buffer": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assert-never": {
|
"node_modules/assert-never": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
||||||
@ -5533,6 +5555,16 @@
|
|||||||
"tweetnacl": "^0.14.3"
|
"tweetnacl": "^0.14.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/big-integer": {
|
||||||
|
"version": "1.6.51",
|
||||||
|
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
|
||||||
|
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/big.js": {
|
"node_modules/big.js": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||||
@ -5618,6 +5650,11 @@
|
|||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/bn.js": {
|
||||||
|
"version": "4.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||||
|
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.0",
|
"version": "1.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
|
||||||
@ -5729,6 +5766,23 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/broadcast-channel": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
|
||||||
|
"integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.7.2",
|
||||||
|
"detect-node": "^2.1.0",
|
||||||
|
"js-sha3": "0.8.0",
|
||||||
|
"microseconds": "0.2.0",
|
||||||
|
"nano-time": "1.0.0",
|
||||||
|
"oblivious-set": "1.0.0",
|
||||||
|
"rimraf": "3.0.2",
|
||||||
|
"unload": "2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/browser-sync": {
|
"node_modules/browser-sync": {
|
||||||
"version": "2.27.10",
|
"version": "2.27.10",
|
||||||
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
|
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
|
||||||
@ -7764,6 +7818,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-node": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/detective": {
|
"node_modules/detective": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
|
||||||
@ -11217,6 +11278,17 @@
|
|||||||
"entities": "^2.0.0"
|
"entities": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http_ece": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
|
||||||
|
"dependencies": {
|
||||||
|
"urlsafe-base64": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-basic": {
|
"node_modules/http-basic": {
|
||||||
"version": "8.1.3",
|
"version": "8.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
|
||||||
@ -12411,6 +12483,13 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-sha3": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/js-stringify": {
|
"node_modules/js-stringify": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
|
||||||
@ -13669,6 +13748,17 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/match-sorter": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"remove-accents": "0.4.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/matcher": {
|
"node_modules/matcher": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz",
|
||||||
@ -14621,6 +14711,13 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/microseconds": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/mime": {
|
"node_modules/mime": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
|
||||||
@ -14680,6 +14777,11 @@
|
|||||||
"mini-svg-data-uri": "cli.js"
|
"mini-svg-data-uri": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimalistic-assert": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@ -14989,6 +15091,16 @@
|
|||||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/nano-time": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"big-integer": "^1.6.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoassert": {
|
"node_modules/nanoassert": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz",
|
||||||
@ -15664,6 +15776,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oblivious-set": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
@ -17655,6 +17774,33 @@
|
|||||||
"react": ">=16"
|
"react": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-query": {
|
||||||
|
"version": "3.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.0.tgz",
|
||||||
|
"integrity": "sha512-Od0IkSuS79WJOhzWBx/ys0x13+7wFqgnn64vBqqAAnZ9whocVhl/y1padD5uuZ6EIkXbFbInax0qvY7zGM0thA==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"broadcast-channel": "^3.4.1",
|
||||||
|
"match-sorter": "^6.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz",
|
||||||
@ -18289,6 +18435,13 @@
|
|||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/remove-accents": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
|
||||||
|
"integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/repeat-element": {
|
"node_modules/repeat-element": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
|
||||||
@ -21169,6 +21322,17 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unload": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.6.2",
|
||||||
|
"detect-node": "^2.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
@ -21273,6 +21437,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||||
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
||||||
},
|
},
|
||||||
|
"node_modules/urlsafe-base64": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-I/iQaabGL0bPOh07ABac77kL4MY="
|
||||||
|
},
|
||||||
"node_modules/use": {
|
"node_modules/use": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||||
@ -21586,6 +21755,44 @@
|
|||||||
"@zxing/text-encoding": "0.9.0"
|
"@zxing/text-encoding": "0.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-push": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-JC0V9hzKTqlDYJ+LTZUXtW7B175qwwaqzbbMSWDxHWxZvd3xY0C2rcotMGDavub2nAAFw+sXTsqR65/KY2A5AQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"asn1.js": "^5.3.0",
|
||||||
|
"http_ece": "1.1.0",
|
||||||
|
"https-proxy-agent": "^5.0.0",
|
||||||
|
"jws": "^4.0.0",
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"urlsafe-base64": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"web-push": "src/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/web-push/node_modules/jwa": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/web-push/node_modules/jws": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.0",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/web-resource-inliner": {
|
"node_modules/web-resource-inliner": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz",
|
||||||
@ -25252,6 +25459,15 @@
|
|||||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
|
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/web-push": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/yauzl": {
|
"@types/yauzl": {
|
||||||
"version": "2.9.2",
|
"version": "2.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
|
||||||
@ -25695,6 +25911,17 @@
|
|||||||
"safer-buffer": "~2.1.0"
|
"safer-buffer": "~2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"asn1.js": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||||
|
"requires": {
|
||||||
|
"bn.js": "^4.0.0",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"minimalistic-assert": "^1.0.0",
|
||||||
|
"safer-buffer": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"assert-never": {
|
"assert-never": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
||||||
@ -26011,6 +26238,13 @@
|
|||||||
"tweetnacl": "^0.14.3"
|
"tweetnacl": "^0.14.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"big-integer": {
|
||||||
|
"version": "1.6.51",
|
||||||
|
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
|
||||||
|
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"big.js": {
|
"big.js": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||||
@ -26078,6 +26312,11 @@
|
|||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"bn.js": {
|
||||||
|
"version": "4.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||||
|
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
|
||||||
|
},
|
||||||
"body-parser": {
|
"body-parser": {
|
||||||
"version": "1.20.0",
|
"version": "1.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
|
||||||
@ -26165,6 +26404,23 @@
|
|||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"broadcast-channel": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
|
||||||
|
"integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.7.2",
|
||||||
|
"detect-node": "^2.1.0",
|
||||||
|
"js-sha3": "0.8.0",
|
||||||
|
"microseconds": "0.2.0",
|
||||||
|
"nano-time": "1.0.0",
|
||||||
|
"oblivious-set": "1.0.0",
|
||||||
|
"rimraf": "3.0.2",
|
||||||
|
"unload": "2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"browser-sync": {
|
"browser-sync": {
|
||||||
"version": "2.27.10",
|
"version": "2.27.10",
|
||||||
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
|
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
|
||||||
@ -27730,6 +27986,13 @@
|
|||||||
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"detect-node": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"detective": {
|
"detective": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
|
||||||
@ -30271,6 +30534,14 @@
|
|||||||
"entities": "^2.0.0"
|
"entities": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"http_ece": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
|
||||||
|
"requires": {
|
||||||
|
"urlsafe-base64": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"http-basic": {
|
"http-basic": {
|
||||||
"version": "8.1.3",
|
"version": "8.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
|
||||||
@ -31084,6 +31355,13 @@
|
|||||||
"nopt": "^5.0.0"
|
"nopt": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"js-sha3": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"js-stringify": {
|
"js-stringify": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
|
||||||
@ -32094,6 +32372,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"match-sorter": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"remove-accents": "0.4.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"matcher": {
|
"matcher": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz",
|
||||||
@ -32706,6 +32995,13 @@
|
|||||||
"picomatch": "^2.3.1"
|
"picomatch": "^2.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"microseconds": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"mime": {
|
"mime": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
|
||||||
@ -32744,6 +33040,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||||
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="
|
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="
|
||||||
},
|
},
|
||||||
|
"minimalistic-assert": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||||
|
},
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@ -32970,6 +33271,16 @@
|
|||||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"nano-time": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"big-integer": "^1.6.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nanoassert": {
|
"nanoassert": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz",
|
||||||
@ -33465,6 +33776,13 @@
|
|||||||
"es-abstract": "^1.19.1"
|
"es-abstract": "^1.19.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"oblivious-set": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"on-finished": {
|
"on-finished": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
@ -34955,6 +35273,18 @@
|
|||||||
"react-merge-refs": "1.1.0"
|
"react-merge-refs": "1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-query": {
|
||||||
|
"version": "3.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.0.tgz",
|
||||||
|
"integrity": "sha512-Od0IkSuS79WJOhzWBx/ys0x13+7wFqgnn64vBqqAAnZ9whocVhl/y1padD5uuZ6EIkXbFbInax0qvY7zGM0thA==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"broadcast-channel": "^3.4.1",
|
||||||
|
"match-sorter": "^6.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-refresh": {
|
"react-refresh": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz",
|
||||||
@ -35449,6 +35779,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"remove-accents": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
|
||||||
|
"integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"repeat-element": {
|
"repeat-element": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
|
||||||
@ -37748,6 +38085,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||||
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
|
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
|
||||||
},
|
},
|
||||||
|
"unload": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.6.2",
|
||||||
|
"detect-node": "^2.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"unpipe": {
|
"unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
@ -37839,6 +38187,11 @@
|
|||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"urlsafe-base64": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-I/iQaabGL0bPOh07ABac77kL4MY="
|
||||||
|
},
|
||||||
"use": {
|
"use": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||||
@ -38051,6 +38404,40 @@
|
|||||||
"util": "^0.12.3"
|
"util": "^0.12.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web-push": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-JC0V9hzKTqlDYJ+LTZUXtW7B175qwwaqzbbMSWDxHWxZvd3xY0C2rcotMGDavub2nAAFw+sXTsqR65/KY2A5AQ==",
|
||||||
|
"requires": {
|
||||||
|
"asn1.js": "^5.3.0",
|
||||||
|
"http_ece": "1.1.0",
|
||||||
|
"https-proxy-agent": "^5.0.0",
|
||||||
|
"jws": "^4.0.0",
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"urlsafe-base64": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
||||||
|
"requires": {
|
||||||
|
"buffer-equal-constant-time": "1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jws": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||||
|
"requires": {
|
||||||
|
"jwa": "^2.0.0",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"web-resource-inliner": {
|
"web-resource-inliner": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz",
|
||||||
|
@ -7,12 +7,14 @@
|
|||||||
"dev:css": "cross-env NODE_ENV=development tailwindcss -i ./styles/tailwind.css -o ./app/styles/tailwind.css --watch",
|
"dev:css": "cross-env NODE_ENV=development tailwindcss -i ./styles/tailwind.css -o ./app/styles/tailwind.css --watch",
|
||||||
"dev:remix": "cross-env NODE_ENV=development remix watch",
|
"dev:remix": "cross-env NODE_ENV=development remix watch",
|
||||||
"dev:server": "cross-env NODE_ENV=development dotenv node ./server.js",
|
"dev:server": "cross-env NODE_ENV=development dotenv node ./server.js",
|
||||||
|
"dev:worker": "esbuild ./app/entry.worker.ts --outfile=./public/entry.worker.js --bundle --format=esm --watch",
|
||||||
"dev:init": "cross-env NODE_ENV=development dotenv run-s build:remix build:server",
|
"dev:init": "cross-env NODE_ENV=development dotenv run-s build:remix build:server",
|
||||||
"dev": "npm run dev:init && run-p dev:build dev:css dev:remix dev:server",
|
"dev": "npm run dev:init && run-p dev:build dev:worker dev:css dev:remix dev:server",
|
||||||
"build:server": "node ./scripts/build-server.js",
|
"build:server": "node ./scripts/build-server.js",
|
||||||
"build:css": "tailwindcss -i ./styles/tailwind.css -o ./app/styles/tailwind.css",
|
"build:css": "tailwindcss -i ./styles/tailwind.css -o ./app/styles/tailwind.css",
|
||||||
"build:remix": "remix build",
|
"build:remix": "remix build",
|
||||||
"build": "cross-env NODE_ENV=production run-s build:css build:remix build:server",
|
"build:worker": "esbuild ./app/entry.worker.ts --outfile=./public/entry.worker.js --minify --bundle --format=esm",
|
||||||
|
"build": "cross-env NODE_ENV=production run-s build:css build:worker build:remix build:server",
|
||||||
"start": "cross-env NODE_ENV=production node ./server.js",
|
"start": "cross-env NODE_ENV=production node ./server.js",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
@ -88,6 +90,7 @@
|
|||||||
"tiny-invariant": "1.2.0",
|
"tiny-invariant": "1.2.0",
|
||||||
"tslog": "3.3.3",
|
"tslog": "3.3.3",
|
||||||
"twilio": "3.77.0",
|
"twilio": "3.77.0",
|
||||||
|
"web-push": "3.5.0",
|
||||||
"zod": "3.16.0"
|
"zod": "3.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -107,6 +110,7 @@
|
|||||||
"@types/react": "18.0.9",
|
"@types/react": "18.0.9",
|
||||||
"@types/react-dom": "18.0.4",
|
"@types/react-dom": "18.0.4",
|
||||||
"@types/secure-password": "3.1.1",
|
"@types/secure-password": "3.1.1",
|
||||||
|
"@types/web-push": "3.3.2",
|
||||||
"@vitejs/plugin-react": "1.3.2",
|
"@vitejs/plugin-react": "1.3.2",
|
||||||
"c8": "7.11.2",
|
"c8": "7.11.2",
|
||||||
"cypress": "9.6.1",
|
"cypress": "9.6.1",
|
||||||
|
@ -157,6 +157,20 @@ CREATE TABLE "PhoneNumber" (
|
|||||||
CONSTRAINT "PhoneNumber_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "PhoneNumber_pkey" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 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,
|
||||||
|
"membershipId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "NotificationSubscription_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "TwilioAccount_organizationId_key" ON "TwilioAccount"("organizationId");
|
CREATE UNIQUE INDEX "TwilioAccount_organizationId_key" ON "TwilioAccount"("organizationId");
|
||||||
|
|
||||||
@ -184,10 +198,12 @@ CREATE INDEX "PhoneCall_phoneNumberId_recipient_idx" ON "PhoneCall"("phoneNumber
|
|||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "PhoneNumber_organizationId_isCurrent_key" ON "PhoneNumber"("organizationId", "isCurrent") WHERE ("isCurrent" = true);
|
CREATE UNIQUE INDEX "PhoneNumber_organizationId_isCurrent_key" ON "PhoneNumber"("organizationId", "isCurrent") WHERE ("isCurrent" = true);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "NotificationSubscription_endpoint_key" ON "NotificationSubscription"("endpoint");
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "TwilioAccount" ADD CONSTRAINT "TwilioAccount_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "TwilioAccount" ADD CONSTRAINT "TwilioAccount_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
@ -214,3 +230,6 @@ ALTER TABLE "PhoneCall" ADD CONSTRAINT "PhoneCall_phoneNumberId_fkey" FOREIGN KE
|
|||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "PhoneNumber" ADD CONSTRAINT "PhoneNumber_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "PhoneNumber" ADD CONSTRAINT "PhoneNumber_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "NotificationSubscription" ADD CONSTRAINT "NotificationSubscription_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "Membership"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
@ -51,14 +51,15 @@ model Subscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Membership {
|
model Membership {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
role MembershipRole
|
role MembershipRole
|
||||||
organizationId String
|
organizationId String
|
||||||
userId String?
|
userId String?
|
||||||
invitedEmail String?
|
invitedEmail String?
|
||||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
invitationToken Token?
|
invitationToken Token?
|
||||||
|
notificationSubscription NotificationSubscription[]
|
||||||
|
|
||||||
@@unique([organizationId, invitedEmail])
|
@@unique([organizationId, invitedEmail])
|
||||||
}
|
}
|
||||||
@ -147,6 +148,19 @@ model PhoneNumber {
|
|||||||
@@unique([organizationId, isCurrent])
|
@@unique([organizationId, isCurrent])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
membership Membership? @relation(fields: [membershipId], references: [id], onDelete: Cascade)
|
||||||
|
membershipId String?
|
||||||
|
}
|
||||||
|
|
||||||
enum SubscriptionStatus {
|
enum SubscriptionStatus {
|
||||||
active
|
active
|
||||||
trialing
|
trialing
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<TileColor>#663399</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@ -2,7 +2,7 @@
|
|||||||
"name": "Shellphone: Your Personal Cloud Phone",
|
"name": "Shellphone: Your Personal Cloud Phone",
|
||||||
"short_name": "Shellphone",
|
"short_name": "Shellphone",
|
||||||
"lang": "en-US",
|
"lang": "en-US",
|
||||||
"start_url": "/",
|
"start_url": "/messages",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"shortcuts": [
|
"shortcuts": [
|
||||||
{
|
{
|
||||||
@ -18,18 +18,18 @@
|
|||||||
],
|
],
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/android-chrome-192x192.png",
|
"src": "/icons/android-chrome-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/android-chrome-384x384.png",
|
"src": "/icons/android-chrome-384x384.png",
|
||||||
"sizes": "384x384",
|
"sizes": "384x384",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"theme_color": "#663399",
|
"theme_color": "#0062CC",
|
||||||
"background_color": "#F9FAFB"
|
"background_color": "#F4F4F5"
|
||||||
}
|
}
|
||||||
|