395 lines
19 KiB
PHP
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,
|
|
};
|
|
}
|
|
}
|