Skip to content

Commit

Permalink
[SKP-189-notification] 푸시 랜딩 및 리스트 화면 API 연결 (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
yusiny authored Sep 24, 2024
2 parents e701a71 + 4541e5b commit ced1e79
Show file tree
Hide file tree
Showing 26 changed files with 418 additions and 86 deletions.
Empty file modified .husky/prepare-commit-msg
100755 → 100644
Empty file.
18 changes: 17 additions & 1 deletion App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useEffect} from 'react';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import Navigator from './src/navigators/Navigator';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
Expand All @@ -8,10 +8,26 @@ import AppSetupWrapper from './src/container/AppSetupContainer';
import Snackbar from './src/components/common/Global/Snackbar/Snackbar';
import Toast from './src/components/common/Global/Toast/Toast';
import usePushNotification from './src/hooks/usePushNotification';
import notifee, {EventType} from '@notifee/react-native';
import {getDeepLinkUrl, linkToDeepLinkURL} from './src/utils/pushUtils';

const App = () => {
usePushNotification();

useEffect(() => {
return notifee.onForegroundEvent(({type, detail}) => {
console.log('onForegroundEvent', type, detail);

if (type === EventType.PRESS) {
const deepLinkURL = getDeepLinkUrl(detail.notification);
if (deepLinkURL) {
console.log('url', deepLinkURL);
linkToDeepLinkURL(deepLinkURL);
}
}
});
});

return (
<AppSetupWrapper>
<SafeAreaProvider>
Expand Down
3 changes: 2 additions & 1 deletion ios/skeepcli/AppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
self.initialProps = @{};

[FIRApp configure];

[application registerForRemoteNotifications];

bool didFinish = [super application:application didFinishLaunchingWithOptions:launchOptions];
[RNSplashScreen show];

Expand Down
2 changes: 1 addition & 1 deletion ios/skeepcli/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>FirebaseAppDelegateProxyEnabled</key>
<true/>
<false/>
<key>LSApplicationCategoryType</key>
<string></string>
<key>KAKAO_APP_KEY</key>
Expand Down
6 changes: 6 additions & 0 deletions src/assets/icon/ic_noti_empty.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ export {default as IcRoundRest} from './ic_round_rest.svg';
export {default as IcRoundEtc} from './ic_round_etc.svg';

export {default as IcBell} from './ic_bell.svg';
export {default as IcNotiEmpty} from './ic_noti_empty.svg';
39 changes: 30 additions & 9 deletions src/components/Notification/NotificationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,56 @@ import React from 'react';
import {Pressable, StyleSheet, Text, View} from 'react-native';
import {theme} from '../../styles';
import {flexBox} from '../../styles/common';
import {NotificationDTO} from '../../hooks/queries/notification/useGetNotification';
import {usePatchNotification} from '../../hooks/mutations/notification/usePatchNotification';
import {useQueryClient} from '@tanstack/react-query';
import {NOTIFICATION_KEYS} from '../../hooks/queries/QueryKeys';

export interface NotificationDTO {
title: string;
date: string;
type: string;
isRead: boolean;
}
interface NotificationItemProps {
item: NotificationDTO;
}
export default function NotificationItem({item}: NotificationItemProps) {
const queryClient = useQueryClient();

const {mutate: checkNotitifcation} = usePatchNotification({
onSuccess(res) {
console.log(res);
queryClient.invalidateQueries({
queryKey: NOTIFICATION_KEYS.all,
});
},
onError(e) {
console.error(e);
},
});
function handleOnPress() {
/* 알림 타입 별로 랜딩 로직 수행 */
// 1. 푸시 확인 API
checkNotitifcation({id: item.id, type: item.type});
}

return (
<Pressable
style={({pressed}) => [
{backgroundColor: pressed ? '#43C7FF1A' : 'white'},
styles.container,
{
backgroundColor: pressed
? !item.isChecked
? '#43C7FF26'
: 'white'
: !item.isChecked
? '#43C7FF1A'
: 'white',
},
]}
onPress={handleOnPress}>
<View style={styles.topBox}>
<Text style={styles.subtitle}>{item.type}</Text>
{!item.isRead && <View style={styles.flag} />}
{!item.isChecked && <View style={styles.flag} />}
</View>

<Text style={styles.title}>{item.title}</Text>
<Text style={styles.date}>{item.date}</Text>
<Text style={styles.date}>{item.createdAt}</Text>
</Pressable>
);
}
Expand Down
28 changes: 19 additions & 9 deletions src/components/Settings/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import React from 'react';
import {View, Text} from 'react-native';
import {IcApple, IcProfile} from '../../assets/icon';
import styles from './Profile.style';
import {TouchableOpacity} from 'react-native-gesture-handler';
import {
sendCategoryPush,
sendDetailPush,
} from '../../hooks/mutations/test/useTestNotification';

type ProfileProps = {
userInfo: {
Expand All @@ -14,16 +19,21 @@ type ProfileProps = {
export default function Profile({userInfo}: ProfileProps) {
return (
<View style={styles.profileSection}>
<IcProfile width={60} height={60} />
<View style={styles.profileTextContainer}>
<Text style={styles.name}>{userInfo.name}</Text>
<View style={styles.emailContainer}>
<Text style={styles.email}>{userInfo.email}</Text>
{userInfo.provider === 'APPLE' && (
<IcApple width={18} height={18} style={styles.appleIcon} />
)}
<TouchableOpacity onPress={() => sendCategoryPush()}>
<IcProfile width={60} height={60} />
</TouchableOpacity>

<TouchableOpacity onPress={() => sendDetailPush()}>
<View style={styles.profileTextContainer}>
<Text style={styles.name}>{userInfo.name}</Text>
<View style={styles.emailContainer}>
<Text style={styles.email}>{userInfo.email}</Text>
{userInfo.provider === 'APPLE' && (
<IcApple width={18} height={18} style={styles.appleIcon} />
)}
</View>
</View>
</View>
</TouchableOpacity>
</View>
);
}
31 changes: 31 additions & 0 deletions src/hooks/mutations/notification/usePatchNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {useMutation} from '@tanstack/react-query';
import {BaseResponse, PATCH} from '../../../apis/client';

interface CheckNotificationRequest {
id: number;
type: string;
}

/**
* 푸시 확인 API
*/
export const checkNotification = async (req: CheckNotificationRequest) => {
const res = await PATCH<string>(`/api/notification/check`, req);
return res.data;
};

interface PatchFCMTokenProps {
onSuccess: (res: BaseResponse<string>) => void;
onError: (e: Error) => void;
}

export const usePatchNotification = ({
onSuccess,
onError,
}: PatchFCMTokenProps) => {
return useMutation({
mutationFn: (req: CheckNotificationRequest) => checkNotification(req),
onSuccess: onSuccess,
onError: onError,
});
};
39 changes: 39 additions & 0 deletions src/hooks/mutations/test/useTestNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {useMutation} from '@tanstack/react-query';
import {BaseResponse, POST} from '../../../apis/client';
import {checkPermission} from '../../../utils/pushUtils';

interface SendFCMPushRequest {
token: string;
isCategory: boolean;
}

/**
* 테스트 푸시 전송용
*/
export const sendFCMPush = async (req: SendFCMPushRequest) => {
const res = await POST<string>(`/api/fcm/test`, req);
return res.data;
};

interface PatchFCMTokenProps {
onSuccess: (res: BaseResponse<string>) => void;
onError: (e: Error) => void;
}

export const usePatchFCMToken = ({onSuccess, onError}: PatchFCMTokenProps) => {
return useMutation({
mutationFn: (req: SendFCMPushRequest) => sendFCMPush(req),
onSuccess: onSuccess,
onError: onError,
});
};

export const sendCategoryPush = async () => {
const token = await checkPermission();
await sendFCMPush({token: token, isCategory: true});
};

export const sendDetailPush = async () => {
const token = await checkPermission();
await sendFCMPush({token: token, isCategory: false});
};
18 changes: 17 additions & 1 deletion src/hooks/queries/QueryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,20 @@ const TOUR_KEYS = {
[...TOUR_KEYS.details(), {x: location.x, y: location.y}] as const, // ["tours", "detail", {x: "x", y: "y"}]
};

export {LOCATION_KEYS, CATEGORY_KEYS, WEATHER_KEYS, TOUR_KEYS};
const NOTIFICATION_KEYS = {
all: ['notifications'] as const,

lists: () => [...NOTIFICATION_KEYS.all, 'list'] as const, // ["notifications", "list"]
list: (filters: object) => [...NOTIFICATION_KEYS.lists(), {filters}] as const, // ["notifications", "list", "..."]

details: () => [...NOTIFICATION_KEYS.all, 'detail'] as const, // ["notifications", "detail"]
detail: (id: string) => [...NOTIFICATION_KEYS.details(), id] as const, // ["notifications", "detail", "id"]
};

export {
LOCATION_KEYS,
CATEGORY_KEYS,
WEATHER_KEYS,
TOUR_KEYS,
NOTIFICATION_KEYS,
};
3 changes: 3 additions & 0 deletions src/hooks/queries/category/useGetCategoryDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const getCategoryList = async ({
console.log(userCategoryId);
return {
items: result.userLocationList,
category: result.userCategory,
nextPage: result.totalPage > page ? page + 1 : undefined,
totalPage: result.totalPage,
totalElement: result.totalElement,
Expand All @@ -37,6 +38,7 @@ const useGetCategoryList = (requestParams: GetPostHistoryRequest) => {
getNextPageParam: lastPage => lastPage.nextPage,
select: data => ({
pages: data.pages.flatMap(page => page.items),
category: data.pages?.[0]?.category,
totalElement: data.pages?.[0]?.totalElement ?? 0,
}),
initialPageParam: requestParams.page,
Expand All @@ -54,6 +56,7 @@ const useGetCategoryList = (requestParams: GetPostHistoryRequest) => {
isFetching,
hasNextPage,
totalElement: data?.totalElement,
category: data?.category,
};
};

Expand Down
59 changes: 59 additions & 0 deletions src/hooks/queries/notification/useGetNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {useInfiniteQuery} from '@tanstack/react-query';
import {GET} from '../../../apis/client';
import {useCallback} from 'react';
import {NOTIFICATION_KEYS} from '../QueryKeys';

const getNotificationList = async (page: number) => {
const {
data: {result},
} = await GET<GetNotificationResponse>(`/api/notification?page=${page}`);

console.log('>', result);
return {
notificationList: result.notificationList,
nextPage: result.totalPage > page ? page + 1 : undefined,
totalPage: result.totalPage,
};
};

export interface NotificationDTO {
id: number;
title: string;
body: string;
type: string;
isChecked: boolean;
createdAt: string;
}

interface GetNotificationResponse {
totalPage: number;
notificationList: NotificationDTO[];
}

const useGetNotification = (page: number) => {
const {data, hasNextPage, fetchNextPage, isFetching} = useInfiniteQuery({
queryKey: NOTIFICATION_KEYS.list({page: page}),
queryFn: ({pageParam = 1}: {pageParam?: number}) =>
getNotificationList(pageParam),
getNextPageParam: lastPage => lastPage.nextPage,
select: data => ({
pages: data.pages.flatMap(page => page.notificationList),
}),
initialPageParam: page,
});

const loadMore = useCallback(() => {
if (hasNextPage) {
fetchNextPage();
}
}, [hasNextPage, fetchNextPage]);

return {
data: data?.pages,
loadMore,
isFetching,
hasNextPage,
};
};

export default useGetNotification;
37 changes: 32 additions & 5 deletions src/hooks/usePushNotification.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import {useEffect} from 'react';
import {displayNotification, messaging} from '../utils/pushUtils';
import {
displayNotification,
getDeepLinkUrl,
getDeepLinkUrlFromUrl,
linkToDeepLinkURL,
messaging,
parseNotificationData,
} from '../utils/pushUtils';

export default function usePushNotification() {
useEffect(() => {
Expand All @@ -8,10 +15,30 @@ export default function usePushNotification() {
displayNotification(remoteMessage);
});

messaging.onNotificationOpenedApp(remoteMessage => {
console.log('background state:', remoteMessage);

const parsedData = parseNotificationData(remoteMessage?.data?.data);
const deepLinkURL = getDeepLinkUrlFromUrl(parsedData.url);
if (deepLinkURL) {
console.log('url', deepLinkURL);
linkToDeepLinkURL(deepLinkURL);
}
});

messaging.getInitialNotification().then(remoteMessage => {
if (remoteMessage) {
console.log('quit state:', remoteMessage);

const parsedData = parseNotificationData(remoteMessage?.data?.data);
const deepLinkURL = getDeepLinkUrlFromUrl(parsedData.url);
if (deepLinkURL) {
console.log('url', deepLinkURL);
linkToDeepLinkURL(deepLinkURL);
}
}
});

return unsubscribe;
}, []);

messaging.setBackgroundMessageHandler(async remoteMessage => {
console.log('Message handled in the background!', remoteMessage);
});
}
Loading

0 comments on commit ced1e79

Please sign in to comment.