feat: add expo mobile application source code

This commit is contained in:
2026-05-21 16:06:35 +07:00
parent 76d7a5c5c6
commit 0c65a7811b
77 changed files with 20356 additions and 0 deletions
+268
View File
@@ -0,0 +1,268 @@
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
import { Platform, AppState, AppStateStatus } from 'react-native';
import { storage } from '../utils/storage';
import { ApiService } from '../services/api';
import NetInfo from '@react-native-community/netinfo';
import { DebugLogger } from '../utils/logger';
interface AppConfig {
branding: {
app_name?: string;
app_tagline?: string;
app_icon_url?: string;
logo_url?: string;
splash_image_url?: string;
brand_color?: string;
theme_color_primary?: string;
theme_color_secondary?: string;
primary_font_family?: string;
};
control_center: {
kill_switch_active?: boolean;
kill_switch_message?: string;
maintenance_start_at?: string;
maintenance_end_at?: string;
maintenance_bypass_ips?: string;
announcement_enabled?: boolean;
announcement_text?: string;
announcement_type?: 'info' | 'warning' | 'danger';
};
app_updates: {
app_version?: string;
min_app_version?: string;
onboarding_version?: string;
store_url_android?: string;
store_url_ios?: string;
store_url_huawei?: string;
};
features: {
enable_registration?: boolean;
enable_guest_mode?: boolean;
require_otp_registration?: boolean;
enable_biometrics?: boolean;
enable_remember_me?: boolean;
review_prompt_enabled?: boolean;
min_actions_before_review?: number;
region_lock_enabled?: boolean;
dashboard_categories?: string;
};
security_auth: {
login_title?: string;
login_subtitle?: string;
token_ttl_minutes?: number;
session_max_age?: number;
login_max_attempts?: number;
biometric_auth_type?: string;
oauth_google_enabled?: boolean;
oauth_apple_enabled?: boolean;
oauth_facebook_enabled?: boolean;
};
connectivity: {
api_base_url?: string;
api_version?: string;
api_timeout_ms?: number;
api_retry_count?: number;
request_cache_ttl?: number;
sync_interval_ms?: number;
enable_ssl_pinning?: boolean;
ssl_pinning_hash?: string;
environment_selector?: string;
};
notifications: {
enable_push_notifications?: boolean;
fcm_topic_default?: string;
default_channel_id?: string;
notification_sound_enabled?: boolean;
badge_count_enabled?: boolean;
priority_level?: string;
};
support_social: {
support_email?: string;
support_whatsapp?: string;
live_chat_url?: string;
faq_url?: string;
privacy_policy_url?: string;
social_instagram_url?: string;
social_twitter_url?: string;
social_facebook_url?: string;
social_youtube_url?: string;
faq_json?: any[];
help_topics_json?: any[];
};
analytics_system: {
crashlytics_enabled?: boolean;
log_level?: string;
event_sampling_rate?: string;
google_analytics_id?: string;
gdpr_compliance_enabled?: boolean;
target_sdk_version?: string;
system_timezone?: string;
default_locale?: string;
};
localization?: {
[lang: string]: Record<string, string>;
};
}
interface ConfigContextType {
config: AppConfig | null;
isLoading: boolean;
isSyncing: boolean;
isConnected: boolean;
syncConfig: () => Promise<void>;
}
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
const STORAGE_KEY = 'cached_mobile_config';
const ETAG_KEY = 'cached_config_etag';
// Minimum sync interval: 3 seconds to feel "instant" while avoiding excessive load
const MIN_SYNC_INTERVAL_MS = 3_000;
const DEFAULT_SYNC_INTERVAL_MS = 5_000;
export function ConfigProvider({ children }: { children: React.ReactNode }) {
const [config, setConfig] = useState<AppConfig | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isConnected, setIsConnected] = useState(true);
const [isSyncing, setIsSyncing] = useState(false);
const isSyncingRef = useRef(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const prevConnected = useRef<boolean | null>(null);
useEffect(() => {
// 1. Monitor network connection
const unsubscribeNet = NetInfo.addEventListener(state => {
const connected = !!state.isConnected;
if (prevConnected.current !== null && prevConnected.current !== connected) {
DebugLogger.log(`Network status changed: ${connected ? 'Online' : 'Offline'}`, connected ? 'success' : 'error');
if (connected) syncConfig();
}
setIsConnected(connected);
prevConnected.current = connected;
});
// 2. Monitor AppState (Sync on foreground)
const subscription = AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'active') {
syncConfig();
}
});
loadInitialConfig();
return () => {
unsubscribeNet();
subscription.remove();
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, []);
// Re-setup interval when config changes (for dynamic sync_interval_ms)
useEffect(() => {
if (intervalRef.current) clearInterval(intervalRef.current);
const rawInterval = config?.connectivity?.sync_interval_ms ?? DEFAULT_SYNC_INTERVAL_MS;
// Clamp to minimum to avoid hammering server
const safeInterval = Math.max(rawInterval, MIN_SYNC_INTERVAL_MS);
intervalRef.current = setInterval(() => {
syncConfig();
}, safeInterval);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [config?.connectivity?.sync_interval_ms]);
const loadInitialConfig = async () => {
try {
// 1. Load from cache first (offline-first approach)
const str = await storage.get(STORAGE_KEY);
if (str) {
try {
const cached = JSON.parse(str);
setConfig(cached);
setIsLoading(false);
} catch {
// Cache is corrupted, remove it
await storage.remove(STORAGE_KEY);
}
}
// 2. Fetch fresh config in background (non-blocking)
await syncConfig();
} catch (error) {
console.warn('[Config] Initialization error:', error);
} finally {
setIsLoading(false);
}
};
const syncConfig = async () => {
if (isSyncingRef.current) return;
isSyncingRef.current = true;
setIsSyncing(true);
try {
const etag = await storage.get(ETAG_KEY);
const response = await ApiService.syncMobileConfig(etag);
if (response?.status === 'not_modified') {
DebugLogger.log('Config is up to date (ETag match)', 'sync');
return;
}
if (response?.status === 'success' && response?.data) {
const freshConfig = response.data;
setConfig(freshConfig);
DebugLogger.log(`Config updated with ETag: ${response.etag?.substring(0, 8)}...`, 'sync');
// Persist to cache
await storage.save(STORAGE_KEY, JSON.stringify(freshConfig));
if (response.etag) {
await storage.save(ETAG_KEY, response.etag);
}
}
} catch (error: any) {
DebugLogger.log(`Sync error: ${error.message}`, 'error');
console.warn('[Config] Sync failed (offline?):', error);
} finally {
isSyncingRef.current = false;
setIsSyncing(false);
setIsLoading(false);
}
};
return (
<ConfigContext.Provider value={{ config, isLoading, isSyncing, isConnected, syncConfig }}>
{children}
</ConfigContext.Provider>
);
}
export function useAppConfig() {
const context = useContext(ConfigContext);
if (context === undefined) {
return {
config: {
branding: { app_name: 'biiproject' },
control_center: {},
app_updates: {},
features: {},
security_auth: {},
connectivity: {},
notifications: {},
support_social: {},
analytics_system: {}
},
isLoading: false,
isSyncing: false,
isConnected: true,
syncConfig: async () => {}
} as any;
}
return context;
}