import { Constants } from './constants'; import { Logger } from './logger'; import type { BaseResourceParamType, SdkParamEncodingTransformer, TFType, } from '../models/mp-client-sdk'; import type { MapLike } from './app-types'; import { flatten, unflatten } from 'flat'; import { EventBus } from './event-bus'; import { Dimensions, ScaledSize } from 'react-native'; import { URL } from 'react-native-url-polyfill'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { ulid } from 'ulid'; /** Maximum payload size in bytes (250KB) to prevent DoS attacks */ const MAX_PAYLOAD_SIZE_BYTES = 250 * 1024; export class Utils { static triggerEvent(eventName: string, payload: any): void { EventBus.triggerEvent(eventName, payload); } static calculateMsp(dim: ScaledSize, limit: number): boolean { return dim.scale * dim.width >= limit || dim.scale * dim.height >= limit; } static _makeUniqueStringArray(input: string[]): string[] { return [...new Set(input)]; } static parseQueryParamsToObject(url?: string): Record { const op: Record = {}; try { if (url && url?.trim().length > 0) { const urlObject = new URL(url); urlObject.searchParams.forEach((value, name) => { op[name] = value; }); } } catch (err) { // TODO: Report error Logger.logError('Unable to parse query params', err); } return op; } static isTablet(): boolean { const dim = Dimensions.get('screen'); return ( (dim.scale < 2 && this.calculateMsp(dim, 1000)) || (dim.scale >= 2 && this.calculateMsp(dim, 1900)) ); } static sleep(delay = 1000): Promise { return new Promise((resolve: (val?: any) => void) => { setTimeout(() => { return resolve(); }, delay); }); } /** * deviceTypesMappings = {'1':'Desktop Web','2':'Mobile Web','3':'Tablet','4':'Other'}; * @param deviceType */ static dtToNum(deviceType: string): number { switch (deviceType) { case 'desktop': return 1; case 'mobile': return 2; case 'tablet': return 3; default: return 4; } } /** * static browserToOsMappings = {'101': 'ios','102': 'android', '7': 'Other'}; * @param browser */ static bwToNum(browser: string): number { switch (browser) { case 'ios': return 101; case 'android': return 102; default: return 7; } } static langToNum(language: string): number { if (language === 'en') { return 1; } else if (language === 'es') { return 2; } else { return 3; } } static mergeMaps( input: MapLike, ...toMerge: Array> ): MapLike { if (!input) { input = {} as MapLike; } for (const mapObject of toMerge) { if (mapObject) { Object.keys(mapObject).forEach((key: string) => { input[key] = mapObject[key]; }); } } return input; } static applyTransformationResourceParam( paramValue: string, tf: TFType | undefined ): any { if (!tf || tf.length === 0) { return paramValue; } if (typeof paramValue === 'undefined') { return paramValue; } // transformers here are expected to be ordered when we get it from the server after build for (let tfItem of tf) { if (tfItem.nm === 'enc') { // only url encode is implemented now tfItem = tfItem as SdkParamEncodingTransformer; if (tfItem.eTyp === 'URL_ENC') { paramValue = encodeURIComponent(paramValue); } else if (tfItem.eTyp === 'BASE64') { paramValue = this.toBase64(paramValue); } } } return paramValue; } /** * Verifies if the given 'toTest' string exists in any of the input array of strings. * By default, this method does a case sensitive match unless specified by the ignoreCase param. * @param toTest * @param inputs * @param ignoreCase - pass a value of 'true' to perform case-insensitive matching. * @param fullMatch */ static isMatch( toTest: string, inputs: string[], ignoreCase: boolean, fullMatch: boolean ): boolean { if (!toTest) { return false; } let isMatched = false; for (let i = 0; i < inputs.length; i += 1) { const _val = inputs[i]; if (_val) { if (this.convertToRegex(_val, ignoreCase, fullMatch).test(toTest)) { isMatched = true; break; } } } return isMatched; } static regExpEscape(input: string): string { return input.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); } static convertToRegex( input: string, ignoreCase: boolean, fullMatch: boolean ): RegExp { if (fullMatch) { return new RegExp( '^' + input.split(/\*+/).map(this.regExpEscape).join('.*') + '$', ignoreCase ? 'i' : '' ); } else { return new RegExp( input.split(/\*+/).map(this.regExpEscape).join('.*'), ignoreCase ? 'i' : '' ); } } static safeExecute(fn: (payload: any) => any, payload: any): any { try { return fn(payload); } catch (err) { Logger.logError('Error executing function: ', err); } } /** * Encode string to Base64 - React Native compatible * Works with both ASCII and UTF-8 strings * @param str String to encode * @returns Base64 encoded string */ static toBase64(str: string): string { try { // Handle UTF-8 strings properly using encodeURIComponent + unescape trick // This works in all JavaScript engines including React Native's Hermes/JSC return btoa(unescape(encodeURIComponent(str))); } catch (err) { Logger.logError('Error encoding to Base64:', err); return str; // Return original value if encoding fails } } /** * Encode an object payload to Base64 string * @param payload Object to encode * @returns Base64 encoded string, or empty string on error */ static encodeToBase64(payload: Record): string { try { const jsonStr = JSON.stringify(payload); return this.toBase64(jsonStr); } catch (err) { Logger.logError('Error encoding payload to Base64:', err); return ''; } } /** * Get the size of a payload in bytes * @param payload Object to measure * @returns Size in bytes */ static getPayloadSizeBytes(payload: Record): number { try { const jsonStr = JSON.stringify(payload); // Use TextEncoder for accurate byte count (handles UTF-8) return new TextEncoder().encode(jsonStr).length; } catch (err) { Logger.logError('Error calculating payload size:', err); return 0; } } /** * Check if payload exceeds the maximum allowed size (250KB) * @param payload Object to check * @returns True if payload is oversized */ static isPayloadOversized(payload: Record): boolean { return this.getPayloadSizeBytes(payload) > MAX_PAYLOAD_SIZE_BYTES; } static getUniqueID(): string { return ulid(); } /** * Make HTTP GET request - React Native implementation * @param url URL to fetch * @returns Promise with response data */ static async getHttp(url: string): Promise { try { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { Logger.logError('HTTP GET request failed:', error); throw error; } } /** * Get value from AsyncStorage (React Native equivalent of sessionStorage) * @param key Storage key * @param defaultValue Default value if key not found * @returns Promise with stored value or default */ static async getValueFromAsyncStorage( key: string, defaultValue: T ): Promise { try { const value = await AsyncStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; } catch (error) { Logger.logError('Error getting value from AsyncStorage:', error); return defaultValue; } } /** * Set value to AsyncStorage (React Native equivalent of sessionStorage) * @param key Storage key * @param value Value to store */ static async setValueToAsyncStorage(key: string, value: any): Promise { try { await AsyncStorage.setItem(key, JSON.stringify(value)); } catch (error) { Logger.logError('Error setting value to AsyncStorage:', error); } } static getSimpleDebugId(prefix?: string): string { return `${prefix ?? ''}${this.randomNumber(100000000, 99999999999)}`; } static randomNumber(min: number, max: number): number { return Math.ceil(Math.random() * (max - min) + min); } static unFlattenResourceParams( input: T[] ): MapLike { if (!input || input.length === 0) { return {}; } const transformMap: MapLike = {}; const op: MapLike = {}; // convert input into a map for quick search O(1) for (const item of input) { if (item.pId) { if (!transformMap[item.pId]) { item.children = []; transformMap[item.id] = item; } else { transformMap[item.pId].children.push(item); } } else { item.children = []; transformMap[item.id] = item; } } // sort children according to child_order inside children if they have any children for (const item of Object.values(transformMap)) { if (item.children && item.children.length > 0) { item.children = item.children.sort(Constants.sortAscending); op[item.nm] = item; } else { op[item.nm] = item; } } return op; } static unFlattenObject(obj: any): Record { if (!obj) { return {}; } return unflatten(obj, { object: false, delimiter: '.' }); } static flattenObject(obj: any): Record { if (!obj) { return {}; } return flatten(obj, { maxDepth: 5, safe: true }); } }