From 4598777a61ea1bc1ce2348aa9a94d570edc05a9f Mon Sep 17 00:00:00 2001 From: David Beentjes Date: Tue, 9 Jun 2026 11:14:01 +0200 Subject: [PATCH] feat: better splash screens (iOS SVG + continuous native Android splash) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept splash.svg on iOS as a native vector LaunchImage, and hold Android's system splash until the WebView is ready to eliminate the phase-1→phase-2 jump when configured. iOS: - Support public/splash.svg (+ splash-dark.svg) as a vector asset (preserves-vector-representation); falls back to the PNG path. Android: - Add androidx.core:core-splashscreen; installSplashScreen() + setKeepOnScreenCondition hold the system splash in native style. - Generate Theme.AndroidPHP.Splash and point the app theme at it. - New android.splash config: style (image|native), background, background_night. - System splash shows the app launcher icon in both styles; image style keeps the full-bleed Compose overlay, native relies on the held system splash (API 31+, overlay fallback below 31). - Apply splash style/icon/background at build time so changes take effect on rebuild, and set the post-splash windowBackground to the splash color to avoid a white flash on handoff. - Guarantee an @drawable/splash drawable (placeholder + app-icon/PNG fallback) so resource linking never fails. --- config/nativephp.php | 23 ++++ resources/androidstudio/app/build.gradle.kts | 1 + .../app/src/main/AndroidManifest.xml | 2 +- .../com/nativephp/mobile/ui/MainActivity.kt | 15 ++- .../app/src/main/res/drawable/splash.xml | 9 ++ .../androidstudio/gradle/libs.versions.toml | 2 + .../references/available-docs.md | 2 +- src/Traits/InstallsAndroid.php | 16 ++- src/Traits/InstallsAndroidSplashScreen.php | 40 +++++- src/Traits/InstallsSplashScreen.php | 58 +++++++++ src/Traits/PreparesBuild.php | 7 +- src/Traits/UpdatesSplashConfiguration.php | 116 ++++++++++++++++++ tests/Unit/Config/SplashConfigTest.php | 17 +++ .../Resources/AndroidSplashResourcesTest.php | 49 ++++++++ .../InstallsAndroidSplashScreenTest.php | 28 +++++ .../Traits/InstallsAndroidThemeSplashTest.php | 30 +++++ .../Unit/Traits/InstallsSplashScreenTest.php | 55 +++++++++ tests/Unit/Traits/PreparesBuildSplashTest.php | 97 ++++++++++++++- 18 files changed, 556 insertions(+), 11 deletions(-) create mode 100644 resources/androidstudio/app/src/main/res/drawable/splash.xml create mode 100644 src/Traits/UpdatesSplashConfiguration.php create mode 100644 tests/Unit/Config/SplashConfigTest.php create mode 100644 tests/Unit/Resources/AndroidSplashResourcesTest.php create mode 100644 tests/Unit/Traits/InstallsAndroidThemeSplashTest.php create mode 100644 tests/Unit/Traits/InstallsSplashScreenTest.php diff --git a/config/nativephp.php b/config/nativephp.php index 870b070f..acfc02bc 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -260,6 +260,29 @@ 'color_on_primary' => env('NATIVEPHP_ANDROID_COLOR_ON_PRIMARY', '#FFFFFF'), ], + /* + |-------------------------------------------------------------------------- + | Android Splash Screen + |-------------------------------------------------------------------------- + | + | style: 'image' - keep the full-bleed Compose splash overlay + | (current behavior; default). + | 'native' - rely solely on the held system splash + | (icon-on-background, jump-free) on API 31+. + | background: Splash window background color (light). Hex #RRGGBB or + | #AARRGGBB. Wrap in quotes in .env ('#' starts a comment). + | background_night: Splash window background color (dark mode). + | + | Provide public/splash.png (and optional public/splash-dark.png). On API < 31 + | the full-bleed overlay is always used as a fallback regardless of style. + | + */ + 'splash' => [ + 'style' => env('NATIVEPHP_ANDROID_SPLASH_STYLE', 'image'), + 'background' => env('NATIVEPHP_ANDROID_SPLASH_BACKGROUND', '#FFFFFF'), + 'background_night' => env('NATIVEPHP_ANDROID_SPLASH_BACKGROUND_NIGHT', '#000000'), + ], + /* |-------------------------------------------------------------------------- | Android Build Configuration diff --git a/resources/androidstudio/app/build.gradle.kts b/resources/androidstudio/app/build.gradle.kts index c832b0a3..bd631a01 100644 --- a/resources/androidstudio/app/build.gradle.kts +++ b/resources/androidstudio/app/build.gradle.kts @@ -156,6 +156,7 @@ android { dependencies { implementation(libs.androidx.core.ktx) + implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.appcompat) implementation(libs.androidx.material) implementation(libs.androidx.constraintlayout) diff --git a/resources/androidstudio/app/src/main/AndroidManifest.xml b/resources/androidstudio/app/src/main/AndroidManifest.xml index d35b1187..a586e2a7 100644 --- a/resources/androidstudio/app/src/main/AndroidManifest.xml +++ b/resources/androidstudio/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ android:label="NativePHP" android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="true" - android:theme="@style/Theme.AndroidPHP" + android:theme="@style/Theme.AndroidPHP.Splash" tools:targetApi="31"> = Build.VERSION_CODES.S) { + splashScreen.setKeepOnScreenCondition { showSplash } + } instance = this // Android 15 edge-to-edge compatibility fix @@ -898,9 +908,10 @@ class MainActivity : FragmentActivity(), WebViewProvider { } ) - // Splash overlay with fade animation (full screen, no insets) + // Full-bleed overlay in image style, or as a fallback below API 31 + val useOverlay = splashStyle == "image" || Build.VERSION.SDK_INT < Build.VERSION_CODES.S AnimatedVisibility( - visible = showSplash, + visible = showSplash && useOverlay, exit = fadeOut(animationSpec = tween(300)) ) { SplashScreen() diff --git a/resources/androidstudio/app/src/main/res/drawable/splash.xml b/resources/androidstudio/app/src/main/res/drawable/splash.xml new file mode 100644 index 00000000..c441ece2 --- /dev/null +++ b/resources/androidstudio/app/src/main/res/drawable/splash.xml @@ -0,0 +1,9 @@ + + + diff --git a/resources/androidstudio/gradle/libs.versions.toml b/resources/androidstudio/gradle/libs.versions.toml index 8324609b..aa117370 100644 --- a/resources/androidstudio/gradle/libs.versions.toml +++ b/resources/androidstudio/gradle/libs.versions.toml @@ -2,6 +2,7 @@ agp = "8.13.2" kotlin = "2.0.0" coreKtx = "1.13.1" +coreSplashscreen = "1.0.1" appcompat = "1.7.1" material = "1.12.0" constraintlayout = "2.2.1" @@ -21,6 +22,7 @@ playServicesLocation = "21.0.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } diff --git a/resources/boost/skills/nativephp-mobile/references/available-docs.md b/resources/boost/skills/nativephp-mobile/references/available-docs.md index dfe432f4..38b22583 100644 --- a/resources/boost/skills/nativephp-mobile/references/available-docs.md +++ b/resources/boost/skills/nativephp-mobile/references/available-docs.md @@ -22,7 +22,7 @@ - [https://nativephp.com/docs/mobile/3/the-basics/native-functions] Use these docs for the native bridge system: the `nativephp_call()` function, PHP facades (Biometrics, Browser, Camera from `Native\Mobile\Facades\`), the `#nativephp` JavaScript package alias with full TypeScript types, `On()`/`Off()` event methods in JS, and Vue.js microphone recording as an integration example - [https://nativephp.com/docs/mobile/3/the-basics/native-components] Use these docs for the EDGE native UI system overview: Blade-style syntax rendered as truly native components (bottom-nav, top-bar, side-nav), hot reload without recompile, and platform-specific rendering per iOS/Android design guidelines - [https://nativephp.com/docs/mobile/3/the-basics/web-view] Use these docs for web view configuration: `` with edge-to-edge display and `viewport-fit`, CSS variables `--inset-top`/`--inset-bottom`/`--inset-left`/`--inset-right` for safe area insets, the `nativephp-safe-area` helper class, and `nativephp.status_bar_style` config (auto/light/dark for Android); supports Livewire, Vue, React, Svelte, HTMX -- [https://nativephp.com/docs/mobile/3/the-basics/splash-screens] Use these docs for splash screen setup: place `public/splash.png` (light) and `public/splash-dark.png` (dark), PNG format minimum 1080x1920px, GD PHP extension required with ~2GB memory +- [https://nativephp.com/docs/mobile/3/the-basics/splash-screens] Use these docs for splash screen setup: place `public/splash.png` (light) and `public/splash-dark.png` (dark), PNG format minimum 1080x1920px, GD PHP extension required with ~2GB memory. iOS also accepts `public/splash.svg` (+ `splash-dark.svg`) as a native vector LaunchImage (no rasterizer); Android splash input is PNG-only. Android system splash is held until the WebView is ready (no phase jump) via `androidx.core:core-splashscreen`; configure `android.splash.style` (`image` = full-bleed Compose overlay, default; `native` = icon-on-background system splash on API 31+) and `android.splash.background` / `background_night` (hex). In `image` style the system splash icon is transparent so only the background color shows before the overlay. API < 31 always falls back to the full-bleed overlay. - [https://nativephp.com/docs/mobile/3/the-basics/app-icon] Use these docs for app icon setup: place `public/icon.png` at 1024x1024px PNG with no transparency, GD PHP extension required (~2GB memory), auto-resized for all Android density variants; default NativePHP icon used if none provided - [https://nativephp.com/docs/mobile/3/the-basics/assets] Use these docs for asset management: must run Vite build (`npm run build`) before `native:run` for JS/CSS; all app root files are bundled (use relative paths, not storage_path()); user-generated files in `public` via `Storage::disk('mobile_public')`; set `FILESYSTEM_DISK=mobile_public` in .env diff --git a/src/Traits/InstallsAndroid.php b/src/Traits/InstallsAndroid.php index ad2b0db8..7dcc0fe3 100644 --- a/src/Traits/InstallsAndroid.php +++ b/src/Traits/InstallsAndroid.php @@ -66,12 +66,14 @@ private function writeAndroidTheme(): void $primary = $this->normalizeThemeColor(config('nativephp.android.theme.color_primary') ?: '#000000'); $primaryNight = $this->normalizeThemeColor(config('nativephp.android.theme.color_primary_night') ?: '#FFFFFF'); $onPrimary = $this->normalizeThemeColor(config('nativephp.android.theme.color_on_primary') ?: '#FFFFFF'); + $splashBg = $this->normalizeThemeColor(config('nativephp.android.splash.background') ?: '#FFFFFF'); + $splashBgNight = $this->normalizeThemeColor(config('nativephp.android.splash.background_night') ?: '#000000'); File::ensureDirectoryExists(dirname($valuesNightPath)); - $this->components->task('Applying Android theme', function () use ($valuesPath, $valuesNightPath, $primary, $primaryNight, $onPrimary) { - File::put($valuesPath, $this->renderThemeXml($primary, $onPrimary)); - File::put($valuesNightPath, $this->renderThemeXml($primaryNight, $onPrimary)); + $this->components->task('Applying Android theme', function () use ($valuesPath, $valuesNightPath, $primary, $primaryNight, $onPrimary, $splashBg, $splashBgNight) { + File::put($valuesPath, $this->renderThemeXml($primary, $onPrimary, $splashBg)); + File::put($valuesNightPath, $this->renderThemeXml($primaryNight, $onPrimary, $splashBgNight)); return true; }); @@ -89,7 +91,7 @@ private function normalizeThemeColor(string $value): string return '#'.(strlen($hex) === 6 ? 'FF'.$hex : $hex); } - private function renderThemeXml(string $primary, string $onPrimary): string + private function renderThemeXml(string $primary, string $onPrimary, string $splashBackground): string { return << @@ -100,12 +102,18 @@ private function renderThemeXml(string $primary, string $onPrimary): string {$onPrimary} {$primary} {$primary} + {$splashBackground} true @android:color/transparent @android:color/transparent false false + XML; diff --git a/src/Traits/InstallsAndroidSplashScreen.php b/src/Traits/InstallsAndroidSplashScreen.php index a04b3c80..af889d90 100644 --- a/src/Traits/InstallsAndroidSplashScreen.php +++ b/src/Traits/InstallsAndroidSplashScreen.php @@ -23,7 +23,8 @@ public function installAndroidSplashScreen(): void $this->logToFile(' Dark splash (splash-dark.png): '.($hasDarkSplash ? 'found' : 'not found')); if (! $hasLightSplash && ! $hasDarkSplash) { - $this->logToFile(' No splash screens found, skipping'); + $this->logToFile(' No splash images found, using app icon as splash drawable'); + $this->writeAppIconSplashFallback(); return; } @@ -38,6 +39,12 @@ public function installAndroidSplashScreen(): void 'xxxhdpi' => [1280, 1920], ]; + // Density PNGs replace the placeholder; same-folder splash.xml + splash.png is an AAPT2 error + $placeholderXml = base_path('nativephp/android/app/src/main/res/drawable/splash.xml'); + if (File::exists($placeholderXml)) { + File::delete($placeholderXml); + } + if ($hasLightSplash && $this->validateSplashImage($lightSplashPath)) { $this->logToFile(' Generating light splash variants...'); foreach ($sizes as $density => $dimensions) { @@ -93,4 +100,35 @@ private function validateSplashImage(string $splashPath): bool return true; } + + private function writeAppIconSplashFallback(): void + { + $resDir = base_path('nativephp/android/app/src/main/res/'); + $drawableDir = $resDir.'drawable'; + File::ensureDirectoryExists($drawableDir); + + // Remove placeholder vector so splash.xml and splash.png don't coexist (AAPT2 error) + $placeholder = $drawableDir.DIRECTORY_SEPARATOR.'splash.xml'; + if (File::exists($placeholder)) { + File::delete($placeholder); + } + + $densities = ['mipmap-xxxhdpi', 'mipmap-xxhdpi', 'mipmap-xhdpi', 'mipmap-hdpi', 'mipmap-mdpi']; + foreach ($densities as $density) { + $iconPath = $resDir.$density.'/ic_launcher_foreground.png'; + if (File::exists($iconPath)) { + @copy($iconPath, $drawableDir.'/splash.png'); + + return; + } + } + + // No icon found — write a 1×1 transparent PNG so resource linking succeeds + $img = imagecreatetruecolor(1, 1); + imagesavealpha($img, true); + $transparent = imagecolorallocatealpha($img, 0, 0, 0, 127); + imagefill($img, 0, 0, $transparent); + imagepng($img, $drawableDir.'/splash.png'); + imagedestroy($img); + } } diff --git a/src/Traits/InstallsSplashScreen.php b/src/Traits/InstallsSplashScreen.php index b1a8d47a..fc28df6b 100644 --- a/src/Traits/InstallsSplashScreen.php +++ b/src/Traits/InstallsSplashScreen.php @@ -8,6 +8,10 @@ trait InstallsSplashScreen { public function installIosSplashScreen() { + if ($this->installIosSvgSplash()) { + return; + } + // Define supported splash screen variants $splashVariants = [ 'splash.png' => ['filename' => 'splash.png', 'idiom' => 'universal'], @@ -146,4 +150,58 @@ private function validateIosSplashScreen(string $splashPath, string $filename = return true; } + + private function installIosSvgSplash(): bool + { + $svgPath = public_path('splash.svg'); + if (! File::exists($svgPath) || ! $this->isValidSvg($svgPath)) { + return false; + } + + $launchImageDir = base_path('nativephp/ios/NativePHP/Assets.xcassets/LaunchImage.imageset'); + File::ensureDirectoryExists($launchImageDir); + + $images = [['idiom' => 'universal', 'filename' => 'splash.svg']]; + @copy($svgPath, $launchImageDir.'/splash.svg'); + + $darkSvg = public_path('splash-dark.svg'); + if (File::exists($darkSvg) && $this->isValidSvg($darkSvg)) { + @copy($darkSvg, $launchImageDir.'/splash-dark.svg'); + $images[] = [ + 'idiom' => 'universal', + 'filename' => 'splash-dark.svg', + 'appearances' => [['appearance' => 'luminosity', 'value' => 'dark']], + ]; + } + + $contentsJson = [ + 'images' => $images, + 'info' => ['author' => 'xcode', 'version' => 1], + 'properties' => [ + 'pre-rendered' => true, + 'preserves-vector-representation' => true, + ], + ]; + + File::put( + $launchImageDir.'/Contents.json', + json_encode($contentsJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); + + return true; + } + + private function isValidSvg(string $path): bool + { + $contents = @file_get_contents($path); + if ($contents === false || trim($contents) === '') { + return false; + } + + $previous = libxml_use_internal_errors(true); + $doc = simplexml_load_string($contents); + libxml_use_internal_errors($previous); + + return $doc !== false && strtolower($doc->getName()) === 'svg'; + } } diff --git a/src/Traits/PreparesBuild.php b/src/Traits/PreparesBuild.php index 46f56b51..325730c9 100644 --- a/src/Traits/PreparesBuild.php +++ b/src/Traits/PreparesBuild.php @@ -9,7 +9,7 @@ trait PreparesBuild { - use CleansEnvFile, InstallsAndroidSplashScreen, InstallsAppIcon, PlatformFileOperations; + use CleansEnvFile, InstallsAndroidSplashScreen, InstallsAppIcon, PlatformFileOperations, UpdatesSplashConfiguration; /** * Validate required environment variables for building @@ -155,6 +155,11 @@ protected function updateAndroidConfiguration(): void $this->logToFile(' Updating status bar style: '.config('nativephp.android.status_bar_style', 'auto')); $this->updateStatusBarStyleConfiguration(); + $this->logToFile(' Updating splash style: '.config('nativephp.android.splash.style', 'image')); + $this->updateSplashStyleConfiguration(); + $this->updateSplashThemeIcon(); + $this->updateSplashThemeBackground(); + $this->logToFile(' Updating local properties...'); $sdkPath = config('nativephp.android.android_sdk_path'); if ($sdkPath) { diff --git a/src/Traits/UpdatesSplashConfiguration.php b/src/Traits/UpdatesSplashConfiguration.php new file mode 100644 index 00000000..1b8cc429 --- /dev/null +++ b/src/Traits/UpdatesSplashConfiguration.php @@ -0,0 +1,116 @@ +)@[a-zA-Z]+\/[^<]*(<\/item>)/', + '${1}'.$icon.'${2}', + $content + ); + + File::put($themePath, $content); + } + } + + /** + * Apply the configured splash background to the theme at build time. + * writeAndroidTheme() only runs at install, so this keeps it in sync on rebuilds. + */ + protected function updateSplashThemeBackground(): void + { + $map = [ + base_path('nativephp/android/app/src/main/res/values/themes.xml') => $this->normalizeSplashColor( + config('nativephp.android.splash.background') ?: '#FFFFFF' + ), + base_path('nativephp/android/app/src/main/res/values-night/themes.xml') => $this->normalizeSplashColor( + config('nativephp.android.splash.background_night') ?: '#000000' + ), + ]; + + foreach ($map as $themePath => $color) { + if (! File::exists($themePath)) { + continue; + } + + $content = File::get($themePath); + + // windowBackground tracks the splash color too, so the handoff doesn't flash white + foreach (['windowSplashScreenBackground', 'android:windowBackground'] as $attr) { + $content = preg_replace( + '/()[^<]*(<\/item>)/', + '${1}'.$color.'${2}', + $content + ); + } + + File::put($themePath, $content); + } + } + + /** + * Normalize a hex color to opaque #AARRGGBB; fall back to white on invalid input. + */ + private function normalizeSplashColor(string $value): string + { + if (! preg_match('/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $value)) { + $value = '#FFFFFF'; + } + + $hex = strtoupper(ltrim($value, '#')); + + return '#'.(strlen($hex) === 6 ? 'FF'.$hex : $hex); + } +} diff --git a/tests/Unit/Config/SplashConfigTest.php b/tests/Unit/Config/SplashConfigTest.php new file mode 100644 index 00000000..2717755f --- /dev/null +++ b/tests/Unit/Config/SplashConfigTest.php @@ -0,0 +1,17 @@ +assertSame('image', $config['android']['splash']['style']); + $this->assertSame('#FFFFFF', $config['android']['splash']['background']); + $this->assertSame('#000000', $config['android']['splash']['background_night']); + } +} diff --git a/tests/Unit/Resources/AndroidSplashResourcesTest.php b/tests/Unit/Resources/AndroidSplashResourcesTest.php new file mode 100644 index 00000000..c0c1ecd0 --- /dev/null +++ b/tests/Unit/Resources/AndroidSplashResourcesTest.php @@ -0,0 +1,49 @@ +resource('gradle/libs.versions.toml'); + $this->assertStringContainsString('androidx-core-splashscreen', $toml); + } + + public function test_core_splashscreen_dependency_in_gradle(): void + { + $gradle = $this->resource('app/build.gradle.kts'); + $this->assertStringContainsString('libs.androidx.core.splashscreen', $gradle); + } + + public function test_manifest_uses_splash_theme(): void + { + $manifest = $this->resource('app/src/main/AndroidManifest.xml'); + $this->assertStringContainsString('@style/Theme.AndroidPHP.Splash', $manifest); + } + + public function test_mainactivity_wires_native_splash(): void + { + $kt = $this->resource('app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt'); + $this->assertStringContainsString('installSplashScreen()', $kt); + $this->assertStringContainsString('setKeepOnScreenCondition', $kt); + $this->assertStringContainsString('REPLACE_SPLASH_STYLE', $kt); + } + + public function test_placeholder_splash_drawable_exists_and_is_vector(): void + { + $path = __DIR__.'/../../../resources/androidstudio/app/src/main/res/drawable/splash.xml'; + $this->assertFileExists($path); + + $doc = simplexml_load_string(file_get_contents($path)); + $this->assertNotFalse($doc, 'splash.xml must be valid XML'); + $this->assertEquals('vector', $doc->getName(), 'splash.xml root element must be '); + } +} diff --git a/tests/Unit/Traits/InstallsAndroidSplashScreenTest.php b/tests/Unit/Traits/InstallsAndroidSplashScreenTest.php index 67737d29..4651c25d 100644 --- a/tests/Unit/Traits/InstallsAndroidSplashScreenTest.php +++ b/tests/Unit/Traits/InstallsAndroidSplashScreenTest.php @@ -29,6 +29,13 @@ protected function setUp(): void File::makeDirectory($this->publicPath, 0755, true); File::makeDirectory($this->androidResPath, 0755, true); + // Mirror reality: a placeholder splash.xml ships in the template's drawable/ + File::makeDirectory($this->androidResPath.'/drawable', 0755, true); + File::put( + $this->androidResPath.'/drawable/splash.xml', + '' + ); + // Set up base path for testing app()->setBasePath($this->testProjectPath); } @@ -221,6 +228,27 @@ public function test_skips_invalid_dark_mode_image() } } + public function test_app_icon_fallback_when_no_splash() + { + // no splash.* provided + $this->installAndroidSplashScreen(); + + // Placeholder removed, PNG written — no AAPT2 duplicate + $this->assertFileExists($this->androidResPath.'/drawable/splash.png'); + $this->assertFileDoesNotExist($this->androidResPath.'/drawable/splash.xml'); + } + + public function test_png_path_removes_placeholder_xml() + { + $this->createTestSplashImage(); + + $this->installAndroidSplashScreen(); + + // Density PNGs written; placeholder xml in drawable/ must be gone + $this->assertFileDoesNotExist($this->androidResPath.'/drawable/splash.xml'); + $this->assertFileExists($this->androidResPath.'/drawable-mdpi/splash.png'); + } + /** * Helper method to create a test splash image */ diff --git a/tests/Unit/Traits/InstallsAndroidThemeSplashTest.php b/tests/Unit/Traits/InstallsAndroidThemeSplashTest.php new file mode 100644 index 00000000..23014fc8 --- /dev/null +++ b/tests/Unit/Traits/InstallsAndroidThemeSplashTest.php @@ -0,0 +1,30 @@ +setAccessible(true); + + $xml = $method->invoke($this, '#FF000000', '#FFFFFFFF', '#FFFFFFFF'); + + $this->assertStringContainsString('Theme.AndroidPHP.Splash', $xml); + $this->assertStringContainsString('windowSplashScreenBackground', $xml); + $this->assertStringContainsString('postSplashScreenTheme', $xml); + // Default system-splash icon is the app launcher icon (a centered, circle-masked + // icon) — not the full-bleed @drawable/splash image, which looks wrong in the circle. + $this->assertStringContainsString('@mipmap/ic_launcher', $xml); + $this->assertStringContainsString('#FFFFFFFF', $xml); // background applied + // The post-splash theme's window background matches the splash background so the + // handoff from system splash to content doesn't flash white. + $this->assertStringContainsString('android:windowBackground', $xml); + } +} diff --git a/tests/Unit/Traits/InstallsSplashScreenTest.php b/tests/Unit/Traits/InstallsSplashScreenTest.php new file mode 100644 index 00000000..df847caf --- /dev/null +++ b/tests/Unit/Traits/InstallsSplashScreenTest.php @@ -0,0 +1,55 @@ +projectPath = sys_get_temp_dir().'/nativephp_ios_splash_'.uniqid(); + File::makeDirectory($this->projectPath.'/public', 0755, true); + File::makeDirectory($this->projectPath.'/nativephp/ios/NativePHP/Assets.xcassets', 0755, true); + app()->setBasePath($this->projectPath); + } + + protected function tearDown(): void + { + File::deleteDirectory($this->projectPath); + parent::tearDown(); + } + + public function test_svg_splash_writes_vector_contents_json(): void + { + File::put($this->projectPath.'/public/splash.svg', ''); + + $this->installIosSplashScreen(); + + $imageset = $this->projectPath.'/nativephp/ios/NativePHP/Assets.xcassets/LaunchImage.imageset'; + $this->assertFileExists($imageset.'/splash.svg'); + $json = json_decode(File::get($imageset.'/Contents.json'), true); + + $this->assertTrue($json['properties']['preserves-vector-representation']); + $this->assertSame('splash.svg', $json['images'][0]['filename']); + } + + public function test_svg_takes_precedence_and_skips_png_path(): void + { + File::put($this->projectPath.'/public/splash.svg', ''); + File::put($this->projectPath.'/public/splash.png', 'not-a-real-png'); + + $this->installIosSplashScreen(); + + $imageset = $this->projectPath.'/nativephp/ios/NativePHP/Assets.xcassets/LaunchImage.imageset'; + $this->assertFileExists($imageset.'/splash.svg'); + $this->assertFileDoesNotExist($imageset.'/splash.png'); + } +} diff --git a/tests/Unit/Traits/PreparesBuildSplashTest.php b/tests/Unit/Traits/PreparesBuildSplashTest.php index d6b7bf44..7eda9e87 100644 --- a/tests/Unit/Traits/PreparesBuildSplashTest.php +++ b/tests/Unit/Traits/PreparesBuildSplashTest.php @@ -5,11 +5,12 @@ use Illuminate\Support\Facades\File; use Native\Mobile\Traits\InstallsAndroidSplashScreen; use Native\Mobile\Traits\InstallsAppIcon; +use Native\Mobile\Traits\UpdatesSplashConfiguration; use Tests\TestCase; class PreparesBuildSplashTest extends TestCase { - use InstallsAndroidSplashScreen, InstallsAppIcon; + use InstallsAndroidSplashScreen, InstallsAppIcon, UpdatesSplashConfiguration; protected string $testProjectPath; @@ -58,6 +59,100 @@ public function test_splash_screen_integration_skips_gracefully_without_image() $this->assertFileDoesNotExist($splashPath); } + public function test_replaces_splash_style_token(): void + { + config()->set('nativephp.android.splash.style', 'native'); + + $mainActivity = $this->androidJavaPath().'/MainActivity.kt'; + File::ensureDirectoryExists(dirname($mainActivity)); + File::put($mainActivity, 'private val splashStyle = "REPLACE_SPLASH_STYLE"'); + + $this->updateSplashStyleConfiguration(); + + $this->assertStringContainsString('private val splashStyle = "native"', File::get($mainActivity)); + $this->assertStringNotContainsString('REPLACE_SPLASH_STYLE', File::get($mainActivity)); + } + + public function test_image_style_uses_app_icon(): void + { + config()->set('nativephp.android.splash.style', 'image'); + $this->writeSplashThemes(); + + $this->updateSplashThemeIcon(); + + foreach ($this->splashThemePaths() as $themePath) { + $this->assertStringContainsString( + '@mipmap/ic_launcher', + File::get($themePath) + ); + } + } + + public function test_native_style_uses_app_icon(): void + { + config()->set('nativephp.android.splash.style', 'native'); + $this->writeSplashThemes(); + + $this->updateSplashThemeIcon(); + + foreach ($this->splashThemePaths() as $themePath) { + $this->assertStringContainsString( + '@mipmap/ic_launcher', + File::get($themePath) + ); + } + } + + public function test_build_applies_splash_background_per_mode(): void + { + config()->set('nativephp.android.splash.background', '#123456'); + config()->set('nativephp.android.splash.background_night', '#654321'); + $this->writeSplashThemes(); + + $this->updateSplashThemeBackground(); + + $light = File::get(base_path('nativephp/android/app/src/main/res/values/themes.xml')); + $night = File::get(base_path('nativephp/android/app/src/main/res/values-night/themes.xml')); + + $this->assertStringContainsString('#FF123456', $light); + $this->assertStringContainsString('#FF654321', $night); + + // Post-splash window background tracks the splash background to avoid a white flash + $this->assertStringContainsString('#FF123456', $light); + $this->assertStringContainsString('#FF654321', $night); + } + + private function androidJavaPath(): string + { + return base_path('nativephp/android/app/src/main/java/com/nativephp/mobile/ui'); + } + + private function splashThemePaths(): array + { + return [ + base_path('nativephp/android/app/src/main/res/values/themes.xml'), + base_path('nativephp/android/app/src/main/res/values-night/themes.xml'), + ]; + } + + private function writeSplashThemes(): void + { + $xml = '' + .'' + .'' + .''; + + foreach ($this->splashThemePaths() as $themePath) { + File::ensureDirectoryExists(dirname($themePath)); + File::put($themePath, $xml); + } + } + /** * Create a test splash image */