['type' => 'string', 'group' => 'branding', 'default' => 'biiproject'], 'app_tagline' => ['type' => 'string', 'group' => 'branding', 'default' => 'Smart Solution for Your Business'], 'app_icon_url' => ['type' => 'image_path', 'group' => 'branding', 'default' => null], 'logo_url' => ['type' => 'image_path', 'group' => 'branding', 'default' => null], 'splash_image_url' => ['type' => 'image_path', 'group' => 'branding', 'default' => null], 'brand_color' => ['type' => 'string', 'group' => 'branding', 'default' => '#C6F135'], 'theme_color_primary' => ['type' => 'string', 'group' => 'branding', 'default' => '#C6F135'], 'theme_color_secondary' => ['type' => 'string', 'group' => 'branding', 'default' => '#1A1A1A'], 'primary_font_family' => ['type' => 'string', 'group' => 'branding', 'default' => 'Outfit'], // 2. CONTROL CENTER 'kill_switch_active' => ['type' => 'boolean', 'group' => 'control_center', 'default' => false], 'kill_switch_message' => ['type' => 'string', 'group' => 'control_center', 'default' => 'System is currently undergoing emergency maintenance. Please try again later.'], 'maintenance_start_at' => ['type' => 'string', 'group' => 'control_center', 'default' => null], 'maintenance_end_at' => ['type' => 'string', 'group' => 'control_center', 'default' => null], 'maintenance_bypass_ips' => ['type' => 'string', 'group' => 'control_center', 'default' => '127.0.0.1'], 'announcement_enabled' => ['type' => 'boolean', 'group' => 'control_center', 'default' => false], 'announcement_text' => ['type' => 'string', 'group' => 'control_center', 'default' => 'Maintenance is scheduled for tonight at 12:00 AM.'], 'announcement_type' => ['type' => 'string', 'group' => 'control_center', 'default' => 'info'], // 3. APP UPDATES 'app_version' => ['type' => 'string', 'group' => 'app_updates', 'default' => '2.0.0'], 'min_app_version' => ['type' => 'string', 'group' => 'app_updates', 'default' => '1.0.0'], 'onboarding_version' => ['type' => 'string', 'group' => 'app_updates', 'default' => '1.0.0'], 'store_url_android' => ['type' => 'string', 'group' => 'app_updates', 'default' => 'https://play.google.com/store/apps/details?id=com.biiproject'], 'store_url_ios' => ['type' => 'string', 'group' => 'app_updates', 'default' => 'https://apps.apple.com/app/biiproject'], 'store_url_huawei' => ['type' => 'string', 'group' => 'app_updates', 'default' => 'https://appgallery.huawei.com/'], // 4. FEATURES 'enable_registration' => ['type' => 'boolean', 'group' => 'features', 'default' => true], 'enable_guest_mode' => ['type' => 'boolean', 'group' => 'features', 'default' => false], 'require_otp_registration' => ['type' => 'boolean', 'group' => 'features', 'default' => false], 'enable_biometrics' => ['type' => 'boolean', 'group' => 'features', 'default' => false], 'enable_remember_me' => ['type' => 'boolean', 'group' => 'features', 'default' => true], 'review_prompt_enabled' => ['type' => 'boolean', 'group' => 'features', 'default' => true], 'min_actions_before_review' => ['type' => 'integer', 'group' => 'features', 'default' => 10], 'region_lock_enabled' => ['type' => 'boolean', 'group' => 'features', 'default' => false], // 5. SECURITY & AUTH 'login_title' => ['type' => 'string', 'group' => 'security_auth', 'default' => 'biiproject'], 'login_subtitle' => ['type' => 'string', 'group' => 'security_auth', 'default' => 'Sign in to continue'], 'token_ttl_minutes' => ['type' => 'integer', 'group' => 'security_auth', 'default' => 43200], 'session_max_age' => ['type' => 'integer', 'group' => 'security_auth', 'default' => 86400], 'login_max_attempts' => ['type' => 'integer', 'group' => 'security_auth', 'default' => 5], 'biometric_auth_type' => ['type' => 'string', 'group' => 'security_auth', 'default' => 'any'], 'oauth_google_enabled' => ['type' => 'boolean', 'group' => 'security_auth', 'default' => false], 'oauth_apple_enabled' => ['type' => 'boolean', 'group' => 'security_auth', 'default' => false], 'oauth_facebook_enabled' => ['type' => 'boolean', 'group' => 'security_auth', 'default' => false], // 6. CONNECTIVITY 'api_base_url' => ['type' => 'string', 'group' => 'connectivity', 'default' => 'https://api.biiproject.com'], 'api_version' => ['type' => 'string', 'group' => 'connectivity', 'default' => 'v1'], 'api_timeout_ms' => ['type' => 'integer', 'group' => 'connectivity', 'default' => 30000], 'api_retry_count' => ['type' => 'integer', 'group' => 'connectivity', 'default' => 3], 'request_cache_ttl' => ['type' => 'integer', 'group' => 'connectivity', 'default' => 3600], 'sync_interval_ms' => ['type' => 'integer', 'group' => 'connectivity', 'default' => 10000], 'enable_ssl_pinning' => ['type' => 'boolean', 'group' => 'connectivity', 'default' => false], 'ssl_pinning_hash' => ['type' => 'string', 'group' => 'connectivity', 'default' => null], 'environment_selector' => ['type' => 'string', 'group' => 'connectivity', 'default' => 'production'], // 7. NOTIFICATIONS 'enable_push_notifications' => ['type' => 'boolean', 'group' => 'notifications', 'default' => true], 'fcm_topic_default' => ['type' => 'string', 'group' => 'notifications', 'default' => 'all_users'], 'default_channel_id' => ['type' => 'string', 'group' => 'notifications', 'default' => 'default_channel'], 'notification_sound_enabled' => ['type' => 'boolean', 'group' => 'notifications', 'default' => true], 'badge_count_enabled' => ['type' => 'boolean', 'group' => 'notifications', 'default' => true], 'priority_level' => ['type' => 'string', 'group' => 'notifications', 'default' => 'high'], // 8. SUPPORT & SOCIAL 'support_email' => ['type' => 'string', 'group' => 'support_social', 'default' => 'support@biiproject.com'], 'support_whatsapp' => ['type' => 'string', 'group' => 'support_social', 'default' => '628123456789'], 'live_chat_url' => ['type' => 'string', 'group' => 'support_social', 'default' => null], 'faq_url' => ['type' => 'string', 'group' => 'support_social', 'default' => null], 'privacy_policy_url' => ['type' => 'string', 'group' => 'support_social', 'default' => 'https://biiproject.com/privacy'], 'social_instagram_url' => ['type' => 'string', 'group' => 'support_social', 'default' => null], 'social_twitter_url' => ['type' => 'string', 'group' => 'support_social', 'default' => null], 'social_facebook_url' => ['type' => 'string', 'group' => 'support_social', 'default' => null], 'social_youtube_url' => ['type' => 'string', 'group' => 'support_social', 'default' => null], // 9. ANALYTICS & SYSTEM 'crashlytics_enabled' => ['type' => 'boolean', 'group' => 'analytics_system', 'default' => true], 'log_level' => ['type' => 'string', 'group' => 'analytics_system', 'default' => 'error'], 'event_sampling_rate' => ['type' => 'string', 'group' => 'analytics_system', 'default' => '1.0'], 'google_analytics_id' => ['type' => 'string', 'group' => 'analytics_system', 'default' => null], 'gdpr_compliance_enabled' => ['type' => 'boolean', 'group' => 'analytics_system', 'default' => false], 'target_sdk_version' => ['type' => 'string', 'group' => 'analytics_system', 'default' => '34'], 'system_timezone' => ['type' => 'string', 'group' => 'analytics_system', 'default' => 'Asia/Jakarta'], 'default_locale' => ['type' => 'string', 'group' => 'analytics_system', 'default' => 'en'], // 10. DYNAMIC CONTENT 'dashboard_categories' => ['type' => 'string', 'group' => 'features', 'default' => 'All,Tech,Finance,Health,Coding'], 'faq_json' => ['type' => 'json', 'group' => 'support_social', 'default' => '[{"q":"How to sync?","a":"Click the sync button on dashboard."}]'], 'help_topics_json' => ['type' => 'json', 'group' => 'support_social', 'default' => '[{"id":"1","name":"Account","icon":"user"},{"id":"2","name":"System","icon":"cpu"}]'], ]; public static function getDefinitions(): array { return self::DEFINITIONS; } public function all(bool $refresh = false): array { if ($refresh) { $this->clearCache(); } return Cache::remember(self::CACHE_KEY, now()->addHours(24), function () { $settings = MobileSetting::all(); $config = []; // 1. Initialize from definitions foreach (self::DEFINITIONS as $key => $def) { if (! isset($config[$def['group']])) { $config[$def['group']] = []; } $config[$def['group']][$key] = $def['default']; } // 2. Fallback from Global System Settings (Branding) // This ensures if mobile settings are empty, we use the main site branding if (class_exists(SystemSetting::class)) { $systemSettings = SystemSetting::whereIn('key', ['app_name', 'app_logo', 'app_tagline', 'app_tagline1', 'app_tagline2']) ->pluck('value', 'key'); if ($systemSettings->has('app_name')) { $config['branding']['app_name'] = $systemSettings->get('app_name'); $config['security_auth']['login_title'] = $systemSettings->get('app_name'); } if ($systemSettings->has('app_logo')) { $logoPath = $systemSettings->get('app_logo'); $logoUrl = Str::startsWith($logoPath, 'http') ? $logoPath : asset($logoPath); $config['branding']['logo_url'] = $logoUrl; } if ($systemSettings->has('app_tagline')) { $config['security_auth']['login_subtitle'] = strip_tags($systemSettings->get('app_tagline')); $config['branding']['app_tagline'] = strip_tags($systemSettings->get('app_tagline')); } } // 3. Overlay with Mobile-Specific settings (Overwrites fallbacks) foreach ($settings as $setting) { $value = $this->castValue($setting->value, $setting->type); // Handle Assets (Storage URLs) if ($setting->type === 'image_path' && $value && ! Str::startsWith($value, 'http')) { $value = asset($value); } $group = $setting->group; // Skip if value is default/null to keep system fallback if (($setting->key === 'logo_url' || $setting->key === 'app_name') && (empty($value) || $value === 'biiproject')) { continue; } // Handle Localization Dynamic Keys if ($group === 'localization') { if (! isset($config['localization'])) { $config['localization'] = ['English' => [], 'Indonesian' => []]; } if (str_starts_with($setting->key, 'lang_en_')) { $key = str_replace('lang_en_', '', $setting->key); $config['localization']['English'][$key] = $value; } elseif (str_starts_with($setting->key, 'lang_id_')) { $key = str_replace('lang_id_', '', $setting->key); $config['localization']['Indonesian'][$key] = $value; } continue; } if (! isset($config[$group])) { $config[$group] = []; } // Clean HTML tags for mobile display on critical text fields if (in_array($setting->key, ['kill_switch_message', 'announcement_text'])) { $value = trim(strip_tags((string) $value)); } $config[$group][$setting->key] = $value; } return $config; }); } public function getGroupedSettingsForAdmin(): array { $settings = MobileSetting::all()->keyBy('key'); $grouped = []; // 1. Process from Definitions (Ensure defaults) foreach (self::DEFINITIONS as $key => $def) { $setting = $settings->get($key); if (! $setting) { $setting = new MobileSetting([ 'key' => $key, 'group' => $def['group'], 'type' => $def['type'], 'value' => is_bool($def['default']) ? ($def['default'] ? 'true' : 'false') : (string) $def['default'], ]); } if (! isset($grouped[$def['group']])) { $grouped[$def['group']] = collect(); } $grouped[$def['group']]->push($setting); $settings->forget($key); // Mark as processed } // 2. Process remaining settings in DB (like dynamic localization keys) foreach ($settings as $setting) { if (! isset($grouped[$setting->group])) { $grouped[$setting->group] = collect(); } $grouped[$setting->group]->push($setting); } return $grouped; } /** * Get all keys that should be treated as boolean. */ public function getBooleanKeys(): array { $definitions = collect(self::DEFINITIONS) ->filter(fn ($def) => $def['type'] === 'boolean') ->keys() ->toArray(); $db = MobileSetting::where('type', 'boolean')->pluck('key')->toArray(); return array_unique(array_merge($definitions, $db)); } public function update(array $data, array $files = []): void { \DB::transaction(function () use ($data, $files) { $settings = MobileSetting::all()->keyBy('key'); // Handle Files foreach ($files as $key => $file) { if ($file instanceof UploadedFile) { $setting = $settings->get($key); $def = self::DEFINITIONS[$key] ?? null; if (! $setting && $def) { $setting = MobileSetting::create([ 'key' => $key, 'group' => $def['group'], 'type' => $def['type'], ]); $settings->put($key, $setting); // Sync to collection } if ($setting && $setting->type === 'image_path') { // Delete old file if exists if ($setting->value) { $oldPath = str_replace('/storage/', '', $setting->value); Storage::disk('public')->delete($oldPath); } // OPTIMIZATION: Convert to WebP (only if GD extension is available) $hasGd = function_exists('imagewebp') && function_exists('imagecreatefromstring'); $filename = $key.'_'.time().($hasGd ? '.webp' : '.'.$file->getClientOriginalExtension()); if ($hasGd) { $tempPath = $file->getRealPath(); $content = file_get_contents($tempPath); $image = imagecreatefromstring($content); if ($image) { imagepalettetotruecolor($image); imagealphablending($image, true); imagesavealpha($image, true); // NEW: Auto Resize (Max width 1200px) $width = imagesx($image); $height = imagesy($image); if ($width > 1200) { $newWidth = 1200; $newHeight = (int) ($height * (1200 / $width)); $resized = imagecreatetruecolor($newWidth, $newHeight); imagealphablending($resized, false); imagesavealpha($resized, true); imagecopyresampled($resized, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height); imagedestroy($image); $image = $resized; } ob_start(); imagewebp($image, null, 80); $webpContent = ob_get_clean(); imagedestroy($image); Storage::disk('public')->put('mobile-assets/'.$filename, $webpContent); $data[$key] = '/storage/mobile-assets/'.$filename; } else { $hasGd = false; } } if (! $hasGd) { // Standard store fallback $path = $file->storeAs('mobile-assets', $filename, 'public'); $data[$key] = '/storage/'.$path; } } } } // Handle Values foreach ($data as $key => $value) { $setting = $settings->get($key); $def = self::DEFINITIONS[$key] ?? null; // Handle dynamic keys (like localization) that are already in DB but not in DEFINITIONS if (! $def && ! $setting) { continue; } if (! $setting && $def) { $setting = MobileSetting::create([ 'key' => $key, 'group' => $def['group'], 'type' => $def['type'], ]); } $newValue = $value; if ($setting->type === 'boolean') { $newValue = (filter_var($value, FILTER_VALIDATE_BOOLEAN) || $value === '1' || $value === 'true') ? 'true' : 'false'; } $setting->update(['value' => $newValue]); } }); $this->clearCache(); } /** * Clear the cache. */ public function clearCache(): void { self::clearCacheStatic(); } public static function clearCacheStatic(): void { Cache::forget(self::CACHE_KEY); } /** * Cast raw string value from DB to appropriate PHP type. */ private function castValue(mixed $value, string $type): mixed { if ($value === null) { return null; } return match ($type) { 'boolean' => $value === 'true' || $value === '1' || $value === true, 'int', 'integer' => (int) $value, 'json' => json_decode($value, true), default => $value, }; } }