From 6bd341271318ceb9fdcdd322385f4d9ffd9fea9b Mon Sep 17 00:00:00 2001 From: sikkzz Date: Wed, 5 Feb 2025 23:52:05 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20app=20bridge=20provider,=20user=20a?= =?UTF-8?q?gent=20provider=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppBridgeMessage.types.ts | 40 ++++++++++++++ .../AppBridgeProvider/AppBridgeProvider.tsx | 46 ++++++++++++++++ .../convertToNativeMessage.ts | 43 +++++++++++++++ src/components/provider/UserAgentProvider.tsx | 55 +++++++++++++++++++ src/types/global.d.ts | 27 +++++++++ 5 files changed, 211 insertions(+) create mode 100644 src/components/provider/AppBridgeProvider/AppBridgeMessage.types.ts create mode 100644 src/components/provider/AppBridgeProvider/AppBridgeProvider.tsx create mode 100644 src/components/provider/AppBridgeProvider/convertToNativeMessage.ts create mode 100644 src/components/provider/UserAgentProvider.tsx diff --git a/src/components/provider/AppBridgeProvider/AppBridgeMessage.types.ts b/src/components/provider/AppBridgeProvider/AppBridgeMessage.types.ts new file mode 100644 index 0000000..425a3fe --- /dev/null +++ b/src/components/provider/AppBridgeProvider/AppBridgeMessage.types.ts @@ -0,0 +1,40 @@ +export enum AppBridgeMessageType { + OPEN_CAMERA = "openCamera", + OPEN_GALLERY = "openGallery", + SHARE = "share", + CREATE_REVIEW = "createReview", + COPY = "copy", +} + +export type AppBridgeMessage = + | OpenCameraMessage + | OpenGalleryMessage + | ShareMessage + | CreateReviewMessage + | CopyMessage; + +export interface OpenCameraMessage { + type: AppBridgeMessageType.OPEN_CAMERA; +} + +export interface OpenGalleryMessage { + type: AppBridgeMessageType.OPEN_GALLERY; +} + +export interface ShareMessage { + type: AppBridgeMessageType.SHARE; +} + +export interface CreateReviewMessage { + type: AppBridgeMessageType.CREATE_REVIEW; + payload: { + json: string; + }; +} + +export interface CopyMessage { + type: AppBridgeMessageType.COPY; + payload: { + json: string; + }; +} diff --git a/src/components/provider/AppBridgeProvider/AppBridgeProvider.tsx b/src/components/provider/AppBridgeProvider/AppBridgeProvider.tsx new file mode 100644 index 0000000..b109d02 --- /dev/null +++ b/src/components/provider/AppBridgeProvider/AppBridgeProvider.tsx @@ -0,0 +1,46 @@ +import type { ReactNode } from "react"; +import { createContext, useContext } from "react"; + +import type { AppBridgeMessage } from "@/components/provider/AppBridgeProvider/AppBridgeMessage.types"; +import { + convertToAndroidAppBridge, + convertToIOSAppBridge, +} from "@/components/provider/AppBridgeProvider/convertToNativeMessage"; +import { useUserAgent } from "@/components/provider/UserAgentProvider"; + +interface AppBridgeProviderProps { + children: ReactNode; +} + +interface AppBridge { + send: (message: AppBridgeMessage) => void; +} + +export const AppBridgeContext = createContext(null); + +export function AppBridgeProvider({ children }: AppBridgeProviderProps) { + const userAgent = useUserAgent(); + + const isIOS = userAgent.isIOS; + + const send = (message: AppBridgeMessage) => { + try { + if (isIOS) return convertToIOSAppBridge(message); + return convertToAndroidAppBridge(message); + } catch { + alert("App Bridge API called: " + message.type); + } + }; + + return {children}; +} + +export function useAppBridge() { + const appBridge = useContext(AppBridgeContext); + + if (appBridge == null) { + throw new Error("Wrap App Bridge Provider"); + } + + return appBridge; +} diff --git a/src/components/provider/AppBridgeProvider/convertToNativeMessage.ts b/src/components/provider/AppBridgeProvider/convertToNativeMessage.ts new file mode 100644 index 0000000..18d1b9a --- /dev/null +++ b/src/components/provider/AppBridgeProvider/convertToNativeMessage.ts @@ -0,0 +1,43 @@ +import { AppBridgeMessageType } from "@/components/provider/AppBridgeProvider/AppBridgeMessage.types"; +import type { AppBridgeMessage } from "@/components/provider/AppBridgeProvider/AppBridgeMessage.types"; + +const iosHandlers = { + [AppBridgeMessageType.OPEN_CAMERA]: () => window.webkit?.messageHandlers.openCamera.postMessage(), + [AppBridgeMessageType.OPEN_GALLERY]: () => + window.webkit?.messageHandlers.openGallery.postMessage(), + [AppBridgeMessageType.SHARE]: () => window.webkit?.messageHandlers.share.postMessage(), + [AppBridgeMessageType.CREATE_REVIEW]: (message: { payload: { json: string } }) => + window.webkit?.messageHandlers.createReview.postMessage(message.payload.json), + [AppBridgeMessageType.COPY]: (message: { payload: { json: string } }) => + window.webkit?.messageHandlers.copy.postMessage(message.payload.json), +}; + +const androidHandlers = { + [AppBridgeMessageType.OPEN_CAMERA]: () => window.AndroidBridge?.openCamera(), + [AppBridgeMessageType.OPEN_GALLERY]: () => window.AndroidBridge?.openGallery(), + [AppBridgeMessageType.SHARE]: () => window.AndroidBridge?.share(), + [AppBridgeMessageType.CREATE_REVIEW]: (message: { payload: { json: string } }) => + window.AndroidBridge?.createReview(message.payload.json), + [AppBridgeMessageType.COPY]: (message: { payload: { json: string } }) => + window.AndroidBridge?.copy(message.payload.json), +}; + +export function convertToIOSAppBridge(message: AppBridgeMessage) { + const handler = iosHandlers[message.type]; + if (handler) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handler(message as any); + } else { + console.warn("Unhandled message type:", message.type); + } +} + +export function convertToAndroidAppBridge(message: AppBridgeMessage) { + const handler = androidHandlers[message.type]; + if (handler) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handler(message as any); + } else { + console.warn("Unhandled message type:", message.type); + } +} diff --git a/src/components/provider/UserAgentProvider.tsx b/src/components/provider/UserAgentProvider.tsx new file mode 100644 index 0000000..1c4a8de --- /dev/null +++ b/src/components/provider/UserAgentProvider.tsx @@ -0,0 +1,55 @@ +"use client"; + +import type { ReactNode } from "react"; +import { createContext, useContext, useEffect, useState } from "react"; + +export interface UserAgent { + rawUA: string; + isIOS: boolean; + isAndroid: boolean; + isMobile: boolean; +} + +export const UserAgentContext = createContext(null); + +export function UserAgentProvider({ children }: { children: ReactNode }) { + const [userAgent, setUserAgent] = useState({ + isAndroid: false, + isIOS: true, + rawUA: "", + isMobile: true, + }); + + useEffect(() => { + const _userAgent = navigator.userAgent.toLowerCase(); + + const isMobile = _userAgent.indexOf("iphone") > -1 || _userAgent.indexOf("android") > -1; + + if (_userAgent.indexOf("android") > -1) { + setUserAgent({ + isIOS: false, + isAndroid: true, + rawUA: _userAgent, + isMobile, + }); + } else { + setUserAgent({ + isIOS: true, + isAndroid: false, + rawUA: _userAgent, + isMobile, + }); + } + }, []); + + return {children}; +} + +export function useUserAgent() { + const userAgent = useContext(UserAgentContext); + + if (userAgent == null) { + throw new Error("Wrap UserAgent Provider"); + } + return userAgent; +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index b78db39..c1d4d45 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -2,3 +2,30 @@ declare module "*.module.scss" { const classes: { [className: string]: string }; export default classes; } + +export {}; + +type MessageHandler = { + postMessage: (message?: T) => void; +}; + +declare global { + interface Window { + webkit?: { + messageHandlers: { + openCamera: MessageHandler; + openGallery: MessageHandler; + share: MessageHandler; + createReview: MessageHandler; + copy: MessageHandler; + }; + }; + AndroidBridge?: { + openCamera: () => void; + openGallery: () => void; + share: () => void; + createReview: (json: string) => void; + copy: (json: string) => void; + }; + } +} From c841c1e724a9424191b667356bcbf9c96e0dd6b3 Mon Sep 17 00:00:00 2001 From: sikkzz Date: Wed, 5 Feb 2025 23:58:48 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20home=20native=20handler=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Home/Home.tsx | 19 +++++++++++++------ .../convertToNativeMessage.ts | 2 ++ src/main.tsx | 10 ++++++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/Home/Home.tsx b/src/components/Home/Home.tsx index 229914d..a301980 100644 --- a/src/components/Home/Home.tsx +++ b/src/components/Home/Home.tsx @@ -1,12 +1,11 @@ import styles from "@/components/Home/Home.module.scss"; +import { AppBridgeMessageType } from "@/components/provider/AppBridgeProvider/AppBridgeMessage.types"; +import { useAppBridge } from "@/components/provider/AppBridgeProvider/AppBridgeProvider"; import IconButton from "@/components/ui/IconButton/IconButton"; import Text from "@/components/ui/Text/Text"; -import { useRoute } from "@/hooks/common/useRoute"; - const Home = () => { - // 이후 네이티브 라우팅으로 변경 - const { navigateToReceiptEdit, navigateToRecognitionFail } = useRoute(); + const { send } = useAppBridge(); return (
@@ -22,8 +21,16 @@ const Home = () => { mainLogo
- - + send({ type: AppBridgeMessageType.OPEN_GALLERY })} + /> + send({ type: AppBridgeMessageType.OPEN_CAMERA })} + />
); diff --git a/src/components/provider/AppBridgeProvider/convertToNativeMessage.ts b/src/components/provider/AppBridgeProvider/convertToNativeMessage.ts index 18d1b9a..8434239 100644 --- a/src/components/provider/AppBridgeProvider/convertToNativeMessage.ts +++ b/src/components/provider/AppBridgeProvider/convertToNativeMessage.ts @@ -24,6 +24,7 @@ const androidHandlers = { export function convertToIOSAppBridge(message: AppBridgeMessage) { const handler = iosHandlers[message.type]; + if (handler) { // eslint-disable-next-line @typescript-eslint/no-explicit-any handler(message as any); @@ -34,6 +35,7 @@ export function convertToIOSAppBridge(message: AppBridgeMessage) { export function convertToAndroidAppBridge(message: AppBridgeMessage) { const handler = androidHandlers[message.type]; + if (handler) { // eslint-disable-next-line @typescript-eslint/no-explicit-any handler(message as any); diff --git a/src/main.tsx b/src/main.tsx index ae5863d..3a49d45 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,7 +5,9 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import AppRouter from "@/router/AppRouter"; +import { AppBridgeProvider } from "@/components/provider/AppBridgeProvider/AppBridgeProvider"; import ReactQueryClientProvider from "@/components/provider/ReactQueryClientProvider"; +import { UserAgentProvider } from "@/components/provider/UserAgentProvider"; import "@/styles/reset.scss"; import "@/styles/global.scss"; @@ -13,8 +15,12 @@ import "@/styles/global.scss"; ReactDom.createRoot(document.getElementById("root")!).render( - - + + + + + + , );