diff --git a/App.tsx b/App.tsx index 3c37d37f..871a4f99 100644 --- a/App.tsx +++ b/App.tsx @@ -4,89 +4,8 @@ import AppInner from './AppInner.tsx'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {BottomSheetModalProvider} from '@gorhom/bottom-sheet'; -import messaging from '@react-native-firebase/messaging'; -import PushNotification from 'react-native-push-notification'; -import PushNotificationIOS from '@react-native-community/push-notification-ios'; - import store from './src/redux/stores'; import './localConfig'; -import {Alert, NativeModules, Platform} from 'react-native'; - -messaging().setBackgroundMessageHandler(async remoteMessage => { - console.log('Message handled in the background!', remoteMessage); -}); - -PushNotification.configure({ - // (optional) 토큰이 생성될 때 실행됨(토큰을 서버에 등록할 때 쓸 수 있음) - onRegister: function (token: any) { - console.log('TOKEN2:', token); - }, - - // (required) 리모트 노티를 수신하거나, 열었거나 로컬 노티를 열었을 때 실행 - onNotification: function (notification: any) { - console.log('NOTIFICATION:', notification); - if (notification.channelId === 'riders') { - // if (notification.message || notification.data.message) { - // store.dispatch( - // userSlice.actions.showPushPopup( - // notification.message || notification.data.message, - // ), - // ); - // } - } - // process the notification - - // (required) 리모트 노티를 수신하거나, 열었거나 로컬 노티를 열었을 때 실행 - notification.finish(PushNotificationIOS.FetchResult.NoData); - }, - - // (optional) 등록한 액션을 누렀고 invokeApp이 false 상태일 때 실행됨, true면 onNotification이 실행됨 (Android) - onAction: function (notification: any) { - console.log('ACTION:', notification.action); - console.log('NOTIFICATION:', notification); - - // process the action - }, - - // (optional) Called when the user fails to register for remote notifications. Typically occurs when APNS is having issues, or the device is a simulator. (iOS) - onRegistrationError: function (err: Error) { - console.error(err.message, err); - }, - - // IOS ONLY (optional): default: all - Permissions to register. - permissions: { - alert: true, - badge: true, - sound: true, - }, - - // Should the initial notification be popped automatically - // default: true - popInitialNotification: true, - - /** - * (optional) default: true - * - Specified if permissions (ios) and token (android and ios) will requested or not, - * - if not, you must call PushNotificationsHandler.requestPermissions() later - * - if you are not using remote notification or do not have Firebase installed, use this: - * requestPermissions: Platform.OS === 'ios' - */ - requestPermissions: true, -}); -PushNotification.createChannel( - { - channelId: 'riders', // (required) - channelName: '앱 전반', // (required) - channelDescription: '앱 실행하는 알림', // (optional) default: undefined. - soundName: 'default', // (optional) See `soundName` parameter of `localNotification` function - importance: 4, // (optional) default: 4. Int value of the Android notification importance - vibrate: true, // (optional) default: true. Creates the default vibration patten if true. - }, - (created: boolean) => - console.log(`createChannel riders returned '${created}'`), // (optional) callback returns whether the channel was created, false means it already existed. -); - -// ㄴ function App() { return ( diff --git a/src/apis/popup/addInterestPopUp.ts b/src/apis/popup/addInterestPopUp.ts index 0b35b919..a6c42e5f 100644 --- a/src/apis/popup/addInterestPopUp.ts +++ b/src/apis/popup/addInterestPopUp.ts @@ -7,17 +7,16 @@ export interface AddRecommendReviewResponse { message: string; }; } -const addInterestPopup = async (popupId: number, fcmToken: string) => { +interface AddInterestPopupParams { + popupId: number; + fcm_token: string; +} +const addInterestPopup = async (params: AddInterestPopupParams) => { try { const response = await nonPublicApiInstance.post( '/api/v1/interest/add-interest', null, - { - params: { - popupId: popupId, - fcm_token: 'fcmToken', - }, - }, + {params: params}, ); console.log('addInterestPopup response:', response.data); diff --git a/src/apis/popup/deleteInterestPopUp.ts b/src/apis/popup/deleteInterestPopUp.ts index 36c88157..066b3a6b 100644 --- a/src/apis/popup/deleteInterestPopUp.ts +++ b/src/apis/popup/deleteInterestPopUp.ts @@ -1,14 +1,23 @@ import nonPublicApiInstance from '../apiInstance/NonPublicApiInstance.ts'; -const deleteInterestPopUp = async (popupId: number, fcm_token: string) => { + +interface AddInterestPopupParams { + popupId: number; + fcm_token: string; +} + +const deleteInterestPopUp = async (params: AddInterestPopupParams) => { try { const response = await nonPublicApiInstance.delete( '/api/v1/interest/remove-interest', { - params: {popupId: popupId, fcm_token: fcm_token}, + data: null, + params: params, }, ); + console.log(response.data); + if (response.data.success) { return response.data; } else { diff --git a/src/apis/push/firebase.ts b/src/apis/push/firebase.ts new file mode 100644 index 00000000..05a827de --- /dev/null +++ b/src/apis/push/firebase.ts @@ -0,0 +1,48 @@ +import {NavigationProp} from '@react-navigation/native'; +import PushNotificationIOS, { + PushNotification, +} from '@react-native-community/push-notification-ios'; +import messaging from '@react-native-firebase/messaging'; + +export const initFirebaseNotification = ( + navigation: NavigationProp, +) => { + // 푸시 메세지 Notification 처리 + messaging().onMessage(async remoteMessage => { + console.log('Message handled in the foreground!', remoteMessage); + if (remoteMessage.data) { + PushNotificationIOS.addNotificationRequest({ + id: 'remoteMessage', // unique identifier for the notification + title: remoteMessage.notification?.title || 'Default Title', + body: remoteMessage.notification?.body || 'Default Body', + userInfo: remoteMessage.data, + }); + } + }); + + // 백그라운드에서 Notification 눌렀을 때 이벤트 처리 + messaging().onNotificationOpenedApp(remoteMessage => { + try { + console.log('Message handled in the background!', remoteMessage); + const popupId = remoteMessage?.data?.popupId; + // @ts-ignore + navigation.navigate('PopUpDetail', {id: popupId}); + } catch (error) { + console.log('Background Notification Error', error); + } + }); + + // 포그라운드에서 Notification 눌렀을 때 이벤트 처리 + PushNotificationIOS.addEventListener( + 'localNotification', + (notification: PushNotification) => { + try { + const pushData = notification.getData(); + // @ts-ignore + navigation.navigate('PopUpDetail', {id: pushData.popupId}); + } catch (error) { + console.log('Foreground Notification Error', error); + } + }, + ); +}; diff --git a/src/apis/push/registerPushToken.ts b/src/apis/push/registerPushToken.ts new file mode 100644 index 00000000..ba9bdf00 --- /dev/null +++ b/src/apis/push/registerPushToken.ts @@ -0,0 +1,13 @@ +import PublicApiInstance from '../apiInstance/PublicApiInstance.ts'; + +interface PushTokenParams { + token: string; + device: string; +} +export const registerPushToken = async (params: PushTokenParams) => { + const response = await PublicApiInstance.post( + '/api/v1/noti/apply/FCMtoken', + params, + ); + return response.data; +}; diff --git a/src/hooks/detailPopUp/useAddInterestPopUp.tsx b/src/hooks/detailPopUp/useAddInterestPopUp.tsx index 3dfd8780..64d4dd1f 100644 --- a/src/hooks/detailPopUp/useAddInterestPopUp.tsx +++ b/src/hooks/detailPopUp/useAddInterestPopUp.tsx @@ -1,5 +1,6 @@ import {useState} from 'react'; import addInterestPopUp from '../../apis/popup/addInterestPopUp.ts'; +import EncryptedStorage from "react-native-encrypted-storage"; interface AddInterestState { loading: boolean; @@ -14,10 +15,11 @@ const useAddInterestPopUp = () => { success: null, }); - const addInterest = async (popUpId: number, fcm_token: string) => { + const addInterest = async (popupId: number) => { setAddInterestState({loading: true, error: null, success: null}); try { - const response = await addInterestPopUp(popUpId, fcm_token); + const fcm_token = (await EncryptedStorage.getItem('pushToken')) ?? ''; + const response = await addInterestPopUp({popupId, fcm_token}); if (response.success) { setAddInterestState({loading: false, error: null, success: true}); } else { diff --git a/src/hooks/detailPopUp/useDeleteInterestPopUp.tsx b/src/hooks/detailPopUp/useDeleteInterestPopUp.tsx index 5bd89787..5e1bcd99 100644 --- a/src/hooks/detailPopUp/useDeleteInterestPopUp.tsx +++ b/src/hooks/detailPopUp/useDeleteInterestPopUp.tsx @@ -1,5 +1,6 @@ import {useState} from 'react'; import getDeletePopUp from '../../apis/popup/deleteInterestPopUp.ts'; +import EncryptedStorage from "react-native-encrypted-storage"; interface DeleteInterestState { loading: boolean; @@ -15,11 +16,12 @@ const useDeleteInterestPopUp = () => { success: null, }); - const deleteInterest = async (popUpId: number, fcm_token: string) => { + const deleteInterest = async (popupId: number) => { setDeleteInterestState({loading: true, error: null, success: null}); try { - const response = await getDeletePopUp(popUpId, fcm_token); + const fcm_token = (await EncryptedStorage.getItem('pushToken')) ?? ''; + const response = await getDeletePopUp({popupId, fcm_token}); if (response.success) { setDeleteInterestState({loading: false, error: null, success: true}); } else { diff --git a/src/hooks/findPopUp/usePostBookmarkPopup.tsx b/src/hooks/findPopUp/usePostBookmarkPopup.tsx index 2075b43b..d5b92558 100644 --- a/src/hooks/findPopUp/usePostBookmarkPopup.tsx +++ b/src/hooks/findPopUp/usePostBookmarkPopup.tsx @@ -1,5 +1,6 @@ import {useState} from 'react'; import addInterestPopUp from '../../apis/popup/addInterestPopUp.ts'; +import EncryptedStorage from "react-native-encrypted-storage"; interface AddInterestState { loading: boolean; @@ -14,10 +15,11 @@ const usePostBookmarkPopup = () => { success: null, }); - const addInterest = async (popUpId: number) => { + const addInterest = async (popupId: number) => { setAddInterestState({loading: true, error: null, success: null}); try { - const response = await addInterestPopUp(popUpId); + const fcm_token = (await EncryptedStorage.getItem('pushToken')) ?? ''; + const response = await addInterestPopUp({popupId, fcm_token}); if (response.success) { setAddInterestState({loading: false, error: null, success: true}); } else { diff --git a/src/navigators/AppNavigator.tsx b/src/navigators/AppNavigator.tsx index e143ae0c..b01af1df 100644 --- a/src/navigators/AppNavigator.tsx +++ b/src/navigators/AppNavigator.tsx @@ -49,6 +49,8 @@ import ReportOptions from './options/ReportOptions.tsx'; import PopUpEditRequestScreen from '../pages/detail/PopUpEditRequestScreen.tsx'; import PopUpEditRequestOptions from './options/PopUpEditRequestOptions.tsx'; import PrivacyPolicyOptions from './options/PrivacyPolicyOptions.tsx'; +import {useNavigation} from '@react-navigation/native'; +import {initFirebaseNotification} from '../apis/push/firebase.ts'; import PreferenceSettingScreen from '../pages/myPage/preferenceSetting/PreferenceSettingScreen.tsx'; import FAQFormScreen from '../pages/myPage/FAQFormScreen.tsx'; import PasswordCheckScreen from '../pages/myPage/PasswordCheckScreen.tsx'; @@ -67,6 +69,9 @@ const DefaultNoHeaderOptions = { }; function AppNavigator() { + const navigation = useNavigation(); + initFirebaseNotification(navigation); + return ( { const dispatch = useDispatch(); @@ -63,8 +65,22 @@ const RootNavigator = () => { } const token = await messaging().getToken(); console.log('phone token', token); - // dispatch(userSlice.actions.setPhoneToken(token)); - // return axios.post(`${Config.API_URL}/phonetoken`, {token}); + + const storedToken = await EncryptedStorage.getItem('pushToken'); + if (storedToken === token) { + console.log('푸시토큰이 이미 등록되어 있습니다.'); + return; + } + const response = await registerPushToken({ + token: token, + device: Platform.OS, + }); + if (response?.success) { + console.log('푸시 토큰 등록에 성공했습니다.'); + await EncryptedStorage.setItem('pushToken', token); + } else { + console.error(`푸시 토큰 등록에 실패했습니다. ${response?.error}`); + } } catch (error) { console.log(error); }