diff --git a/.env.example b/.env.example index ffe3c1c..09212f8 100644 --- a/.env.example +++ b/.env.example @@ -41,5 +41,5 @@ FEEDBACK_EMAIL= # generate web push keys using this command: web-push generate-vapid-keys --json WEB_PUSH_PRIVATE_KEY= -NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY= +WEB_PUSH_PUBLIC_KEY= WEB_PUSH_EMAIL= diff --git a/docker/prod/compose.yml b/docker/prod/compose.yml index ede75cd..e20f158 100644 --- a/docker/prod/compose.yml +++ b/docker/prod/compose.yml @@ -32,6 +32,14 @@ services: - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err} - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:?err} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:?err} + - WEB_PUSH_PRIVATE_KEY=${WEB_PUSH_PRIVATE_KEY} + - WEB_PUSH_PUBLIC_KEY=${WEB_PUSH_PUBLIC_KEY} + - WEB_PUSH_EMAIL=${WEB_PUSH_EMAIL} + - R2_ACCESS_KEY=${R2_ACCESS_KEY} + - R2_SECRET_KEY=${R2_SECRET_KEY} + - R2_BUCKET=${R2_BUCKET} + - R2_URL=${R2_URL} + - NEXT_PUBLIC_R2_PUBLIC_URL=${NEXT_PUBLIC_R2_PUBLIC_URL} depends_on: postgres: condition: service_healthy diff --git a/src/components/Account/SubscribeNotification.tsx b/src/components/Account/SubscribeNotification.tsx index 4d1ba47..9a97bfc 100644 --- a/src/components/Account/SubscribeNotification.tsx +++ b/src/components/Account/SubscribeNotification.tsx @@ -4,6 +4,7 @@ import { toast } from 'sonner'; import { env } from '~/env'; import { Bell, BellOff, ChevronRight } from 'lucide-react'; import { api } from '~/utils/api'; +import { useAppStore } from '~/store/appStore'; const base64ToUint8Array = (base64: string) => { const padding = '='.repeat((4 - (base64.length % 4)) % 4); @@ -21,6 +22,7 @@ const base64ToUint8Array = (base64: string) => { export const SubscribeNotification: React.FC = () => { const updatePushSubscription = api.user.updatePushNotification.useMutation(); const [isSubscribed, setIsSubscribed] = useState(false); + const webPushPublicKey = useAppStore((s) => s.webPushPublicKey); useEffect(() => { if (typeof window !== 'undefined' && 'serviceWorker' in navigator) { @@ -47,14 +49,14 @@ export const SubscribeNotification: React.FC = () => { toast.success('You will receive notifications now'); navigator.serviceWorker.ready .then(async (reg) => { - if (!env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY) { + if (!webPushPublicKey) { toast.error('Notification is not supported'); return; } const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: base64ToUint8Array(env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY), + applicationServerKey: base64ToUint8Array(webPushPublicKey), }); setIsSubscribed(true); @@ -82,7 +84,7 @@ export const SubscribeNotification: React.FC = () => { } } - if (!env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY) { + if (!webPushPublicKey) { return null; } diff --git a/src/components/NotificationModal.tsx b/src/components/NotificationModal.tsx index 501c689..f18b69b 100644 --- a/src/components/NotificationModal.tsx +++ b/src/components/NotificationModal.tsx @@ -13,6 +13,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from './ui/alert-dialog'; +import { useAppStore } from '~/store/appStore'; const base64ToUint8Array = (base64: string) => { const padding = '='.repeat((4 - (base64.length % 4)) % 4); @@ -32,6 +33,8 @@ const NOTIFICATION_DISMISSED_TIME_THRESHOLD = 1000 * 60 * 60 * 24 * 30; // 14 da export const NotificationModal: React.FC = () => { const updatePushSubscription = api.user.updatePushNotification.useMutation(); + const webPushPublicKey = useAppStore((s) => s.webPushPublicKey); + const [modalOpen, setModalOpen] = useState(false); useEffect(() => { @@ -65,12 +68,12 @@ export const NotificationModal: React.FC = () => { toast.success('You will receive notifications now'); navigator.serviceWorker.ready .then(async (reg) => { - if (!env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY) { + if (!webPushPublicKey) { return; } const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: base64ToUint8Array(env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY), + applicationServerKey: base64ToUint8Array(webPushPublicKey), }); updatePushSubscription.mutate({ subscription: JSON.stringify(sub) }); @@ -90,7 +93,7 @@ export const NotificationModal: React.FC = () => { setModalOpen(false); } - if (!env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY) { + if (!webPushPublicKey) { return null; } diff --git a/src/env.js b/src/env.js index dac246b..ac03c9d 100644 --- a/src/env.js +++ b/src/env.js @@ -35,6 +35,7 @@ export const env = createEnv({ WEB_PUSH_PRIVATE_KEY: z.string().optional(), UNSEND_API_KEY: z.string().optional(), UNSEND_URL: z.string().optional(), + WEB_PUSH_PUBLIC_KEY: z.string().optional(), }, /** @@ -44,7 +45,6 @@ export const env = createEnv({ */ client: { NEXT_PUBLIC_R2_PUBLIC_URL: z.string().optional(), - NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY: z.string().optional(), NEXT_PUBLIC_BEAM_ID: z.string().optional(), }, @@ -70,7 +70,7 @@ export const env = createEnv({ NEXT_PUBLIC_R2_PUBLIC_URL: process.env.NEXT_PUBLIC_R2_PUBLIC_URL, WEB_PUSH_EMAIL: process.env.WEB_PUSH_EMAIL, WEB_PUSH_PRIVATE_KEY: process.env.WEB_PUSH_PRIVATE_KEY, - NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, + WEB_PUSH_PUBLIC_KEY: process.env.WEB_PUSH_PUBLIC_KEY, NEXT_PUBLIC_BEAM_ID: process.env.NEXT_PUBLIC_BEAM_ID, UNSEND_API_KEY: process.env.UNSEND_API_KEY, UNSEND_URL: process.env.UNSEND_URL, diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 2f906ad..3742fef 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -13,6 +13,7 @@ import { type NextPageWithUser } from '~/types'; import { LoadingSpinner } from '~/components/ui/spinner'; import { useEffect, useState } from 'react'; import { useAddExpenseStore } from '~/store/addStore'; +import { useAppStore } from '~/store/appStore'; const poppins = Poppins({ weight: ['200', '300', '400', '500', '600', '700'], subsets: ['latin'] }); @@ -82,6 +83,9 @@ const Auth: React.FC<{ Page: NextPageWithUser; pageProps: any }> = ({ Page, page const [showSpinner, setShowSpinner] = useState(false); const { setCurrency } = useAddExpenseStore((s) => s.actions); + const { setWebPushPublicKey } = useAppStore((s) => s.actions); + + const { data: webPushPublicKey } = api.user.getWebPushPublicKey.useQuery(); useEffect(() => { setTimeout(() => { @@ -89,6 +93,12 @@ const Auth: React.FC<{ Page: NextPageWithUser; pageProps: any }> = ({ Page, page }, 300); }, []); + useEffect(() => { + if (webPushPublicKey) { + setWebPushPublicKey(webPushPublicKey); + } + }, [webPushPublicKey, setWebPushPublicKey]); + useEffect(() => { if (status === 'authenticated') { setCurrency(data.user.currency); diff --git a/src/pages/balances.tsx b/src/pages/balances.tsx index 1a30b5e..de46589 100644 --- a/src/pages/balances.tsx +++ b/src/pages/balances.tsx @@ -14,6 +14,7 @@ import { type NextPageWithUser } from '~/types'; import useEnableAfter from '~/hooks/useEnableAfter'; import { LoadingSpinner } from '~/components/ui/spinner'; import { NotificationModal } from '~/components/NotificationModal'; +import { GetServerSideProps } from 'next'; const BalancePage: NextPageWithUser = () => { function shareWithFriends() { @@ -179,4 +180,9 @@ const FriendBalance: React.FC<{ BalancePage.auth = true; +// export const getServerSideProps = (async () => { + +// return { props: { webPushKey: env } }; +// }) satisfies GetServerSideProps<{ webPushKey: string }>; + export default BalancePage; diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index f9ce8ae..01f24cb 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -18,6 +18,7 @@ import { sendFeedbackEmail, sendInviteEmail } from '~/server/mailer'; import { pushNotification } from '~/server/notification'; import { toFixedNumber, toUIString } from '~/utils/numbers'; import { SplitwiseGroupSchema, SplitwiseUserSchema } from '~/types'; +import { env } from '~/env'; export const userRouter = createTRPCRouter({ me: protectedProcedure.query(async ({ ctx }) => { @@ -477,4 +478,8 @@ export const userRouter = createTRPCRouter({ await importUserBalanceFromSplitWise(ctx.session.user.id, input.usersWithBalance); await importGroupFromSplitwise(ctx.session.user.id, input.groups); }), + + getWebPushPublicKey: protectedProcedure.query(async ({ ctx }) => { + return env.WEB_PUSH_PUBLIC_KEY; + }), }); diff --git a/src/server/notification.ts b/src/server/notification.ts index 5639df3..b50e146 100644 --- a/src/server/notification.ts +++ b/src/server/notification.ts @@ -1,10 +1,10 @@ import webPush, { type PushSubscription } from 'web-push'; import { env } from '~/env'; -if (env.WEB_PUSH_EMAIL && env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY && env.WEB_PUSH_PRIVATE_KEY) { +if (env.WEB_PUSH_EMAIL && env.WEB_PUSH_PUBLIC_KEY && env.WEB_PUSH_PRIVATE_KEY) { webPush.setVapidDetails( `mailto:${env.WEB_PUSH_EMAIL}`, - env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, + env.WEB_PUSH_PUBLIC_KEY, env.WEB_PUSH_PRIVATE_KEY, ); } diff --git a/src/store/appStore.ts b/src/store/appStore.ts new file mode 100644 index 0000000..d20fc6c --- /dev/null +++ b/src/store/appStore.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand'; + +interface AppState { + webPushPublicKey: string | null; + actions: { + setWebPushPublicKey: (key: string) => void; + }; +} + +export const useAppStore = create()((set) => ({ + webPushPublicKey: null, + actions: { + setWebPushPublicKey: (key) => set({ webPushPublicKey: key }), + }, +}));