phase 1-6 implemented

This commit is contained in:
dimitar committed 2026-03-10 04:14:03 +01:00
1 parent 22c274bb83
commit 2c4927478a
73 files changed
+9702 -2354

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
/**
* Environment variable validation for mobile app
* Validates that all required environment variables are set
*/
interface RequiredEnvVars {
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY: string;
EXPO_PUBLIC_API_URL: string;
}
interface OptionalEnvVars {
EXPO_PUBLIC_APP_NAME?: string;
EXPO_PUBLIC_APP_VERSION?: string;
NODE_ENV?: string;
}
type EnvVars = RequiredEnvVars & OptionalEnvVars;
/**
* Validate required environment variables
* Throws error if any required variable is missing
*/
export function validateEnv(): EnvVars {
const errors: string[] = [];
// Required variables
const requiredVars = [
"EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
"EXPO_PUBLIC_API_URL",
] as const;
for (const varName of requiredVars) {
if (!process.env[varName]) {
errors.push(`Missing required environment variable: ${varName}`);
}
}
if (errors.length > 0) {
const errorMessage = [
"❌ Environment validation failed:",
...errors,
"",
"Please check your .env file and ensure all required variables are set.",
"See .env.example for the list of required variables.",
"",
"To create your .env file:",
"1. Copy .env.example to .env",
"2. Fill in the required values",
].join("\n");
throw new Error(errorMessage);
}
// Validate URL format
const apiUrl = process.env.EXPO_PUBLIC_API_URL!;
try {
new URL(apiUrl);
} catch {
throw new Error(
`Invalid EXPO_PUBLIC_API_URL: "${apiUrl}". Must be a valid URL (e.g., http://192.168.1.100:3000)`,
);
}
return {
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY:
process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!,
EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL!,
EXPO_PUBLIC_APP_NAME: process.env.EXPO_PUBLIC_APP_NAME || "FitAI",
EXPO_PUBLIC_APP_VERSION: process.env.EXPO_PUBLIC_APP_VERSION || "1.0.0",
NODE_ENV: process.env.NODE_ENV || "development",
};
}
/**
* Get validated environment variables
* Caches the result after first validation
*/
let cachedEnv: EnvVars | null = null;
export function getEnv(): EnvVars {
if (!cachedEnv) {
cachedEnv = validateEnv();
}
return cachedEnv;
}
/**
* Check if running in development mode
*/
export function isDevelopment(): boolean {
return __DEV__;
}
/**
* Check if running in production mode
*/
export function isProduction(): boolean {
return !__DEV__;
}
+99
View File
@@ -0,0 +1,99 @@
/**
* Error handling utilities for type-safe error processing
*/
/**
* Clerk error structure
*/
interface ClerkError {
errors?: Array<{
message: string;
code?: string;
}>;
message?: string;
}
/**
* Type guard to check if error is a Clerk error
*/
function isClerkError(error: unknown): error is ClerkError {
return (
typeof error === "object" &&
error !== null &&
("errors" in error || "message" in error)
);
}
/**
* Extract error message from unknown error type
* Handles Clerk errors, standard Error objects, and unknown types
*
* @param error - The caught error
* @param fallback - Fallback message if no message can be extracted
* @returns Error message string
*/
export function getErrorMessage(
error: unknown,
fallback = "An unknown error occurred",
): string {
// Handle Clerk errors with nested error array
if (isClerkError(error)) {
if (error.errors?.[0]?.message) {
return error.errors[0].message;
}
if (error.message) {
return error.message;
}
}
// Handle standard Error objects
if (error instanceof Error) {
return error.message;
}
// Handle string errors
if (typeof error === "string") {
return error;
}
// Handle objects with message property
if (
typeof error === "object" &&
error !== null &&
"message" in error &&
typeof (error as Record<string, unknown>).message === "string"
) {
return (error as { message: string }).message;
}
// Fallback for unknown types
return fallback;
}
/**
* Get Clerk error code if available
*/
export function getClerkErrorCode(error: unknown): string | undefined {
if (isClerkError(error) && error.errors?.[0]?.code) {
return error.errors[0].code;
}
return undefined;
}
/**
* Safely extract gymId from Clerk user's public metadata
*
* @param metadata - Clerk publicMetadata object
* @returns gymId string or null if not found
*/
export function getGymIdFromMetadata(metadata: unknown): string | null {
if (
typeof metadata === "object" &&
metadata !== null &&
"gymId" in metadata &&
typeof (metadata as Record<string, unknown>).gymId === "string"
) {
return (metadata as { gymId: string }).gymId;
}
return null;
}
+151
View File
@@ -0,0 +1,151 @@
/**
* Centralized logging utility for FitAI Mobile App
* Provides structured logging with appropriate log levels
*
* In production, logs can be sent to a remote logging service
* For now, we use console with prefixes for easy filtering
*/
/**
* Log levels
*/
type LogLevel = "debug" | "info" | "warn" | "error";
/**
* Check if we're in development mode
*/
const isDevelopment = __DEV__;
/**
* Log level configuration
* - Development: debug and above
* - Production: info and above
*/
const LOG_LEVELS: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
const currentLogLevel: LogLevel = isDevelopment ? "debug" : "info";
const currentLogLevelValue = LOG_LEVELS[currentLogLevel];
/**
* Check if a log level should be output
*/
function shouldLog(level: LogLevel): boolean {
return LOG_LEVELS[level] >= currentLogLevelValue;
}
/**
* Format log message with timestamp and level
*/
function formatMessage(level: LogLevel, message: string): string {
const timestamp = new Date().toISOString();
return `[${timestamp}] [${level.toUpperCase()}] ${message}`;
}
/**
* Centralized logger
*/
export const log = {
/**
* Log debug information (only in development)
*/
debug: (message: string, data?: Record<string, unknown>) => {
if (shouldLog("debug")) {
console.log(formatMessage("debug", message), data || "");
}
},
/**
* Log general information
*/
info: (message: string, data?: Record<string, unknown>) => {
if (shouldLog("info")) {
console.log(formatMessage("info", message), data || "");
}
},
/**
* Log warnings
*/
warn: (message: string, data?: Record<string, unknown>) => {
if (shouldLog("warn")) {
console.warn(formatMessage("warn", message), data || "");
}
},
/**
* Log errors with optional error object
*/
error: (message: string, error?: unknown, data?: Record<string, unknown>) => {
if (shouldLog("error")) {
const errorInfo: Record<string, unknown> = {
...(data || {}),
};
if (error instanceof Error) {
errorInfo.error = {
name: error.name,
message: error.message,
stack: error.stack,
};
} else if (error) {
errorInfo.error = error;
}
console.error(formatMessage("error", message), errorInfo);
}
},
/**
* Log API requests
*/
apiRequest: (method: string, url: string, data?: Record<string, unknown>) => {
log.debug(`API ${method} ${url}`, data);
},
/**
* Log API responses
*/
apiResponse: (
method: string,
url: string,
status: number,
data?: Record<string, unknown>,
) => {
const level = status >= 400 ? "error" : status >= 300 ? "warn" : "info";
if (level === "error") {
log.error(`API ${method} ${url} - ${status}`, undefined, data);
} else if (level === "warn") {
log.warn(`API ${method} ${url} - ${status}`, data);
} else {
log.debug(`API ${method} ${url} - ${status}`, data);
}
},
/**
* Log authentication events
*/
auth: (event: string, data?: Record<string, unknown>) => {
log.info(`Auth: ${event}`, data);
},
/**
* Log navigation events
*/
navigation: (screen: string, data?: Record<string, unknown>) => {
log.debug(`Navigation: ${screen}`, data);
},
};
export default log;
/**
* TODO: In production, consider integrating with:
* - Sentry for error tracking
* - LogRocket for session replay
* - Custom logging service for analytics
*/