Skip to content

Commit

Permalink
[Feat] 웹뷰 자동 로그인 구현 (#195)
Browse files Browse the repository at this point in the history
* Chore: turbo.json globalEnv 추가

* Feat: WebView 환경 감지하여 API URL 분기 처리

* Feat: postMessage 처리 추가하여 WebView 로그인/로그아웃 상태 동기화

* Feat: 개발용 WebView 설정 및 message handler 연결

* Feat: Webview 환경에서 페이지 전환 로직 분기 처리

* Chore: 줄바꿈 (prettier)

* Chore: @react-native-async-storage/async-storage install

* Chore: 의존성 업데이트트

* Feat: window.d.ts 추가

* Chore: 모바일 앱 개발을 위한 환경 변수 업데이트

* Refactor: messageHandler를 webviewLoginBridge로 이름 변경

* Feat: 웹뷰에서 자동로그인을 위한 토큰 전송 구현

* Feat: 웹뷰 자동로그인을 위한 웹 측 리스너 구현

* Feat: 웹에서 앱으로 로그인/로그아웃 메시지 전송 구현

* Feat: 웹뷰 환경에서 자동로그인을 위한 토큰 검증 로직 추가

* Chore: prettier 적용용

* Chore: 의존성 업데이트

* Refactor: window.d.ts -> global.d.ts로 변경

* Refactor: 로그인 API 로직 개선 및 토큰 검증 로직직 추가

* Refactor: useIsReactNativeWebview-> useWebview로 훅 이름 변경

* Chore: globalEnv 수정

* Chore: 의존성 업데이트

* Chore: global.d.ts platform 제거거

* Refactor: 웹뷰 이벤트 핸들러 로직 분리

* Refactor: 웹뷰 메시지 처리 로직 개선

* Refactor: 웹뷰 메세지 타입 상수화

* Remove: useWebview.ts 삭제

* Chore: globalEnv 설정 추가

* Chore: 파일명 대소문자 변경

* Chore: React import 구문 추가

* Refactor: getBaseUrl -> getNativeApiUrl로 변경

* Refactor: parseMessageEvent -> parseMessage로 변경

* Fix: token 유효성 검사 로직 제거

* Refactor: WebView API URL 체크 로직을 getWebApiUrl 유틸 함수로 분리

* Chore: 불필요한 className 제거

* Style: MembersSettingModal의 gap 간격 제거

* Refactor: WebView 메시지 핸들러에 accessToken 체크 로직 추가 및 불필요한 코드 제거

* Chore: AuthGuard에서 웹뷰 리다이렉트 로직 제거

* Feat: WebView 메시지에 따른 분기처리

* Chore: 의존성 업데이트트

* Chore: react-device-detect 라이브러리 제거

* Refactor: JSON.stringify를 stringifyJson 유틸함수로 대체

* Refactor: WebView 감지 로직 개선

* Refactor: react-device-detect 의존성 제거 후 내부 유틸함수로 대체

* Refactor: WebView 이벤트 핸들링 구조 개선

* Refactor: 프로젝트 전반적인 네이밍 Native -> WebView로 통일

* Refactor: AsyncStorage 핸들링 구조 개선

* Refactor: WebView 메시지 브릿지 구조 개선

* Chore: detectWebView.ts 삭제

* Refactor: 메시지 핸들러 파라미터 수정 및 불필요한 WebView 속성 제거

* Chore: import 문 줄바꿈

* Refactor: WebView 디바이스 감지 로직 간소화

* Refactor: WebView 디바이스 감지 로직을 별도 유틸리티로 분리하여 hook 간소화

* Refactor: WebView 메시지 브릿지를 이벤트 리스너로 변경

* Chore: sendMessageToWebView import 경로를 절대 경로로 변경

* Refactor: switch 문의 case 블록 스코프 처리

* Chore: import React 문 제거

* Chore: Merge branch 'develop' of https://github.com/codeit-internship-group-b/codeit-resources into 194-feat-rn-webview-브릿지-통신-구현

* Chore: ReactNativeWebView 객체 접근 방식을 옵셔널 체이닝으로 변경

* Fix: 불필요한 return 제거
  • Loading branch information
miraclee1226 authored Jan 23, 2025
1 parent a3e078b commit 1b81e4b
Show file tree
Hide file tree
Showing 39 changed files with 2,193 additions and 1,646 deletions.
4 changes: 2 additions & 2 deletions apps/mobile/app/(route)/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import WebView, { WebViewMessageEvent } from "react-native-webview";

import { ROUTES } from "@/constants/routes";
import { useHandleNavigationActions } from "@/hooks/useHandleNavigationActions";
import { getBaseUrl } from "@/utils/getBaseUrl";
import { getWebViewApiUrl } from "@/utils/getWebViewApiUrl";

export default function DashboardScreen() {
const baseUrl = getBaseUrl();
const baseUrl = getWebViewApiUrl();
const handleNavigationActions = useHandleNavigationActions();

const requestOnMessage = (e: WebViewMessageEvent) => {
Expand Down
28 changes: 22 additions & 6 deletions apps/mobile/app/(route)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import { useRef } from "react";
import { WebView, WebViewMessageEvent } from "react-native-webview";

import { useHandleNavigationActions } from "@/hooks/useHandleNavigationActions";
import { getBaseUrl } from "@/utils/getBaseUrl";
import { handleAuthStorage } from "@/store/authStorage";
import { getWebViewApiUrl } from "@/utils/getWebViewApiUrl";
import { parseMessage } from "@/utils/parseMessage";
import { webViewLoadHandler } from "@/utils/webViewLoadHandler";

export default function HomeScreen() {
// TODO : login상태에 따른 분기 처리 설정
const webviewRef = useRef<WebView>(null);

const baseUrl = getBaseUrl();
const baseUrl = getWebViewApiUrl();
const handleNavigationActions = useHandleNavigationActions();
const { event, handler } = webViewLoadHandler(webviewRef);

const requestOnMessage = (e: WebViewMessageEvent) => {
handleNavigationActions(e);
const handleMessage = (e: WebViewMessageEvent) => {
const { type, data } = parseMessage(e);

handleNavigationActions({ type, data });
handleAuthStorage({ type, data });
};

return <WebView className="flex-1" source={{ uri: `${baseUrl}` }} onMessage={requestOnMessage} />;
return (
<WebView
ref={webviewRef}
source={{ uri: `${baseUrl}` }}
onMessage={handleMessage}
{...{ [event]: handler }}
className="flex-1"
/>
);
}
4 changes: 2 additions & 2 deletions apps/mobile/app/(route)/meetings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import WebView, { WebViewMessageEvent } from "react-native-webview";

import { ROUTES } from "@/constants/routes";
import { useHandleNavigationActions } from "@/hooks/useHandleNavigationActions";
import { getBaseUrl } from "@/utils/getBaseUrl";
import { getWebViewApiUrl } from "@/utils/getWebViewApiUrl";

export default function MeetingsScreen() {
const baseUrl = getBaseUrl();
const baseUrl = getWebViewApiUrl();
const handleNavigationActions = useHandleNavigationActions();

const requestOnMessage = (e: WebViewMessageEvent) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/app/(route)/seats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import WebView, { WebViewMessageEvent } from "react-native-webview";

import { ROUTES } from "@/constants/routes";
import { useHandleNavigationActions } from "@/hooks/useHandleNavigationActions";
import { getBaseUrl } from "@/utils/getBaseUrl";
import { getWebViewApiUrl } from "@/utils/getWebViewApiUrl";

export default function SeatsScreen() {
const baseUrl = getBaseUrl();
const baseUrl = getWebViewApiUrl();
const handleNavigationActions = useHandleNavigationActions();

const requestOnMessage = (e: WebViewMessageEvent) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/app/(route)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import WebView, { WebViewMessageEvent } from "react-native-webview";

import { ROUTES } from "@/constants/routes";
import { useHandleNavigationActions } from "@/hooks/useHandleNavigationActions";
import { getBaseUrl } from "@/utils/getBaseUrl";
import { getWebViewApiUrl } from "@/utils/getWebViewApiUrl";

export default function SettingsScreen() {
const baseUrl = getBaseUrl();
const baseUrl = getWebViewApiUrl();
const handleNavigationActions = useHandleNavigationActions();

const requestOnMessage = (e: WebViewMessageEvent) => {
Expand Down
File renamed without changes.
11 changes: 5 additions & 6 deletions apps/mobile/hooks/useHandleNavigationActions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { StackActions } from "@react-navigation/native";
import { WEBVIEW_MESSAGE_TYPES } from "@repo/constants";
import { Message } from "@ui/src/types/WebViewMessageTypes";
import { useNavigation, usePathname } from "expo-router";
import { WebViewMessageEvent } from "react-native-webview";

import { DIR_NAME } from "@/constants/routes";
import { parseMessageEvent } from "@/utils/parseMessageEvent";

export const useHandleNavigationActions = () => {
const pathname = usePathname();
const navigation = useNavigation();

const handleNavigationActions = (e: WebViewMessageEvent) => {
const parsedMessage = parseMessageEvent(e);
if (!parsedMessage || parsedMessage.type !== WEBVIEW_MESSAGE_TYPES.ROUTER_EVENT) return;
const handleNavigationActions = ({ type, data }: Message<string>) => {
if (type !== WEBVIEW_MESSAGE_TYPES.ROUTER_EVENT) return;

const path = data;

const { data: path } = parsedMessage;
if (pathname === path) return;

const action = path === "back" ? StackActions.pop(1) : StackActions.push(`${DIR_NAME}${path}`);
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "^2.1.0",
"@react-navigation/native": "^6.1.18",
"@repo/ui": "workspace:*",
"babel-preset-expo": "^12.0.5",
Expand Down
38 changes: 38 additions & 0 deletions apps/mobile/store/authStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { WEBVIEW_MESSAGE_TYPES } from "@repo/constants";
import { IUser } from "@repo/types/userType";
import { LoginData, Message } from "@repo/ui/src/types/WebViewMessageTypes";
import { stringifyJson } from "@repo/ui/src/utils/stringifyJson";

export const getAuthData = async () => {
const [accessToken, userStr] = await Promise.all([AsyncStorage.getItem("accessToken"), AsyncStorage.getItem("user")]);

return {
accessToken,
user: userStr ? JSON.parse(userStr) : null,
};
};

export const setAuthData = async ({ accessToken, user }: LoginData) => {
await Promise.all([
AsyncStorage.setItem("accessToken", accessToken),
AsyncStorage.setItem("user", stringifyJson(user)),
]);
};

export const clearAuthData = async () => {
await Promise.all([AsyncStorage.removeItem("accessToken"), AsyncStorage.removeItem("user")]);
};

export const handleAuthStorage = async ({ type, data }: Message<LoginData>) => {
switch (type) {
case WEBVIEW_MESSAGE_TYPES.SIGN_IN_SUCCESS: {
const { user, accessToken } = data;
await setAuthData({ accessToken, user });
break;
}
case WEBVIEW_MESSAGE_TYPES.SIGN_OUT_SUCCESS:
await clearAuthData();
break;
}
};
5 changes: 3 additions & 2 deletions apps/mobile/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@
"@ui/*": ["../../packages/ui/*"],
"@/public/*": ["./public/*"],
"@repo/types/*": ["../../packages/types/src/*"]
}
},
"jsx": "preserve"
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"nativewind-env.d.ts",
"**/__tests__/**/*.ts",
"**/__tests__/**/*.tsx",
"**/*.d.ts",
".expo/types/**/*.ts",
"expo-env.d.ts"
],
"exclude": ["node_modules"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Platform } from "react-native";

export const getBaseUrl = () => {
export const getWebViewApiUrl = () => {
if (Platform.OS === "ios") return process.env.EXPO_PUBLIC_API_URL_IOS;
if (Platform.OS === "android") return process.env.EXPO_PUBLIC_API_URL_ANDROID;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WebViewMessageEvent } from "react-native-webview";

export const parseMessageEvent = (e: WebViewMessageEvent) => {
export const parseMessage = (e: WebViewMessageEvent) => {
return JSON.parse(e.nativeEvent.data);
};
14 changes: 14 additions & 0 deletions apps/mobile/utils/sendMessageToWeb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { stringifyJson } from "@repo/ui/src/utils/stringifyJson";
import { Message } from "@ui/src/types/WebViewMessageTypes";
import { RefObject } from "react";
import WebView from "react-native-webview";

export interface SendMessage<T> extends Message<T> {
webViewRef: RefObject<WebView<object>>;
}

export const sendMessageToWeb = <T>({ webViewRef, type, data }: SendMessage<T>) => {
if (webViewRef.current) {
webViewRef.current.postMessage(stringifyJson({ type, data }));
}
};
44 changes: 44 additions & 0 deletions apps/mobile/utils/webViewLoadHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { WEBVIEW_MESSAGE_TYPES } from "@repo/constants";
import { RefObject } from "react";
import { Platform } from "react-native";
import WebView from "react-native-webview";
import { WebViewProgressEvent } from "react-native-webview/lib/WebViewTypes";

import { getAuthData } from "@/store/authStorage";

import { sendMessageToWeb } from "./sendMessageToWeb";

interface WebViewLoadHandler {
event: "onLoadProgress" | "onLoad";
handler: (e: WebViewProgressEvent) => void;
}

const sendAuthData = async (webViewRef: RefObject<WebView<object>>) => {
const authData = await getAuthData();

sendMessageToWeb({
webViewRef,
type: WEBVIEW_MESSAGE_TYPES.AUTO_LOGIN,
data: authData,
});
};

export const webViewLoadHandler = (webViewRef: RefObject<WebView<object>>): WebViewLoadHandler => {
if (Platform.OS === "ios") {
return {
event: "onLoadProgress",
handler: (e: WebViewProgressEvent) => {
const progress = e.nativeEvent.progress;

if (progress === 1) {
sendAuthData(webViewRef);
}
},
};
}

return {
event: "onLoad",
handler: () => sendAuthData(webViewRef),
};
};
13 changes: 4 additions & 9 deletions apps/web/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@ import { API_ENDPOINTS } from "@repo/constants";
import { type FieldValues } from "react-hook-form";
import axios from "axios";
import { type SignInResponseType } from "@repo/types/src/responseType";
import { getWebApiUrl } from "@/api/getWebApiUrl";

const baseUrl = getWebApiUrl();

export const postSignIn = async (payload: FieldValues): Promise<SignInResponseType> => {
const { data } = await axios.post<SignInResponseType>(
`${process.env.NEXT_PUBLIC_API_URL}${API_ENDPOINTS.AUTH.SIGN_IN}`,
payload,
{
headers: {
"Content-Type": "application/json",
},
},
);
const { data } = await axios.post<SignInResponseType>(`${baseUrl}${API_ENDPOINTS.AUTH.SIGN_IN}`, payload);

return data;
};
10 changes: 10 additions & 0 deletions apps/web/api/getWebApiUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { detectWebView } from "@/lib/bridge/detectWebView";

export const getWebApiUrl = (): string | undefined => {
const { isAndroid, isIOS } = detectWebView();

if (isAndroid) return process.env.NEXT_PUBLIC_ANDROID_API_URL;
if (isIOS) return process.env.NEXT_PUBLIC_IOS_API_URL;

return process.env.NEXT_PUBLIC_API_URL;
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function PanelHeader({ selectedMember, onClose }: PanelHeaderProp

return (
<>
<button onClick={onClose} type="button" className="flex flex-row">
<button onClick={onClose} type="button">
<DoubleChevron />
</button>
<div className={selectedMember ? "flex justify-between" : ""}>
Expand Down
11 changes: 11 additions & 0 deletions apps/web/app/_components/AuthGuard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ import { useEffect, useState } from "react";
import { PAGE_NAME } from "@ui/src/utils/constants/pageNames";
import { useRouter } from "next/navigation";
import { useAuthStore } from "@/app/store/useAuthStore";
import { createWebViewEventListener } from "../../lib/bridge/createWebViewEventListener";
import { parseWebViewAuthMessage } from "../../lib/bridge/parseWebViewAuthMessage";
import { useDetectWebView } from "../_hooks/useDetectWebView";
import SignInForm from "./SignInForm";

export default function AuthGuard(): JSX.Element | null {
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const { isLoggedIn } = useAuthStore();
const { isIOSWebView, isAndroidWebView } = useDetectWebView();

const webViewEventListener = createWebViewEventListener({ isIOSWebView, isAndroidWebView });

useEffect(() => {
webViewEventListener(parseWebViewAuthMessage);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
if (isLoggedIn) {
Expand Down
7 changes: 3 additions & 4 deletions apps/web/app/_hooks/useAppRouter.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { useRouter } from "next/navigation";
import { WEBVIEW_MESSAGE_TYPES } from "@repo/constants";
import { sendMessageToNative } from "../utils/sendMessageToNative";
import { sendMessageToWebView } from "@/lib/bridge/sendMessageToWebView";
import { useDetectWebView } from "./useDetectWebView";

interface UseAppRouterResult {
push: (url: string) => void;
}

export const useAppRouter = (): UseAppRouterResult => {
const { isWebView } = useDetectWebView();
const router = useRouter();
const { isWebView } = useDetectWebView();

const push = (url: string): void => {
// web view 실행
if (isWebView) {
sendMessageToNative({
sendMessageToWebView({
type: WEBVIEW_MESSAGE_TYPES.ROUTER_EVENT,
data: url,
});
return;
}

// web 실행
Expand Down
Loading

0 comments on commit 1b81e4b

Please sign in to comment.