Skip to content

Commit 4fd93b6

Browse files
leomp12claude
andcommitted
feat(storefront): Add web push notifications support with Firebase Cloud Messaging
Implements composable and utilities for Firebase Cloud Messaging integration: - Composable use-notification-permission with permission handling and foreground listener setup - Firebase-specific implementation in firebase-notifications script with async imports - VAPID key support in Firebase config type definition 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 3256a0a commit 4fd93b6

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

packages/storefront/client.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface Window {
1818
messagingSenderId: string,
1919
appId: string,
2020
measurementId?: string,
21+
vapidKey?: string,
2122
};
2223
$reCaptchaSiteKey?: string;
2324
ECOM_STORE_ID: number;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { ref, onMounted } from 'vue';
2+
3+
export type Props = {
4+
delayBeforePrompt?: number;
5+
daysBeforeReprompt?: number;
6+
onTokenReceived?: (token: string) => void | Promise<void>;
7+
registerFn?: () => Promise<string | null>;
8+
setupListenerFn?: (callback: (payload: any) => void) => Promise<void>;
9+
}
10+
11+
const useNotificationPermission = (props: Props = {}) => {
12+
const {
13+
delayBeforePrompt = 3000,
14+
daysBeforeReprompt = 7,
15+
onTokenReceived,
16+
registerFn,
17+
setupListenerFn,
18+
} = props;
19+
const isVisible = ref(false);
20+
const isRequesting = ref(false);
21+
22+
const checkPermission = () => {
23+
if (!('Notification' in window)) return false;
24+
if (Notification.permission === 'granted') return false;
25+
if (Notification.permission === 'denied') return false;
26+
const dismissed = localStorage.getItem('notification-prompt-dismissed');
27+
if (dismissed) {
28+
const dismissedAt = parseInt(dismissed, 10);
29+
const daysSinceDismiss = (Date.now() - dismissedAt) / (1000 * 60 * 60 * 24);
30+
if (daysSinceDismiss < daysBeforeReprompt) return false;
31+
}
32+
return true;
33+
};
34+
35+
const requestPermission = async () => {
36+
if (!registerFn) {
37+
console.error('registerFn not provided');
38+
return;
39+
}
40+
isRequesting.value = true;
41+
try {
42+
const token = await registerFn();
43+
if (token) {
44+
if (onTokenReceived) {
45+
await onTokenReceived(token);
46+
}
47+
isVisible.value = false;
48+
localStorage.removeItem('notification-prompt-dismissed');
49+
}
50+
} catch (error) {
51+
console.error('Error enabling notifications:', error);
52+
} finally {
53+
isRequesting.value = false;
54+
}
55+
};
56+
57+
const dismiss = () => {
58+
isVisible.value = false;
59+
localStorage.setItem('notification-prompt-dismissed', Date.now().toString());
60+
};
61+
62+
const setupForegroundListener = async () => {
63+
if (!setupListenerFn) return;
64+
await setupListenerFn((payload) => {
65+
console.log('Foreground notification:', payload);
66+
if (payload.notification) {
67+
const { title, body, icon } = payload.notification;
68+
// eslint-disable-next-line no-new
69+
new Notification(title || 'Nova notificação', {
70+
body,
71+
icon,
72+
data: payload.data,
73+
});
74+
}
75+
});
76+
};
77+
onMounted(() => {
78+
setTimeout(() => {
79+
isVisible.value = checkPermission();
80+
}, delayBeforePrompt);
81+
setupForegroundListener();
82+
});
83+
84+
return {
85+
isVisible,
86+
isRequesting,
87+
requestPermission,
88+
dismiss,
89+
};
90+
};
91+
92+
export default useNotificationPermission;
93+
94+
export {
95+
useNotificationPermission,
96+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export const registerNotification = async (): Promise<string | null> => {
2+
try {
3+
const vapidKey = window.$firebaseConfig?.vapidKey;
4+
if (!vapidKey) {
5+
console.error('VAPID key not configured');
6+
return null;
7+
}
8+
9+
const permission = await Notification.requestPermission();
10+
if (permission !== 'granted') {
11+
console.warn('Notification permission denied');
12+
return null;
13+
}
14+
15+
const { firebaseApp } = await import('./firebase-app');
16+
const { getMessaging, getToken } = await import('firebase/messaging');
17+
18+
const messaging = getMessaging(firebaseApp);
19+
const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js');
20+
const token = await getToken(messaging, {
21+
vapidKey,
22+
serviceWorkerRegistration: registration,
23+
});
24+
25+
if (token) {
26+
console.log('FCM token:', token);
27+
return token;
28+
}
29+
console.warn('Failed to get FCM token');
30+
return null;
31+
} catch (error) {
32+
console.error('Error requesting notification permission:', error);
33+
return null;
34+
}
35+
};
36+
37+
export const setupForegroundListener = async (
38+
callback: (payload: any) => void,
39+
): Promise<void> => {
40+
if (import.meta.env.SSR) return;
41+
if (Notification.permission !== 'granted') return;
42+
43+
try {
44+
const { firebaseApp } = await import('./firebase-app');
45+
const { getMessaging, onMessage } = await import('firebase/messaging');
46+
47+
const messaging = getMessaging(firebaseApp);
48+
onMessage(messaging, callback);
49+
} catch (error) {
50+
console.error('Error setting up foreground listener:', error);
51+
}
52+
};

0 commit comments

Comments
 (0)