feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
<?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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user