@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
674 lines (579 loc) • 20.4 kB
text/typescript
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}`));
};
}
}