UNPKG

@getpassage/react-native

Version:

Passage React Native SDK for mobile authentication

380 lines (379 loc) 14.2 kB
import { jwtDecode } from "jwt-decode"; import { DEFAULT_LOGGER_ENDPOINT } from "./config"; // Optional imports - these may not be available in all environments let AppState; let Platform; let DeviceInfo; try { const RN = require("react-native"); AppState = RN.AppState; Platform = RN.Platform; } catch (_a) { // React Native not available } try { DeviceInfo = require("react-native-device-info"); } catch (_b) { // react-native-device-info not available } class ConsoleTransport { log(level, message, metadataOrContext) { const args = metadataOrContext ? [metadataOrContext] : []; switch (level) { case "debug": console.log(message, ...args); break; case "info": console.info(message, ...args); break; case "warn": console.warn(message, ...args); break; case "error": console.error(message, ...args); break; } } } class HttpTransport { constructor(config) { this.queue = []; this.isProcessing = false; this.config = Object.assign({ batchSize: 10, flushInterval: 5000, maxRetries: 3, retryDelay: 1000 }, config); this.sessionId = this.extractSessionId(); this.setupAppStateHandlers(); this.scheduleFlush(); } extractSessionId() { // If explicit sessionId provided, use it if (this.config.sessionId) { return this.config.sessionId; } // If intentToken provided, decode it to get sessionId if (this.config.intentToken) { try { const decoded = jwtDecode(this.config.intentToken); return decoded.sessionId; } catch (error) { console.warn("Failed to decode intent token for session ID:", error); return null; } } // No intentToken available, return null return null; } setupAppStateHandlers() { // Try to detect React Native AppState if (AppState) { AppState.addEventListener("change", (nextAppState) => { if (nextAppState === "background" || nextAppState === "inactive") { this.flush(); } }); } else if (typeof document !== "undefined") { // Fall back to web page visibility API if available document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") { this.flush(); } }); // Flush on page unload window.addEventListener("beforeunload", () => { this.flush(); }); } } scheduleFlush() { if (this.flushTimer) { clearTimeout(this.flushTimer); } this.flushTimer = setTimeout(() => { this.flush(); this.scheduleFlush(); }, this.config.flushInterval); } createLogEntry(level, message, metadataOrContext) { var _a; let context; let metadata = {}; if (metadataOrContext !== undefined && metadataOrContext !== null) { if (typeof metadataOrContext === "string") { context = metadataOrContext; } else if (metadataOrContext instanceof Error) { // If it's an Error object, serialize it properly metadata = Object.assign({ name: metadataOrContext.name, message: metadataOrContext.message, stack: metadataOrContext.stack }, Object.getOwnPropertyNames(metadataOrContext).reduce((acc, key) => { acc[key] = metadataOrContext[key]; return acc; }, {})); } else if (typeof metadataOrContext === "object") { // Pass the whole object directly as metadata metadata = Object.assign({}, metadataOrContext); } else { // Handle primitive types or unknown values metadata = { value: metadataOrContext }; } } return { source: "sdk", level, message, context, metadata, timestamp: new Date().toISOString(), sessionId: (_a = this.sessionId) !== null && _a !== void 0 ? _a : undefined, sdkName: this.config.sdkName, sdkVersion: this.config.sdkVersion, appVersion: this.config.appVersion, platform: this.config.platform, deviceInfo: this.config.deviceInfo, }; } log(level, message, metadataOrContext) { const entry = this.createLogEntry(level, message, metadataOrContext); this.queue.push(entry); // Flush if batch size reached if (this.queue.length >= this.config.batchSize) { this.flush(); } } async flush() { if (this.isProcessing || this.queue.length === 0) { return; } this.isProcessing = true; const logsToSend = [...this.queue]; this.queue = []; try { await this.sendLogs(logsToSend); } catch (error) { // Re-queue failed logs (with limit to prevent infinite growth) if (this.queue.length < 100) { this.queue.unshift(...logsToSend); } console.warn("Failed to send logs to server:", error); } finally { this.isProcessing = false; } } async sendLogs(logs, retryCount = 0) { try { const response = await fetch(this.config.endpoint, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ logs }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { if (retryCount < this.config.maxRetries) { await new Promise((resolve) => setTimeout(resolve, this.config.retryDelay * (retryCount + 1))); return this.sendLogs(logs, retryCount + 1); } throw error; } } // Cleanup method destroy() { if (this.flushTimer) { clearTimeout(this.flushTimer); } this.flush(); // Final flush } } class Logger { constructor(config = {}) { var _a, _b; this.transports = [new ConsoleTransport()]; this.enabled = false; // Default to disabled this.debugMode = false; this.config = Object.assign({ enableHttpTransport: true, httpTransport: Object.assign({ endpoint: DEFAULT_LOGGER_ENDPOINT, sdkName: "react-native" }, config.httpTransport) }, config); // Auto-configure HTTP transport if enabled if (this.config.enableHttpTransport && ((_a = this.config.httpTransport) === null || _a === void 0 ? void 0 : _a.endpoint) && ((_b = this.config.httpTransport) === null || _b === void 0 ? void 0 : _b.sdkName)) { this.setupDefaultHttpTransport(); } } setupDefaultHttpTransport() { try { // Only add if we don't already have an HTTP transport const hasHttpTransport = this.transports.some((transport) => transport instanceof HttpTransport); if (!hasHttpTransport && this.config.httpTransport) { const httpConfig = this.config.httpTransport; this.addHttpTransport(httpConfig); } } catch (error) { console.warn("Failed to setup default HTTP transport:", error); } } addTransport(transport) { this.transports.push(transport); } removeTransport(transport) { this.transports = this.transports.filter((t) => t !== transport); } setEnabled(enabled) { this.enabled = enabled; } setDebugMode(debug) { this.debugMode = debug; this.enabled = debug; } updateIntentToken(intentToken) { // Update HTTP transport config if it exists const httpTransport = this.transports.find((transport) => transport instanceof HttpTransport); if (httpTransport) { // Remove existing HTTP transport this.removeTransport(httpTransport); // Add new HTTP transport with updated intent token if (this.config.httpTransport) { this.addHttpTransport(Object.assign(Object.assign({}, this.config.httpTransport), { intentToken: intentToken || undefined })); } } } logToTransports(level, message, metadataOrContext) { if (!this.enabled) return; this.transports.forEach((transport) => { try { transport.log(level, message, metadataOrContext); } catch (error) { // Prevent errors in transports from breaking the app console.error("Logger transport error:", error); } }); } debug(message, metadataOrContext) { this.logToTransports("debug", message, metadataOrContext); } log(message, metadataOrContext) { this.logToTransports("debug", message, metadataOrContext); } info(message, metadataOrContext) { this.logToTransports("info", message, metadataOrContext); } warn(message, metadataOrContext) { this.logToTransports("warn", message, metadataOrContext); } error(message, metadataOrContext) { this.logToTransports("error", message, metadataOrContext); } // Convenience method to add HTTP transport with sensible defaults addHttpTransport(config) { var _a, _b, _c, _d; try { // Get platform info if available let platform; let deviceInfo; if (Platform) { platform = Platform.OS; // Try to get device info if (DeviceInfo) { deviceInfo = { brand: (_a = DeviceInfo.getBrand) === null || _a === void 0 ? void 0 : _a.call(DeviceInfo), model: (_b = DeviceInfo.getModel) === null || _b === void 0 ? void 0 : _b.call(DeviceInfo), systemVersion: (_c = DeviceInfo.getSystemVersion) === null || _c === void 0 ? void 0 : _c.call(DeviceInfo), deviceId: (_d = DeviceInfo.getUniqueId) === null || _d === void 0 ? void 0 : _d.call(DeviceInfo), }; } } else { // Not in React Native - collect web-specific device info platform = typeof navigator !== "undefined" ? "web" : "unknown"; if (typeof navigator !== "undefined" && typeof window !== "undefined") { deviceInfo = { userAgent: navigator.userAgent, language: navigator.language, platform: navigator.platform, cookieEnabled: navigator.cookieEnabled, onLine: navigator.onLine, connection: navigator.connection ? { effectiveType: navigator.connection.effectiveType, downlink: navigator.connection.downlink, rtt: navigator.connection.rtt, } : undefined, screen: { width: window.screen.width, height: window.screen.height, colorDepth: window.screen.colorDepth, pixelDepth: window.screen.pixelDepth, }, viewport: { width: window.innerWidth, height: window.innerHeight, }, }; } } const transport = new HttpTransport(Object.assign({ platform, deviceInfo }, config)); this.addTransport(transport); return transport; } catch (error) { console.error("Failed to add HTTP transport:", error); throw error; } } // Data truncation utilities for sensitive logging truncateData(data, maxLength = 100) { if (!data) return "null"; const str = typeof data === "string" ? data : JSON.stringify(data); if (str.length <= maxLength) return str; return `${str.substring(0, maxLength)}... (truncated, original length: ${str.length})`; } truncateUrl(url, maxLength = 100) { if (!url) return ""; if (url.length <= maxLength) return url; return `${url.substring(0, maxLength)}...`; } } // Get configuration from environment or defaults function getDefaultLoggerConfig() { // Try to get configuration from environment or global const config = { enableHttpTransport: true, httpTransport: { endpoint: DEFAULT_LOGGER_ENDPOINT, sdkName: "react-native-js", }, }; // Check for global configuration (if set by app) try { // React Native global object const globalObj = globalThis; if (globalObj && globalObj.PASSAGE_LOGGER_CONFIG) { const globalConfig = globalObj.PASSAGE_LOGGER_CONFIG; Object.assign(config, globalConfig); } } catch (error) { // Ignore global config if not available } return config; } // Export singleton instance with default configuration export const logger = new Logger(getDefaultLoggerConfig()); // Export classes for advanced usage export { Logger, HttpTransport, ConsoleTransport };