@getpassage/react-native
Version:
Passage React Native SDK for mobile authentication
380 lines (379 loc) • 14.2 kB
JavaScript
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 };