Files
biiproject-kit-v1/app/Services/MobileConfig/MobileConfigService.php
T

395 lines
19 KiB
PHP

<?php
namespace App\Services\MobileConfig;
use App\Models\MobileSetting;
use App\Models\SystemSetting;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class MobileConfigService
{
private const CACHE_KEY = 'mobile_config_all';
private const DEFINITIONS = [
// 1. BRANDING
'app_name' => ['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,
};
}
}