feat: add expo mobile application source code
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user