phase 1-6 implemented
This commit is contained in:
1 parent
22c274bb83
commit
2c4927478a
73 files changed
+9702
-2354
No files matched your search
@@ -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__;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
Reference in new issue
Block a user