Files
nice-app-web/src/utils/appBridge.ts
2025-09-17 10:04:58 +09:00

275 lines
8.2 KiB
TypeScript

import {
AppBridgeMessage,
AppBridgeResponse,
BridgeMessageType,
DeviceInfo,
ShareContent
} from '@/types';
import { LoginCredentials, UserInfo } from '@/types/auth';
class AppBridge {
private static instance: AppBridge;
private messageId = 0;
private pendingCallbacks = new Map<string, (response: AppBridgeResponse) => void>();
private responseListeners: Set<(response: AppBridgeResponse) => void> = new Set();
private constructor() {
this.setupMessageListener();
}
// 외부에서 네이티브 응답을 구독할 수 있도록 리스너 등록/해제 메서드 추가
public addResponseListener(listener: (response: AppBridgeResponse) => void) {
this.responseListeners.add(listener);
}
public removeResponseListener(listener: (response: AppBridgeResponse) => void) {
this.responseListeners.delete(listener);
}
static getInstance(): AppBridge {
if (!AppBridge.instance) {
AppBridge.instance = new AppBridge();
}
return AppBridge.instance;
}
private setupMessageListener(): void {
window.addEventListener('message', (event) => {
try {
const response: AppBridgeResponse & { callbackId?: string } = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
console.log('response', response);
if (response.callbackId && this.pendingCallbacks.has(response.callbackId)) {
const callback = this.pendingCallbacks.get(response.callbackId);
if (callback) {
callback(response);
this.pendingCallbacks.delete(response.callbackId);
}
}
// 등록된 리스너들에게 모든 응답 전달
this.responseListeners.forEach(listener => listener(response));
} catch (error) {
console.error('Failed to parse bridge message:', error);
}
});
}
private generateMessageId(): string {
return `bridge_${++this.messageId}_${Date.now()}`;
}
private sendMessage<T>(type: BridgeMessageType, data?: unknown): Promise<T> {
return new Promise((resolve, reject) => {
const callbackId = this.generateMessageId();
const message: AppBridgeMessage & { callbackId: string } = {
type,
data,
callbackId
};
this.pendingCallbacks.set(callbackId, (response: AppBridgeResponse) => {
if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.error || 'Bridge call failed'));
}
});
// Android WebView 인터페이스
if (window.AndroidBridge && window.AndroidBridge.postMessage) {
console.log('Android postMessage', message);
window.AndroidBridge.postMessage(JSON.stringify(message));
return;
}
// iOS WKWebView 인터페이스
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {
console.log('iOS postMessage', message);
window.webkit.messageHandlers.bridge.postMessage(message);
return;
}
// 네이티브 환경이 아닌 경우 에러 발생
setTimeout(() => {
this.pendingCallbacks.delete(callbackId);
reject(new Error('Native bridge not available'));
}, 100);
});
}
// 앱 정보 관련
async getAppInfo(): Promise<{ version: string; buildNumber: string }> {
return this.sendMessage(BridgeMessageType.GET_APP_INFO);
}
async getDeviceInfo(): Promise<DeviceInfo> {
return this.sendMessage(BridgeMessageType.GET_DEVICE_INFO);
}
// 네비게이션 관련
async navigateBack(): Promise<void> {
return this.sendMessage(BridgeMessageType.NAVIGATE_BACK);
}
async navigateTo(path: string): Promise<void> {
return this.sendMessage(BridgeMessageType.NAVIGATE_TO, { path });
}
async navigateToLogin(): Promise<void> {
return this.sendMessage(BridgeMessageType.NAVIGATE_TO_LOGIN);
}
async closeWebView(): Promise<void> {
return this.sendMessage(BridgeMessageType.CLOSE_WEBVIEW);
}
// 알림 관련
async showToast(message: string, duration: number = 3000): Promise<void> {
return this.sendMessage(BridgeMessageType.SHOW_TOAST, { message, duration });
}
async showAlert(title: string, message: string): Promise<void> {
return this.sendMessage(BridgeMessageType.SHOW_ALERT, { title, message });
}
async showConfirm(title: string, message: string): Promise<boolean> {
return this.sendMessage(BridgeMessageType.SHOW_CONFIRM, { title, message });
}
// 저장소 관련
async setStorage(key: string, value: unknown): Promise<void> {
return this.sendMessage(BridgeMessageType.SET_STORAGE, { key, value: JSON.stringify(value) });
}
async getStorage<T>(key: string): Promise<T | null> {
try {
const result = await this.sendMessage<string>(BridgeMessageType.GET_STORAGE, { key });
return result ? JSON.parse(result) : null;
} catch {
return null;
}
}
async removeStorage(key: string): Promise<void> {
return this.sendMessage(BridgeMessageType.REMOVE_STORAGE, { key });
}
async requestToken(): Promise<any> {
return this.sendMessage(BridgeMessageType.REQUEST_TOKEN);
}
// 공유 관련
async shareContent(content: ShareContent): Promise<void> {
return this.sendMessage(BridgeMessageType.SHARE_CONTENT, content);
}
// 로그인 요청
async login(credentials?: LoginCredentials): Promise<UserInfo> {
return this.sendMessage(BridgeMessageType.LOGIN, credentials);
}
// 로그아웃 요청
async logout(): Promise<void> {
return this.sendMessage(BridgeMessageType.LOGOUT);
}
// 간편 인증 등록 요청
async registerBiometric(): Promise<void> {
return this.sendMessage(BridgeMessageType.REGISTER_BIOMETRIC);
}
// 간편 인증 등록 팝업 열기
async openBiometricRegistrationPopup(): Promise<boolean> {
return this.sendMessage(BridgeMessageType.OPEN_BIOMETRIC_REGISTRATION_POPUP);
}
// 간편 인증 등록 팝업 닫기
async closeBiometricRegistrationPopup(): Promise<void> {
return this.sendMessage(BridgeMessageType.CLOSE_BIOMETRIC_REGISTRATION_POPUP);
}
// 언어 설정
async setLanguage(language: string): Promise<void> {
return this.sendMessage(BridgeMessageType.SET_LANGUAGE, { language });
}
async getLanguage(): Promise<string> {
return this.sendMessage(BridgeMessageType.GET_LANGUAGE);
}
// 메시지 카운트 업데이트
async updateMessageCount(count: number): Promise<void> {
return this.sendMessage(BridgeMessageType.UPDATE_MESSAGE_COUNT, { count });
}
// 네이티브 환경 체크
isNativeEnvironment(): boolean {
return !!(
(window.AndroidBridge && window.AndroidBridge.postMessage) ||
(window.Android && window.Android.processMessage) ||
(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge)
);
}
isAndroid(): boolean {
return !!(
(window.AndroidBridge && window.AndroidBridge.postMessage) ||
(window.Android && window.Android.processMessage)
);
}
isIOS(): boolean {
return !!(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge);
}
// 에러 핸들링을 위한 래퍼 메소드들
async safeCall<T>(
bridgeMethod: () => Promise<T>,
fallback?: T,
onError?: (error: Error) => void
): Promise<T | undefined> {
try {
return await bridgeMethod();
} catch (error) {
console.error('Bridge call failed:', error);
if (onError) {
onError(error as Error);
}
return fallback;
}
}
// 타임아웃을 가진 브리지 호출
async callWithTimeout<T>(
bridgeMethod: () => Promise<T>,
timeout: number = 5000
): Promise<T> {
return Promise.race([
bridgeMethod(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Bridge call timeout')), timeout)
)
]);
}
}
// 글로벌 타입 선언 확장
declare global {
interface Window {
AndroidBridge?: {
postMessage: (message: string) => void;
};
Android?: {
processMessage: (message: string) => void;
};
webkit?: {
messageHandlers?: {
bridge?: {
postMessage: (message: unknown) => void;
};
};
};
}
}
export const appBridge = AppBridge.getInstance();
export default appBridge;