Skip to content

Commit

Permalink
Merge pull request #35 from QRTaxi/push-noti
Browse files Browse the repository at this point in the history
Feat: 브라우저 푸시 알림 기능 추가
  • Loading branch information
HiimKwak authored Aug 16, 2023
2 parents ea53279 + 3570a6a commit a04610e
Show file tree
Hide file tree
Showing 13 changed files with 886 additions and 38 deletions.
11 changes: 10 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
VITE_BASE_URL = 'api.qrtaxi.co.kr'
VITE_BASE_URL = 'api.qrtaxi.co.kr'

VITE_FB_API_KEY = 'AIzaSyArnrcm0hW_k7suIGw_BgaFLIlJxTTxkPk'
VITE_FB_AUTH_DOMAIN = 'qrtaxi-caf9c.firebaseapp.com'
VITE_FB_PROJECT_ID = 'qrtaxi-caf9c'
VITE_FB_STORAGE_BUCKET = 'qrtaxi-caf9c.appspot.com'
VITE_FB_MESSAGING_SENDER_ID = '951135648092'
VITE_FB_APP_ID = '1:951135648092:web:adcb142f98491e56e26999'
VITE_FB_MEASUREMENT_ID = 'G-MKRQG86R31'
VITE_FB_VAPID_KEY = 'BEccaYOA6A6N2Xi9mf9RSz9lBNQwHkN918FWBYkoebObXQxQcXmBebux53cKsWq7eOEGlXUdKU3zV8JaAQ7MH-g'
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/*.js
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module.exports = {
'plugin:react-hooks/recommended',
'plugin:prettier/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
ignorePatterns: ['dist', '.eslintrc.cjs', './firebase-message-sw.ts'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"dependencies": {
"axios": "^1.4.0",
"firebase": "^10.1.0",
"lottie-react": "^2.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
42 changes: 42 additions & 0 deletions public/firebase-messaging-sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/// <reference lib="webworker" />

export default null;

self.addEventListener('install', event => {
console.log('fcm sw install..');
event.waitUntil(self.skipWaiting());
// .catch((error: Error) => console.error(error))
});

self.addEventListener('activate', () => {
console.log('fcm sw activate..');
});

self.addEventListener('push', event => {
if (!event.data) return;

const message = event.data.json();
const notificationOptions = {
body: message.notification.body,
icon: '/icons/QT_initial_v1.svg',
data: { url: `${self.location.origin}/${message.data.status}` },
};
event.waitUntil(
self.registration.showNotification(
message.notification.title,
notificationOptions,
),
);
});

self.addEventListener('notificationclick', event => {
// Close the notification popout
event.notification.close();
// Get all the Window clients
event.waitUntil(
clients
.openWindow(event.notification.data.url)
.then(windowClient => (windowClient ? windowClient.focus() : null))
.catch(error => console.error(error)),
);
});
30 changes: 21 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,35 @@ import GlobalStyle from 'src/styles/globalStyles';
import { ThemeProvider } from 'styled-components';
import { RouterProvider } from 'react-router-dom';
import { theme } from 'src/styles/theme';
import { Suspense } from 'react';
import { RecoilRoot } from 'recoil';
import { Suspense, useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import Router from './components/common/Router';
import Layout from './components/common/Layout';

import { requestPermission } from './FirebaseConfig';
import { userStatus } from './utils/recoil/store';

export default function App() {
const { id: assign_id } = useRecoilValue(userStatus);

// Need this handle FCM token generation when a user manually blocks or allows notification
useEffect(() => {
if (window.Notification?.permission === 'granted' && assign_id) {
requestPermission(assign_id).catch((error: Error) =>
console.error(error),
);
}
}, []);

return (
<>
<ThemeProvider theme={theme}>
<GlobalStyle />
<RecoilRoot>
<Layout>
<Suspense>
<RouterProvider router={Router} />
</Suspense>
</Layout>
</RecoilRoot>
<Layout>
<Suspense>
<RouterProvider router={Router} />
</Suspense>
</Layout>
</ThemeProvider>
</>
);
Expand Down
63 changes: 63 additions & 0 deletions src/FirebaseConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';
import UserApi from '@/utils/api/user';

interface FirebaseConfig {
apiKey: string;
authDomain: string;
projectId: string;
storageBucket: string;
messagingSenderId: string;
appId: string;
measurementId: string;
}

const firebaseConfig: FirebaseConfig = {
apiKey: import.meta.env.VITE_FB_API_KEY as string,
authDomain: import.meta.env.VITE_FB_AUTH_DOMAIN as string,
projectId: import.meta.env.VITE_FB_PROJECT_ID as string,
storageBucket: import.meta.env.VITE_FB_STORAGE_BUCKET as string,
messagingSenderId: import.meta.env.VITE_FB_MESSAGING_SENDER_ID as string,
appId: import.meta.env.VITE_FB_APP_ID as string,
measurementId: import.meta.env.VITE_FB_MEASUREMENT_ID as string,
};

// Initialize Firebase
export const firebaseApp = initializeApp(firebaseConfig);
export const messaging = getMessaging(firebaseApp);

// getFirebaseToken function generates the FCM token
export const handleFirebaseToken = async (assign_id: number) => {
try {
const fcm_token = await getToken(messaging, {
vapidKey: import.meta.env.VITE_FB_VAPID_KEY as string,
});
if (fcm_token && assign_id) {
UserApi.postFirebaseToken({ assign_id, push_token: fcm_token })
.then((response: number) => {
console.log(response);
})
.catch((error: Error) => console.error(error));
// set token on localStorage
localStorage.setItem('fcm_token', fcm_token);
}
} catch (error) {
console.error(error);
}
};

export const requestPermission = async (assign_id: number) => {
console.log('권한 요청 중...');
const permission = await Notification.requestPermission();
if (permission === 'denied') {
console.log('알림 권한 허용 안됨');
return;
}

console.log('알림 권한이 허용됨');
handleFirebaseToken(assign_id).catch((error: Error) => console.error(error));

onMessage(messaging, payload => {
console.log('메시지가 도착했습니다.', payload);
});
};
5 changes: 4 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RecoilRoot } from 'recoil';
import App from 'src/App';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
);
18 changes: 18 additions & 0 deletions src/utils/api/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
UserInfoFailedResponse,
DriverInfoFailedResponse,
CancelBookingFailedResponse,
FirebaseTokenPayload,
} from '@/utils/types/user';
import client from '../axios';
import { AxiosError, AxiosResponse } from 'axios';
Expand Down Expand Up @@ -100,6 +101,23 @@ class UserApi {
}
}
}

static async postFirebaseToken(payload: FirebaseTokenPayload) {
try {
const response: AxiosResponse = await client.post(
'/call/push/token/',
payload,
);
return response.status;
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError.response) {
return axiosError.response.status;
} else {
throw new Error();
}
}
}
}

export default UserApi;
2 changes: 1 addition & 1 deletion src/utils/recoil/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const userStatus = atom<UserStatus>({
key: 'userState',
default: {
hashed_assign_id: '',
id: 0,
id: null,
status: 'booking',
},
effects_UNSTABLE: [persistAtom],
Expand Down
18 changes: 17 additions & 1 deletion src/utils/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export type UserAssignID = UserInfoResponse['hashed_assign_id'];

export interface UserStatus {
hashed_assign_id: UserAssignID;
id: UserInfoResponse['id'];
id: UserInfoResponse['id'] | null;
status:
| 'booking'
| 'waiting'
Expand All @@ -83,3 +83,19 @@ export interface UserStatus {
| 'finish'
| 'cancel';
}

/* Firebase Token Types */
export interface FirebaseTokenPayload {
assign_id: number;
push_token: string;
}

export interface PushMessage {
notification: {
title: string;
body: string;
};
data: {
status: string;
};
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"lib": ["ES2020", "DOM", "DOM.Iterable", "webworker"],
"module": "ESNext",
"skipLibCheck": true,

Expand Down
Loading

0 comments on commit a04610e

Please sign in to comment.