UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

674 lines (579 loc) • 20.4 kB
import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage"; import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage"; import { parseJsonIfNeeded } from "@applicaster/zapp-react-native-utils/functionUtils"; export const JS_BRIDGE_EVENTS = { DISMISS_HOOK: "DISMISS_HOOK", LOG: "LOG", GET_SESSION_STORAGE: "GET_SESSION_STORAGE", SET_SESSION_STORAGE: "SET_SESSION_STORAGE", GET_LOCAL_STORAGE: "GET_LOCAL_STORAGE", SET_LOCAL_STORAGE: "SET_LOCAL_STORAGE", GET_SECURE_LOCAL_STORAGE: "GET_SECURE_LOCAL_STORAGE", SET_SECURE_LOCAL_STORAGE: "SET_SECURE_LOCAL_STORAGE", CLOSE_WEBVIEW: "CLOSE_WEBVIEW", NAVIGATE_TO_SCREEN: "NAVIGATE_TO_SCREEN", NAVIGATE_TO_ENTRY: "NAVIGATE_TO_ENTRY", OPEN_URL_SCHEME: "OPEN_URL_SCHEME", OPEN_NATIVE_SHARE: "OPEN_NATIVE_SHARE", ADD_SESSION_STORAGE_LISTENER: "ADD_SESSION_STORAGE_LISTENER", REMOVE_SESSION_STORAGE_LISTENER: "REMOVE_SESSION_STORAGE_LISTENER", ADD_LOCAL_STORAGE_LISTENER: "ADD_LOCAL_STORAGE_LISTENER", REMOVE_LOCAL_STORAGE_LISTENER: "REMOVE_LOCAL_STORAGE_LISTENER", }; export const INVOKE_APPLICASTER_CALLBACK = "__invoke__applicaster__callback"; const APPLICASTER_CALLBACK_KEY = "__applicaster__js2native__callbacks"; const getSessionStorageItem = "getSessionStorageItem"; const setSessionStorageItem = "setSessionStorageItem"; const getLocalStorageItem = "getLocalStorageItem"; const setLocalStorageItem = "setLocalStorageItem"; const getSecureLocalStorageItem = "getSecureLocalStorageItem"; const setSecureLocalStorageItem = "setSecureLocalStorageItem"; const addSessionStorageListenerFn = "addSessionStorageListener"; const addLocalStorageListenerFn = "addLocalStorageListener"; const activeListeners: { [key: string]: () => void } = {}; const sendMessage = ` function sendMessage(type, payload, meta) { if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') { window.ReactNativeWebView.postMessage(JSON.stringify({ type: type, payload: payload, meta: meta })); } } `; const consolePolyfill = ` var consoleLog = console.log; var consoleWarn = console.warn; var consoleError = console.error; function logInConsole(level, args) { var serializableArgs = []; try { serializableArgs = args.map(function(arg) { try { return JSON.parse(JSON.stringify(arg)); } catch (e) { if (typeof arg === 'function') return '[Function]'; if (typeof arg === 'symbol') return String(arg); if (arg instanceof Error) return { name: arg.name, message: arg.message, stack: arg.stack }; return '[Unserializable]'; } }); } catch(e) { serializableArgs = ['Error processing console arguments']; } sendMessage("${JS_BRIDGE_EVENTS.LOG}", { level: level, messages: JSON.stringify(serializableArgs) }); } function log() { var args = [].slice.call(arguments); consoleLog.apply(null, args); try { logInConsole("log", args); } catch (e) { consoleError("Error in custom console.log:", e); } }; function warn() { var args = [].slice.call(arguments); consoleWarn.apply(null, args); try { logInConsole("warn", args); } catch (e) { consoleError("Error in custom console.warn:", e); } } function error() { var args = [].slice.call(arguments); consoleError.apply(null, args); try { logInConsole("error", args); } catch (e) { consoleError("Error in custom console.error:", e); } } setTimeout(function() { console.log = log; console.warn = warn; console.error = error; }, 0); `; const callbackHandling = ` window.Applicaster = window.Applicaster || {}; window.Applicaster.${APPLICASTER_CALLBACK_KEY} = window.Applicaster.${APPLICASTER_CALLBACK_KEY} || {}; var ${INVOKE_APPLICASTER_CALLBACK} = function(key, args) { var callback = window.Applicaster.${APPLICASTER_CALLBACK_KEY}[key]; var deleteCallback = true; if (typeof callback === "function") { var result = callback.apply(null, args); if (result === false) { deleteCallback = false; } } if(deleteCallback) { delete window.Applicaster.${APPLICASTER_CALLBACK_KEY}[key]; } } window.Applicaster.${INVOKE_APPLICASTER_CALLBACK} = ${INVOKE_APPLICASTER_CALLBACK}; `; export function invokeWebCallback(key, args) { const argsArray = Array.isArray(args) ? args : [args]; let stringifiedArgs = "[]"; try { stringifiedArgs = JSON.stringify(argsArray); } catch (e) { try { stringifiedArgs = JSON.stringify([ { error: "Failed to stringify arguments", details: e.message }, ]); } catch { stringifiedArgs = "[]"; } } return ` (function() { var callbackFn = window.Applicaster && window.Applicaster.${APPLICASTER_CALLBACK_KEY}; if (callbackFn && typeof callbackFn["${key}"] === 'function') { try { setTimeout(function() { callbackFn["${key}"].apply(null, ${stringifiedArgs}); }, 0); } catch (e) { console.error("Error invoking web callback ${key}:", e); } } else { // console.warn("Web callback ${key} not found or not a function."); } return true; })(); `; } const hookCallback = ` function hookCallback(options) { var _opts = {}; if (typeof options !== "undefined") { _opts = options; } var success = typeof _opts.success === 'boolean' ? _opts.success : false; var error = _opts.error || undefined; var payload = _opts.payload || undefined; sendMessage("${JS_BRIDGE_EVENTS.DISMISS_HOOK}", { success: success, error: error, payload: payload }) }; `; const uuid = ` function uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } `; const registerCallback = ` function registerCallback(cb, persistent) { var index = uuid(); window.Applicaster = window.Applicaster || {}; window.Applicaster.${APPLICASTER_CALLBACK_KEY} = window.Applicaster.${APPLICASTER_CALLBACK_KEY} || {}; window.Applicaster.${APPLICASTER_CALLBACK_KEY}[index] = function() { var result = cb.apply(null, arguments); return persistent ? false : result; }; return index; } `; const getSetStorage = ` function getSetStorage(type, payload) { return new Promise(function(resolve, reject) { var index = registerCallback(function(result) { if (result && result.success === false) { reject(result.error || new Error("Storage operation failed")); } else { resolve(result); } }); sendMessage(type, payload, index); }); } `; const getInSessionStorage = ` function ${getSessionStorageItem}(key, namespace) { return getSetStorage( "${JS_BRIDGE_EVENTS.GET_SESSION_STORAGE}", { key: key, namespace: namespace } ); } `; const setInSessionStorage = ` function ${setSessionStorageItem}(key, value, namespace) { try { JSON.stringify(value); return getSetStorage( "${JS_BRIDGE_EVENTS.SET_SESSION_STORAGE}", { key: key, value: value, namespace: namespace } ); } catch (e) { return Promise.reject(new Error("Value is not serializable: " + e.message)); } } `; const getInLocalStorage = ` function ${getLocalStorageItem}(key, namespace) { return getSetStorage( "${JS_BRIDGE_EVENTS.GET_LOCAL_STORAGE}", { key: key, namespace: namespace } ); } `; const setInLocalStorage = ` function ${setLocalStorageItem}(key, value, namespace) { try { JSON.stringify(value); return getSetStorage( "${JS_BRIDGE_EVENTS.SET_LOCAL_STORAGE}", { key: key, value: value, namespace: namespace } ); } catch (e) { return Promise.reject(new Error("Value is not serializable: " + e.message)); } } `; const getInSecureLocalStorage = ` function ${getSecureLocalStorageItem}(key, namespace) { return getSetStorage( "${JS_BRIDGE_EVENTS.GET_SECURE_LOCAL_STORAGE}", { key: key, namespace: namespace } ); } `; const setInSecureLocalStorage = ` function ${setSecureLocalStorageItem}(key, value, namespace) { try { JSON.stringify(value); return getSetStorage( "${JS_BRIDGE_EVENTS.SET_SECURE_LOCAL_STORAGE}", { key: key, value: value, namespace: namespace } ); } catch (e) { return Promise.reject(new Error("Value is not serializable: " + e.message)); } } `; const addSessionStorageListener = ` function ${addSessionStorageListenerFn}(optionsOrCallback, callback) { var opts = {}; var cb = callback; if (typeof optionsOrCallback === 'function') { cb = optionsOrCallback; } else if (typeof optionsOrCallback === 'object' && optionsOrCallback !== null) { opts = optionsOrCallback; } else if (optionsOrCallback !== undefined && optionsOrCallback !== null) { console.error("Invalid arguments for addSessionStorageListener. Expected ({ key, namespace }, callback) or (callback)."); return function() {}; } if (typeof cb !== 'function') { console.error("addSessionStorageListener requires a callback function."); return function() {}; } var listenerId = registerCallback(cb, true); sendMessage("${JS_BRIDGE_EVENTS.ADD_SESSION_STORAGE_LISTENER}", { key: opts.key, namespace: opts.namespace }, listenerId); return function removeListener() { sendMessage("${JS_BRIDGE_EVENTS.REMOVE_SESSION_STORAGE_LISTENER}", { listenerId: listenerId }); if (window.Applicaster && window.Applicaster.${APPLICASTER_CALLBACK_KEY}) { delete window.Applicaster.${APPLICASTER_CALLBACK_KEY}[listenerId]; } }; } `; const addLocalStorageListener = ` function ${addLocalStorageListenerFn}(optionsOrCallback, callback) { var opts = {}; var cb = callback; if (typeof optionsOrCallback === 'function') { cb = optionsOrCallback; } else if (typeof optionsOrCallback === 'object' && optionsOrCallback !== null) { opts = optionsOrCallback; } else if (optionsOrCallback !== undefined && optionsOrCallback !== null) { console.error("Invalid arguments for addLocalStorageListener. Expected ({ key, namespace }, callback) or (callback)."); return function() {}; } if (typeof cb !== 'function') { console.error("addLocalStorageListener requires a callback function."); return function() {}; } var listenerId = registerCallback(cb, true); sendMessage("${JS_BRIDGE_EVENTS.ADD_LOCAL_STORAGE_LISTENER}", { key: opts.key, namespace: opts.namespace }, listenerId); return function removeListener() { sendMessage("${JS_BRIDGE_EVENTS.REMOVE_LOCAL_STORAGE_LISTENER}", { listenerId: listenerId }); if (window.Applicaster && window.Applicaster.${APPLICASTER_CALLBACK_KEY}) { delete window.Applicaster.${APPLICASTER_CALLBACK_KEY}[listenerId]; } }; } `; const closeWebview = ` function closeWebview() { sendMessage("${JS_BRIDGE_EVENTS.CLOSE_WEBVIEW}") } `; const navigateToScreen = ` function navigateToScreen(screenId) { if(!screenId) { console.error("navigateToScreen requires a screenId"); return; } sendMessage("${JS_BRIDGE_EVENTS.NAVIGATE_TO_SCREEN}", { screenId: screenId }); } `; const navigateToEntry = ` function navigateToEntry(entry) { if(!entry) { console.error("navigateToEntry requires an entry object"); return; } sendMessage("${JS_BRIDGE_EVENTS.NAVIGATE_TO_ENTRY}", { entry: entry }); } `; const openUrlScheme = ` function openUrlScheme(url) { if(!url || typeof url !== 'string') { console.error("openUrlScheme requires a valid URL string"); return; } sendMessage("${JS_BRIDGE_EVENTS.OPEN_URL_SCHEME}", { url: url }); } `; const runInvokeShare = ` function invokeShare(item, options) { if(!item) { console.error("invokeShare requires an item object"); return; } sendMessage("${JS_BRIDGE_EVENTS.OPEN_NATIVE_SHARE}", { item: item, options: options }); } `; const initializeBridge = (payload, hookPlugin, screens, safeAreaInsets) => ` (function() { window.Applicaster = window.Applicaster || {}; ${sendMessage} ${consolePolyfill} ${hookCallback} ${uuid} ${callbackHandling} ${registerCallback} ${getSetStorage} ${getInSessionStorage} ${setInSessionStorage} ${getInLocalStorage} ${setInLocalStorage} ${getInSecureLocalStorage} ${setInSecureLocalStorage} ${addSessionStorageListener} ${addLocalStorageListener} ${closeWebview} ${navigateToScreen} ${navigateToEntry} ${openUrlScheme} ${runInvokeShare} var JS2Native = { screenHook: { data: { payload: (function() { try { return JSON.parse(${payload}); } catch(e) { console.error('Failed to parse screenHook payload:', e); return null; } })(), plugin: (function() { try { return JSON.parse(${hookPlugin}); } catch(e) { console.error('Failed to parse screenHook plugin:', e); return null; } })() }, callback: hookCallback }, sessionStorage: { getItem: ${getSessionStorageItem}, setItem: ${setSessionStorageItem}, addListener: ${addSessionStorageListenerFn} }, localStorage: { getItem: ${getLocalStorageItem}, setItem: ${setLocalStorageItem}, getSecuredItem: ${getSecureLocalStorageItem}, setSecuredItem: ${setSecureLocalStorageItem}, addListener: ${addLocalStorageListenerFn} }, nativeEnvironment: true, navigation: { closeWebview: closeWebview, navigateToScreen: navigateToScreen, navigateToEntry: navigateToEntry, screens: (function() { try { return JSON.parse(${screens}); } catch(e) { console.error('Failed to parse navigation screens:', e); return []; } })(), openUrlScheme: openUrlScheme }, safeAreaInsets: (function() { try { return JSON.parse(${safeAreaInsets}); } catch(e) { console.error('Failed to parse safeAreaInsets:', e); return {}; } })(), nativeShare: invokeShare }; window.Applicaster.JS2Native = JS2Native; window.JS2Native = JS2Native; return JS2Native; })(); `; export const jsBridge = (hooksProps, screens, safeAreaInsets) => ` var Applicaster = window.Applicaster || {}; window.Applicaster = Applicaster; ${initializeBridge( JSON.stringify(hooksProps?.payload), JSON.stringify(hooksProps?.hookPlugin), JSON.stringify(Object.values(screens || {})), JSON.stringify(safeAreaInsets || {}) )} true; `; // handler for removing listeners async function removeStorageListenerHandler(payload: { listenerId?: string }) { const { listenerId } = payload || {}; if (!listenerId) { return; } const removeListener = activeListeners[listenerId]; if (removeListener) { try { removeListener(); delete activeListeners[listenerId]; } catch (error) { // } } } function log({ level, messages }) { try { const parsedMessages = parseJsonIfNeeded(messages); const logFn = console[level] || console.log; if (Array.isArray(parsedMessages)) { logFn("JS LOG:", ...parsedMessages); } else { logFn("JS LOG:", parsedMessages); } } catch (e) { // console.error("Error parsing or logging JS message:", e, messages); } } async function storageGetItem({ key, namespace }) { try { const result = await sessionStorage.getItem(key, namespace); return { success: true, value: result }; } catch (error) { return { success: false, error: error.message }; } } async function storageSetItem({ key, value, namespace }) { try { const result = await sessionStorage.setItem(key, value, namespace); return { success: !!result }; } catch (error) { return { success: false, error: error.message }; } } async function localStorageGetItem({ key, namespace }) { try { const result = await localStorage.getItem(key, namespace); return { success: true, value: result }; } catch (error) { return { success: false, error: error.message }; } } async function localStorageSetItem({ key, value, namespace }) { try { const result = await localStorage.setItem(key, value, namespace); return { success: !!result }; } catch (error) { return { success: false, error: error.message }; } } async function secureStorageGetItem({ key, namespace }) { try { const result = await localStorage.getKeychainItem(key, namespace); return { success: true, value: result }; } catch (error) { return { success: false, error: error.message }; } } async function secureStorageSetItem({ key, value, namespace }) { try { const result = await localStorage.setKeychainItem(key, value, namespace); return { success: !!result }; } catch (error) { return { success: false, error: error.message }; } } // handler for adding listeners async function addStorageListenerHandler( storageType: "local" | "session", payload: { key?: string; namespace?: string }, callbackKey: string, webViewRef: React.RefObject<any> ) { if (!webViewRef?.current?.injectJavaScript) { return; } const storage = storageType === "local" ? localStorage : sessionStorage; const { key, namespace } = payload || {}; const listenerCallback = (eventData: { key: string; namespace: string; value: any; }) => { const run = invokeWebCallback(callbackKey, [eventData]); if (webViewRef?.current?.injectJavaScript) { webViewRef.current.injectJavaScript(run); } else { removeStorageListenerHandler({ listenerId: callbackKey }); } }; try { const removeListener = storage.addListener( { key, namespace }, listenerCallback ); activeListeners[callbackKey] = removeListener; } catch (error) { const errorRun = invokeWebCallback(callbackKey, [ { success: false, error: `Failed to add listener: ${error.message}` }, ]); if (webViewRef?.current?.injectJavaScript) { webViewRef.current.injectJavaScript(errorRun); webViewRef.current.injectJavaScript( `delete window.Applicaster.${APPLICASTER_CALLBACK_KEY}['${callbackKey}']; true;` ); } delete activeListeners[callbackKey]; } } export function resolveBridgeAction( type: string, webViewRef: React.RefObject<any> ) { switch (type) { case JS_BRIDGE_EVENTS.LOG: return (payload) => log(payload); case JS_BRIDGE_EVENTS.GET_SESSION_STORAGE: return storageGetItem; case JS_BRIDGE_EVENTS.SET_SESSION_STORAGE: return storageSetItem; case JS_BRIDGE_EVENTS.GET_LOCAL_STORAGE: return localStorageGetItem; case JS_BRIDGE_EVENTS.SET_LOCAL_STORAGE: return localStorageSetItem; case JS_BRIDGE_EVENTS.GET_SECURE_LOCAL_STORAGE: return secureStorageGetItem; case JS_BRIDGE_EVENTS.SET_SECURE_LOCAL_STORAGE: return secureStorageSetItem; case JS_BRIDGE_EVENTS.ADD_LOCAL_STORAGE_LISTENER: return (payload, callbackKey) => addStorageListenerHandler("local", payload, callbackKey, webViewRef); case JS_BRIDGE_EVENTS.REMOVE_LOCAL_STORAGE_LISTENER: return (payload) => removeStorageListenerHandler(payload); case JS_BRIDGE_EVENTS.ADD_SESSION_STORAGE_LISTENER: return (payload, callbackKey) => addStorageListenerHandler("session", payload, callbackKey, webViewRef); case JS_BRIDGE_EVENTS.REMOVE_SESSION_STORAGE_LISTENER: return (payload) => removeStorageListenerHandler(payload); default: return async (_payload, callbackKey) => { if (callbackKey && webViewRef?.current?.injectJavaScript) { const errorResult = { success: false, error: `Unknown action type: ${type}`, }; const errorRun = invokeWebCallback(callbackKey, [errorResult]); webViewRef.current.injectJavaScript(errorRun); } return Promise.reject(new Error(`Unknown action type: ${type}`)); }; } }