import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; import { Event } from './Events/Event'; import { UserProperties } from './UserProperties'; import { parseBluxNotification, type NotificationClickedHandler, } from './notifications/BluxNotification'; import type { NotificationForegroundWillDisplayEvent, NotificationForegroundWillDisplayHandler, } from './notifications/NotificationForegroundWillDisplayEvent'; import type { NotificationUrlOpenOptions } from './notifications/NotificationUrlOpenOptions'; import type { InAppUrlOpenOptions } from './notifications/InAppUrlOpenOptions'; import { parseBluxInApp, type InAppClickedHandler, } from './notifications/BluxInApp'; const LINKING_ERROR = `The package '@blux.ai/react-native' doesn't seem to be linked. Make sure: \n\n` + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n'; const BluxModule = NativeModules.Blux ? NativeModules.Blux : new Proxy( {}, { get() { throw new Error(LINKING_ERROR); }, } ); export type LogLevel = 'error' | 'verbose'; export type InitializeParams = { bluxApplicationId: string; bluxAPIKey: string; requestPermissionOnLaunch: boolean; customDeviceId?: string; }; export type WebViewMessage = { nativeEvent: { data: string; }; }; /** * Custom HTML 인앱 메시지에서 `BluxBridge.triggerAction()` 호출 시 전달되는 이벤트 */ export type InAppCustomActionEvent = { /** 액션 식별자 (예: "share", "navigate") */ actionId: string; /** 액션과 함께 전달된 데이터 */ data: Record; }; /** * Custom HTML 인앱 메시지에서 `BluxBridge.triggerAction()` 호출 시 실행되는 핸들러 타입 */ export type InAppCustomActionHandler = (event: InAppCustomActionEvent) => void; const NOTIFICATION_CLICKED_EVENT_KEY = 'notification-clicked'; const NOTIFICATION_FOREGROUND_WILL_DISPLAY_EVENT_KEY = 'notification-foreground-will-display'; const INAPP_CLICKED_EVENT_KEY = 'inapp-clicked'; const INAPP_CUSTOM_ACTION_EVENT_KEY = 'inapp-custom-action'; class BluxClient { static eventEmitter = new NativeEventEmitter(BluxModule); private static inAppCustomActionHandlers: InAppCustomActionHandler[] = []; static async initialize(params: InitializeParams): Promise { await BluxModule.initialize( params.bluxApplicationId, params.bluxAPIKey, params.requestPermissionOnLaunch, params.customDeviceId ?? null ); } static async setLogLevel(level: LogLevel): Promise { await BluxModule.setLogLevel(level); } static async signIn({ userId }: { userId: string }): Promise { await BluxModule.signIn(userId); } static async signOut(): Promise { await BluxModule.signOut(); } static async sendEvent(event: Event | Event[]): Promise { await BluxModule.sendRequest( (Array.isArray(event) ? event : [event]).map((r) => r.request) ); } static async sendEventData(events: object[]): Promise { await BluxModule.sendRequest(events); } static async setUserProperties({ userProperties, }: { userProperties: UserProperties; }): Promise { await BluxModule.setUserProperties(userProperties.toJson()); } static async setCustomUserProperties({ customUserProperties, }: { customUserProperties: Record; }): Promise { await BluxModule.setCustomUserProperties(customUserProperties); } /** * 푸시 알림 URL을 어떻게 열지 호스트 앱이 제어합니다. * * - `internalWebView` (기본값): SDK 내장 WebView로 연다 * - `externalBrowser`: 외부 브라우저로 연다 * - `none`: SDK가 자동으로 열지 않는다 ({@link setNotificationClickedHandler}에서 직접 처리) * * @example * ```typescript * await BluxClient.setNotificationUrlOpenOptions({ httpUrlOpenTarget: 'externalBrowser' }); * ``` */ static async setNotificationUrlOpenOptions( options: NotificationUrlOpenOptions ): Promise { await BluxModule.setNotificationUrlOpenOptions({ httpUrlOpenTarget: options.httpUrlOpenTarget ?? 'internalWebView', }); } /** * 앱이 포그라운드 상태일 때 푸시 알림이 표시되기 직전에 실행될 핸들러를 등록합니다. * * 핸들러 안에서 `event.display()`를 호출해야 시스템 알림이 실제로 표시됩니다. * 호출하지 않으면 시스템 알림은 표시되지 않으니, 인앱 UI로만 처리할 때 유용합니다. * * @example * ```typescript * BluxClient.setNotificationForegroundWillDisplayHandler(async (event) => { * await event.display(); * }); * ``` */ static async setNotificationForegroundWillDisplayHandler( handler: NotificationForegroundWillDisplayHandler ): Promise { this.eventEmitter.removeAllListeners( NOTIFICATION_FOREGROUND_WILL_DISPLAY_EVENT_KEY ); this.eventEmitter.addListener( NOTIFICATION_FOREGROUND_WILL_DISPLAY_EVENT_KEY, (raw) => { try { const notification = parseBluxNotification(raw?.notification); const event: NotificationForegroundWillDisplayEvent = { notification, display: async () => { await BluxModule.displayNotification({ id: notification.id, title: notification.title ?? null, body: notification.body, url: notification.url ?? null, imageUrl: notification.imageUrl ?? null, data: notification.data ?? null, }); }, }; handler(event); } catch (e) { console.error('NotificationForegroundWillDisplayHandler error:', e); } } ); await BluxModule.startNotificationForegroundWillDisplayHandler?.(); } /** * 푸시 알림 클릭 시 실행될 핸들러를 등록합니다. * * 등록 이전에 클릭된 알림이 있으면 등록 직후 즉시 핸들러로 전달됩니다. * * @example * ```typescript * BluxClient.setNotificationClickedHandler((notification) => { * if (notification.url) myRouter.go(notification.url); * }); * ``` */ static async setNotificationClickedHandler( handler: NotificationClickedHandler ): Promise { this.eventEmitter.removeAllListeners(NOTIFICATION_CLICKED_EVENT_KEY); this.eventEmitter.addListener(NOTIFICATION_CLICKED_EVENT_KEY, (raw) => { try { handler(parseBluxNotification(raw)); } catch (e) { console.error('NotificationClickedHandler error:', e); } }); await BluxModule.startNotificationClickedHandler?.(); } /** * 인앱 메시지 URL을 어떻게 열지 호스트 앱이 제어합니다. * * - `internalWebView` (기본값): SDK 내장 WebView로 연다 * - `externalBrowser`: 외부 브라우저로 연다 * - `none`: SDK가 자동으로 열지 않는다 * * @example * ```typescript * await BluxClient.setInAppUrlOpenOptions({ httpUrlOpenTarget: 'externalBrowser' }); * ``` */ static async setInAppUrlOpenOptions( options: InAppUrlOpenOptions ): Promise { await BluxModule.setInAppUrlOpenOptions({ httpUrlOpenTarget: options.httpUrlOpenTarget ?? 'internalWebView', }); } /** * 인앱 메시지의 http/https 링크 클릭을 호스트 앱이 직접 처리하도록 콜백을 등록합니다. * * 등록되어 있으면 SDK는 클릭된 URL을 자동으로 열지 않고 콜백만 호출합니다. * (클릭 트래킹과 인앱 dismiss는 정상 수행) * 등록되어 있지 않으면 {@link setInAppUrlOpenOptions} 정책이 적용됩니다. * * Custom HTML 인앱의 `BluxBridge.triggerAction()` 처리에는 영향을 주지 않습니다. * Custom HTML에서 호스트 라우팅을 받고 싶다면 {@link addInAppCustomActionHandler}를 사용하세요. * * @example * ```typescript * BluxClient.setInAppClickedHandler((inApp) => { * if (inApp.url) myRouter.go(inApp.url); * }); * ``` */ static async setInAppClickedHandler( handler: InAppClickedHandler ): Promise { this.eventEmitter.removeAllListeners(INAPP_CLICKED_EVENT_KEY); this.eventEmitter.addListener(INAPP_CLICKED_EVENT_KEY, (raw) => { try { handler(parseBluxInApp(raw)); } catch (e) { console.error('InAppClickedHandler error:', e); } }); await BluxModule.startInAppClickedHandler?.(); } /** * Custom HTML 인앱 메시지에서 `BluxBridge.triggerAction()` 호출 시 실행될 핸들러를 등록합니다. * * 여러 핸들러를 등록할 수 있으며 등록 순서대로 실행됩니다. * 반환된 unsubscribe 함수를 호출하면 등록한 핸들러만 제거됩니다. * * @returns 등록한 핸들러를 제거하는 unsubscribe 함수 * * @example * ```typescript * const unsubscribe = BluxClient.addInAppCustomActionHandler((event) => { * if (event.actionId === 'share') { * // 공유 로직 * } else if (event.actionId === 'navigate') { * const { screen, id } = event.data; * } * }); * * unsubscribe(); * ``` */ static addInAppCustomActionHandler( handler: InAppCustomActionHandler ): () => void { this.inAppCustomActionHandlers.push(handler); this.eventEmitter.removeAllListeners(INAPP_CUSTOM_ACTION_EVENT_KEY); this.eventEmitter.addListener( INAPP_CUSTOM_ACTION_EVENT_KEY, (event: InAppCustomActionEvent) => { for (const h of this.inAppCustomActionHandlers) { try { h(event); } catch (e) { console.error('InAppCustomActionHandler error:', e); } } } ); BluxModule.startInAppCustomActionHandler?.(); return () => { this.inAppCustomActionHandlers = this.inAppCustomActionHandlers.filter( (h) => h !== handler ); }; } /** * 현재 표시 중인 인앱 메시지를 프로그래밍 방식으로 닫습니다. * * Custom HTML 인앱에서 async 핸들러 완료 후 수동으로 닫을 때 사용합니다. * * @example * ```typescript * BluxClient.addInAppCustomActionHandler(async (event) => { * if (event.actionId === 'share') { * await Share.share({ message: event.data.url }); * BluxClient.dismissInApp(); * } * }); * * // HTML에서 shouldDismiss: false로 호출 * // BluxBridge.triggerAction('share', { url: '...' }, false); * ``` */ static dismissInApp(): void { BluxModule.dismissInApp?.(); } static async onMessage(message: WebViewMessage | string): Promise { try { const parsedMessage = JSON.parse( typeof message === 'string' ? message : message.nativeEvent.data ); const { action, payload } = parsedMessage; switch (action) { case 'initialize': if (payload?.bluxApplicationId && payload?.bluxAPIKey) { await BluxClient.initialize({ bluxApplicationId: payload.bluxApplicationId, bluxAPIKey: payload.bluxAPIKey, requestPermissionOnLaunch: payload.requestPermissionOnLaunch ?? true, customDeviceId: payload.customDeviceId ?? undefined, }); } break; case 'signIn': if (payload?.userId) { await BluxClient.signIn({ userId: payload.userId }); } break; case 'signOut': await BluxClient.signOut(); break; case 'setUserProperties': const userProperties = UserProperties.from(payload); if (userProperties) { await BluxClient.setUserProperties({ userProperties }); } break; case 'setCustomUserProperties': if (payload) { await BluxClient.setCustomUserProperties({ customUserProperties: payload, }); } break; case 'sendEvent': if (payload?.requests) { await BluxClient.sendEventData(payload.requests); } break; default: console.warn(`Unknown action: ${action}`); } } catch (error) { console.error('Error parsing message:', error); } } } export default BluxClient; export * from './Events'; export * from './UserProperties'; export type { BluxNotification, NotificationClickedHandler, } from './notifications/BluxNotification'; export type { NotificationForegroundWillDisplayEvent, NotificationForegroundWillDisplayHandler, } from './notifications/NotificationForegroundWillDisplayEvent'; export type { HttpUrlOpenTarget } from './notifications/HttpUrlOpenTarget'; export type { NotificationUrlOpenOptions } from './notifications/NotificationUrlOpenOptions'; export type { InAppUrlOpenOptions } from './notifications/InAppUrlOpenOptions'; export type { BluxInApp, InAppClickedHandler } from './notifications/BluxInApp';