diff --git a/CHANGELOG.md b/CHANGELOG.md index d943dc5..b106862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.14] - 2026-07-26 + +### Changed + +- iPhone, iPad, and Android now open through a restrained native launch + transition: a compact centered sailboat mark on the platform-appropriate + light or dark background, followed by a short fade as soon as the gateway or + workspace has painted. The previous oversized full-screen artwork and + artificial 1.2-second logo hold are removed. + +### Fixed + +- App Store packaging now safely copies an exported IPA across filesystem + boundaries before its atomic final rename, so clean builds on the retained + APFS release volume no longer fail after a successful Xcode export. + ## [0.0.13] - 2026-07-26 ### Added @@ -380,7 +396,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 notarization, stapled tickets, Gatekeeper verification, persistent Application Support, and isolated Apple container machines. -[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.13...HEAD +[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.14...HEAD +[0.0.14]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.14 [0.0.13]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.13 [0.0.12]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.12 [0.0.11]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.11 diff --git a/README.md b/README.md index 66728da..4a34c3c 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to | `PORT` | `8123` | HTTP/WebSocket control-plane port. | | `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and narrow workspace mirrors. | | `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `lxc` on Linux, `wsl` on Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. | -| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.13` | Versioned channel-machine image contract. | +| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.14` | Versioned channel-machine image contract. | ### Agent-first JSON CLI diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/android/app/src/main/res/drawable-land-hdpi/splash.png index 5ca6e09..90c0b67 100644 Binary files a/android/app/src/main/res/drawable-land-hdpi/splash.png and b/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/android/app/src/main/res/drawable-land-mdpi/splash.png index ddaee7f..cd0921a 100644 Binary files a/android/app/src/main/res/drawable-land-mdpi/splash.png and b/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android/app/src/main/res/drawable-land-xhdpi/splash.png index c82d23f..48df226 100644 Binary files a/android/app/src/main/res/drawable-land-xhdpi/splash.png and b/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxhdpi/splash.png index 3dc63d8..b810bde 100644 Binary files a/android/app/src/main/res/drawable-land-xxhdpi/splash.png and b/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png index 51c58db..0992bff 100644 Binary files a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png and b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-nodpi/splash_android12.png b/android/app/src/main/res/drawable-nodpi/splash_android12.png new file mode 100644 index 0000000..be3ff8a Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/splash_android12.png differ diff --git a/android/app/src/main/res/drawable-nodpi/splash_mark.png b/android/app/src/main/res/drawable-nodpi/splash_mark.png new file mode 100644 index 0000000..44f73fa Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/splash_mark.png differ diff --git a/android/app/src/main/res/drawable-port-hdpi/splash.png b/android/app/src/main/res/drawable-port-hdpi/splash.png index 4f9762d..379bde2 100644 Binary files a/android/app/src/main/res/drawable-port-hdpi/splash.png and b/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-mdpi/splash.png b/android/app/src/main/res/drawable-port-mdpi/splash.png index dfd3f08..c6d12a2 100644 Binary files a/android/app/src/main/res/drawable-port-mdpi/splash.png and b/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xhdpi/splash.png b/android/app/src/main/res/drawable-port-xhdpi/splash.png index c69f2da..b9101ef 100644 Binary files a/android/app/src/main/res/drawable-port-xhdpi/splash.png and b/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxhdpi/splash.png index 6aa7c84..71a229e 100644 Binary files a/android/app/src/main/res/drawable-port-xxhdpi/splash.png and b/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png index 30606b9..4bd51ac 100644 Binary files a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png and b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable/splash.png b/android/app/src/main/res/drawable/splash.png index ddaee7f..cd0921a 100644 Binary files a/android/app/src/main/res/drawable/splash.png and b/android/app/src/main/res/drawable/splash.png differ diff --git a/android/app/src/main/res/layout/launch_screen.xml b/android/app/src/main/res/layout/launch_screen.xml new file mode 100644 index 0000000..f5edec7 --- /dev/null +++ b/android/app/src/main/res/layout/launch_screen.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..e245963 --- /dev/null +++ b/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #0B0C0F + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..19ac3cb --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #F2EFE7 + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 2d4726a..e45be59 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -21,8 +21,9 @@ diff --git a/capacitor.config.json b/capacitor.config.json index 679d899..83bf98e 100644 --- a/capacitor.config.json +++ b/capacitor.config.json @@ -38,11 +38,12 @@ "resizeOnFullScreen": true }, "SplashScreen": { - "launchShowDuration": 1200, - "launchAutoHide": true, - "backgroundColor": "#08090c", + "launchShowDuration": 500, + "launchAutoHide": false, + "launchFadeOutDuration": 180, "showSpinner": false, - "androidScaleType": "CENTER_CROP" + "androidScaleType": "CENTER_INSIDE", + "layoutName": "launch_screen" }, "StatusBar": { "overlaysWebView": true, diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 6074b23..5f6dd6a 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -300,14 +300,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.0.13; + MARKETING_VERSION = 0.0.14; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.gitcommit90.onehelm.mobile; PRODUCT_NAME = 1Helm; @@ -322,14 +322,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.0.13; + MARKETING_VERSION = 0.0.14; PRODUCT_BUNDLE_IDENTIFIER = com.gitcommit90.onehelm.mobile; PRODUCT_NAME = 1Helm; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/ios/App/App/Assets.xcassets/LaunchBackground.colorset/Contents.json b/ios/App/App/Assets.xcassets/LaunchBackground.colorset/Contents.json new file mode 100644 index 0000000..ed2f54c --- /dev/null +++ b/ios/App/App/Assets.xcassets/LaunchBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.906", + "green" : "0.937", + "red" : "0.949" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.059", + "green" : "0.047", + "red" : "0.043" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png index 3456817..7ee7d69 100644 Binary files a/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png and b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png differ diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png index 3456817..7ee7d69 100644 Binary files a/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png and b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png differ diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png index 3456817..7ee7d69 100644 Binary files a/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png and b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png differ diff --git a/ios/App/App/Base.lproj/LaunchScreen.storyboard b/ios/App/App/Base.lproj/LaunchScreen.storyboard index e7ae5d7..a681fd3 100644 --- a/ios/App/App/Base.lproj/LaunchScreen.storyboard +++ b/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -12,11 +12,24 @@ - + - - + + + + + + + + + + + + + + + @@ -24,9 +37,9 @@ - - - - + + + + diff --git a/package-lock.json b/package-lock.json index 199def0..65e4408 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "1helm", - "version": "0.0.13", + "version": "0.0.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "1helm", - "version": "0.0.13", + "version": "0.0.14", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { diff --git a/package.json b/package.json index 719076a..5d4ba5b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "1helm", "productName": "1Helm", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "license": "AGPL-3.0-only", diff --git a/public/index.html b/public/index.html index 64b022a..ae9a038 100644 --- a/public/index.html +++ b/public/index.html @@ -34,6 +34,6 @@
- + diff --git a/scripts/generate-mobile-assets.mjs b/scripts/generate-mobile-assets.mjs index bc018fe..86a1492 100644 --- a/scripts/generate-mobile-assets.mjs +++ b/scripts/generate-mobile-assets.mjs @@ -5,21 +5,36 @@ import sharp from "sharp"; const root = resolve(import.meta.dirname, ".."); const source = join(root, "desktop", "icons", "1helm-macos-app-logo.jpg"); -const dark = { r: 8, g: 9, b: 12, alpha: 1 }; async function icon(target, size) { await mkdir(dirname(target), { recursive: true }); await sharp(source).resize(size, size, { fit: "cover" }).png({ compressionLevel: 9 }).toFile(target); } -async function splash(target, width, height) { - const markSize = Math.round(Math.min(width, height) * 0.34); - const mark = await sharp(source) - .resize(markSize, markSize, { fit: "cover" }) +async function markBuffer(size) { + const radius = Math.round(size * 0.22); + const mask = Buffer.from(``); + const artwork = await sharp(source).resize(size, size, { fit: "cover" }).png().toBuffer(); + return sharp({ create: { width: size, height: size, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }) + .composite([{ input: artwork }, { input: mask, blend: "dest-in" }]) .png({ compressionLevel: 9 }) .toBuffer(); +} + +async function launchMark(target, canvasSize, markSize = canvasSize) { + const mark = await markBuffer(markSize); + await mkdir(dirname(target), { recursive: true }); + await sharp({ create: { width: canvasSize, height: canvasSize, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }) + .composite([{ input: mark, left: Math.round((canvasSize - markSize) / 2), top: Math.round((canvasSize - markSize) / 2) }]) + .png({ compressionLevel: 9 }) + .toFile(target); +} + +async function legacySplash(target, width, height) { + const markSize = Math.round(Math.min(width, height) * 0.18); + const mark = await markBuffer(markSize); await mkdir(dirname(target), { recursive: true }); - await sharp({ create: { width, height, channels: 4, background: dark } }) + await sharp({ create: { width, height, channels: 4, background: { r: 242, g: 239, b: 231, alpha: 1 } } }) .composite([{ input: mark, left: Math.round((width - markSize) / 2), top: Math.round((height - markSize) / 2) }]) .png({ compressionLevel: 9 }) .toFile(target); @@ -47,12 +62,14 @@ const androidSplashes = { "drawable-port-xxhdpi/splash.png": [960, 1600], "drawable-port-xxxhdpi/splash.png": [1280, 1920], }; -for (const [relative, [width, height]] of Object.entries(androidSplashes)) await splash(join(androidRes, relative), width, height); +for (const [relative, [width, height]] of Object.entries(androidSplashes)) await legacySplash(join(androidRes, relative), width, height); +await launchMark(join(androidRes, "drawable-nodpi", "splash_mark.png"), 256); +await launchMark(join(androidRes, "drawable-nodpi", "splash_android12.png"), 256, 96); const iosAssets = join(root, "ios", "App", "App", "Assets.xcassets"); await icon(join(iosAssets, "AppIcon.appiconset", "AppIcon-512@2x.png"), 1024); for (const filename of await readdir(join(iosAssets, "Splash.imageset"))) { - if (filename.endsWith(".png")) await splash(join(iosAssets, "Splash.imageset", filename), 2732, 2732); + if (filename.endsWith(".png")) await launchMark(join(iosAssets, "Splash.imageset", filename), 512); } -console.log("Generated branded iOS and Android icons and launch screens."); +console.log("Generated branded mobile icons and compact launch marks."); diff --git a/scripts/package-ios-ipa.mjs b/scripts/package-ios-ipa.mjs index 37bed4d..8de1c6b 100644 --- a/scripts/package-ios-ipa.mjs +++ b/scripts/package-ios-ipa.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { mkdtemp, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"; +import { copyFile, mkdtemp, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -37,7 +37,9 @@ try { run("xcodebuild", ["-exportArchive", "-archivePath", archive, "-exportPath", exported, "-exportOptionsPlist", options, "-allowProvisioningUpdates"]); const ipaName = (await readdir(exported)).find((name) => name.endsWith(".ipa")); if (!ipaName) throw new Error("Xcode did not produce an IPA."); - await rename(join(exported, ipaName), candidate); + // Xcode's temp directory and a release volume can be different filesystems. + // Copy into the release directory before the same-volume atomic rename. + await copyFile(join(exported, ipaName), candidate); const entries = spawnSync("unzip", ["-Z1", candidate], { encoding: "utf8" }); if (entries.status !== 0 || /(^|\n)(?:AGENTS\.md|.*\/AGENTS\.md)(?:\n|$)/.test(entries.stdout || "")) throw new Error("The IPA is invalid or contains host-local instructions."); await rm(canonical, { force: true }); diff --git a/src/client/app.ts b/src/client/app.ts index 298a716..f9c765d 100644 --- a/src/client/app.ts +++ b/src/client/app.ts @@ -6,7 +6,7 @@ import { openRoutingPopover, pushRoutingActivity } from "./routing.ts"; import { openOnboarding } from "./onboarding.ts"; import { defaultTerminalComputer, openTerminals, refitChannelTerminals, getTerminalChrome } from "./term.ts"; import { openCreateChannel, renderActivity, renderBoard, renderChannelSettings, renderFiles, renderGlobalThreads, renderMemory, renderNotes, renderTexts, renderThreads, type ChannelView } from "./channel.ts"; -import { apiUrl, forgetMobileServer, getServerOrigin, isNativeMobile, serverAssetUrl, setMobileServer } from "./mobile.ts"; +import { apiUrl, finishNativeLaunch, forgetMobileServer, getServerOrigin, isNativeMobile, serverAssetUrl, setMobileServer } from "./mobile.ts"; /** Per-channel layout bound to the user profile (server user_ui_state). */ type ChannelUiView = { @@ -698,6 +698,7 @@ function renderAuth(): void { pw.addEventListener("keydown", (ev) => { if ((ev as KeyboardEvent).key === "Enter") submit(); }); (isNativeMobile() && !server.value ? server : u).focus(); if (!isNativeMobile()) void renderAccessRequest(access); + finishNativeLaunch(); } async function renderAccessRequest(container: HTMLElement): Promise { @@ -771,6 +772,7 @@ export function renderApp(): void { focusedNote.node.focus({ preventScroll: true }); focusedNote.node.setSelectionRange(focusedNote.start, focusedNote.end); } + finishNativeLaunch(); } function setMobileMenu(open: boolean): void { @@ -3128,4 +3130,5 @@ void initializeApiTransport().then(() => boot()).catch((error) => { h("h1", { class: "font-display text-2xl text-fg" }, "1Helm could not open"), h("p", { class: "text-sm leading-6 text-muted" }, (error as Error).message || "The device security store is unavailable."), h("button", { class: "btn-primary", onclick: () => location.reload() }, "Try again")))); + finishNativeLaunch(); }); diff --git a/src/client/mobile.ts b/src/client/mobile.ts index cda41c5..711e9bb 100644 --- a/src/client/mobile.ts +++ b/src/client/mobile.ts @@ -3,6 +3,7 @@ import { Browser } from "@capacitor/browser"; import { Capacitor } from "@capacitor/core"; import { Keyboard, KeyboardResize } from "@capacitor/keyboard"; import { SecureStorage, KeychainAccess } from "@aparajita/capacitor-secure-storage"; +import { SplashScreen } from "@capacitor/splash-screen"; import { StatusBar, Style } from "@capacitor/status-bar"; const SESSION_KEY = "session"; @@ -14,11 +15,21 @@ type StoredSession = { server: string; token: string }; const native = Capacitor.isNativePlatform(); let serverOrigin = ""; +let launchFinishQueued = false; export const isNativeMobile = (): boolean => native; export const mobilePlatform = (): string => native ? Capacitor.getPlatform() : "web"; export const getServerOrigin = (): string => serverOrigin; +/** Reveal the first real native screen only after the WebView has painted it. */ +export function finishNativeLaunch(): void { + if (!native || launchFinishQueued) return; + launchFinishQueued = true; + requestAnimationFrame(() => requestAnimationFrame(() => { + void SplashScreen.hide({ fadeOutDuration: 180 }).catch(() => undefined); + })); +} + export function normalizeServerOrigin(value: string): string { const raw = value.trim(); if (!raw) throw new Error("Enter your 1Helm server address."); diff --git a/src/server/channel-computers.ts b/src/server/channel-computers.ts index d8ca57d..85dc3fb 100644 --- a/src/server/channel-computers.ts +++ b/src/server/channel-computers.ts @@ -67,7 +67,7 @@ const APPLE_RUNTIME_VERSION = "1.1.0"; export const APPLE_RUNTIME_PACKAGE = `container-${APPLE_RUNTIME_VERSION}-installer-signed.pkg`; export const APPLE_RUNTIME_URL = `https://github.com/apple/container/releases/download/${APPLE_RUNTIME_VERSION}/${APPLE_RUNTIME_PACKAGE}`; export const APPLE_RUNTIME_SHA256 = "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714"; -export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.13"; +export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.14"; const CONTAINER_CANDIDATES = [process.env.HELM_CONTAINER_CLI, "/usr/local/bin/container", "/opt/homebrew/bin/container", "container"].filter(Boolean) as string[]; const LXC_RUNTIME_VERSION = "1helm-lxc-runtime-v1"; const LXC_HELPER_CANDIDATES = [ diff --git a/src/server/db.ts b/src/server/db.ts index feb7fd2..7335063 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -979,7 +979,7 @@ export function migrate(): void { const platformBackend = process.platform === "darwin" ? "apple" : process.platform === "win32" ? "wsl" : "lxc"; const configuredBackend = String(process.env.HELM_CHANNEL_COMPUTER_BACKEND || platformBackend); const backend = ["apple", "lxc", "wsl", "native", "mock"].includes(configuredBackend) ? configuredBackend : platformBackend; - const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.13"); + const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.14"); // Earlier Linux/Windows releases persisted the compatibility `native` // seam into every channel row. A production host update must actually // move those rows onto the platform isolation backend; changing the unit's diff --git a/test/channel-computers.mjs b/test/channel-computers.mjs index e4562c0..be94c5c 100644 --- a/test/channel-computers.mjs +++ b/test/channel-computers.mjs @@ -168,7 +168,7 @@ test("Apple channel-computer contract preserves isolation, files, wakes, archive test("runtime digest and packaged image recipe stay pinned", async () => { assert.equal(computers.APPLE_RUNTIME_SHA256, "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714"); assert.match(computers.APPLE_RUNTIME_URL, /\/1\.1\.0\/container-1\.1\.0-installer-signed\.pkg$/); - assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.13"); + assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.14"); const packaging = await readFile(join(root, "scripts", "package-mac-dmg.cjs"), "utf8"); assert.match(packaging, /container\(\?:\$\|\\\/\)/, "release packaging includes container/ image assets"); const image = await readFile(join(root, "container", "Containerfile"), "utf8"); diff --git a/test/mobile.mjs b/test/mobile.mjs index 7dd8436..d05f6e6 100644 --- a/test/mobile.mjs +++ b/test/mobile.mjs @@ -87,10 +87,11 @@ test("mobile compatibility is explicit and CORS is confined to packaged Capacito }); test("Capacitor shells keep sessions native, connections HTTPS-only, and release identities stable", async () => { - const [config, mobile, api, app, androidManifest, androidBuild, androidRules, androidPackage, iosInfo, iosProject, privacy, packageJson] = await Promise.all([ + const [config, mobile, api, app, androidManifest, androidBuild, androidRules, androidPackage, androidStyles, androidLaunch, iosInfo, iosProject, iosLaunch, privacy, packageJson, iosPackage] = await Promise.all([ read("capacitor.config.json"), read("src/client/mobile.ts"), read("src/client/api.ts"), read("src/client/app.ts"), read("android/app/src/main/AndroidManifest.xml"), read("android/app/build.gradle"), read("android/app/src/main/res/xml/data_extraction_rules.xml"), read("scripts/package-android-apk.mjs"), - read("ios/App/App/Info.plist"), read("ios/App/App.xcodeproj/project.pbxproj"), read("ios/App/App/PrivacyInfo.xcprivacy"), read("package.json"), + read("android/app/src/main/res/values/styles.xml"), read("android/app/src/main/res/layout/launch_screen.xml"), read("ios/App/App/Info.plist"), read("ios/App/App.xcodeproj/project.pbxproj"), read("ios/App/App/Base.lproj/LaunchScreen.storyboard"), + read("ios/App/App/PrivacyInfo.xcprivacy"), read("package.json"), read("scripts/package-ios-ipa.mjs"), ]); const parsed = JSON.parse(config); assert.equal(parsed.appId, "com.gitcommit90.onehelm.mobile"); @@ -102,12 +103,19 @@ test("Capacitor shells keep sessions native, connections HTTPS-only, and release assert.equal(parsed.android.webContentsDebuggingEnabled, false); assert.equal(parsed.ios.preferredContentMode, "mobile"); assert.equal(parsed.ios.webContentsDebuggingEnabled, false); + assert.equal(parsed.plugins.SplashScreen.launchAutoHide, false, "native launch art remains only until the first real screen paints"); + assert.ok(parsed.plugins.SplashScreen.launchShowDuration <= 500, "launch has no artificial logo hold"); + assert.equal(parsed.plugins.SplashScreen.launchFadeOutDuration, 180); + assert.equal(parsed.plugins.SplashScreen.androidScaleType, "CENTER_INSIDE"); + assert.equal(parsed.plugins.SplashScreen.layoutName, "launch_screen"); assert.match(mobile, /SecureStorage/); assert.match(mobile, /KeychainAccess\.whenUnlockedThisDeviceOnly/); assert.match(mobile, /parsed\.protocol !== "https:"/); assert.match(mobile, /persistSecureSession/); assert.match(mobile, /removeSecureSession/); + assert.match(mobile, /SplashScreen\.hide\(\{ fadeOutDuration: 180 \}\)/); + assert.match(mobile, /requestAnimationFrame\(\(\) => requestAnimationFrame/, "launch fades only after the gateway or workspace paints"); assert.match(mobile, /App\.getLaunchUrl/, "a cold-start OAuth callback is retained"); assert.doesNotMatch(mobile, /destination\.origin === serverOrigin/, "server links cannot replace the audited packaged WebView"); assert.doesNotMatch(api, /let token = localStorage\.getItem/, "the session is not eagerly copied out of native secure storage"); @@ -126,6 +134,12 @@ test("Capacitor shells keep sessions native, connections HTTPS-only, and release assert.match(androidRules, /exclude domain="sharedpref"/); assert.match(androidPackage, /7b2d96ab21a242f9b17ddc7c65d133033bb9f0322158b6aab57bf8d46a7d27bf/); assert.match(androidPackage, /expected the permanent 1Helm release certificate/); + assert.match(androidStyles, /windowSplashScreenBackground">@color\/launch_background/); + assert.match(androidStyles, /windowSplashScreenAnimatedIcon">@drawable\/splash_android12/); + assert.doesNotMatch(androidStyles, /android:background">@drawable\/splash/); + assert.match(androidLaunch, /android:layout_width="96dp"/); + assert.match(androidLaunch, /android:layout_height="96dp"/); + assert.match(androidLaunch, /android:src="@drawable\/splash_mark"/); assert.match(iosInfo, /com\.gitcommit90\.onehelm\.mobile/); assert.match(iosInfo, /onehelm<\/string>/); @@ -134,7 +148,12 @@ test("Capacitor shells keep sessions native, connections HTTPS-only, and release assert.match(iosInfo, /ITSAppUsesNonExemptEncryption/); assert.match(iosProject, /PRODUCT_BUNDLE_IDENTIFIER = com\.gitcommit90\.onehelm\.mobile/); assert.match(iosProject, /PrivacyInfo\.xcprivacy in Resources/); + assert.match(iosLaunch, /contentMode="scaleAspectFit"/); + assert.match(iosLaunch, /firstAttribute="width" constant="88"/); + assert.match(iosLaunch, /firstAttribute="height" constant="88"/); + assert.doesNotMatch(iosLaunch, /contentMode="scaleAspectFill"/, "launch mark never fills or crops to the screen"); assert.match(privacy, /NSPrivacyTracking[\s\S]*/); + assert.match(iosPackage, /copyFile\(join\(exported, ipaName\), candidate\)/, "IPA packaging supports a separate APFS release volume"); const pkg = JSON.parse(packageJson); for (const dependency of ["@capacitor/core", "@capacitor/android", "@capacitor/ios", "@aparajita/capacitor-secure-storage"]) assert.ok(pkg.dependencies[dependency]); @@ -142,6 +161,12 @@ test("Capacitor shells keep sessions native, connections HTTPS-only, and release const iosIcon = await sharp(join(root, "ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png")).metadata(); assert.equal(iosIcon.width, 1024); assert.equal(iosIcon.height, 1024); + const iosLaunchMark = await sharp(join(root, "ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png")).metadata(); + assert.equal(iosLaunchMark.width, 512); assert.equal(iosLaunchMark.height, 512); assert.equal(iosLaunchMark.hasAlpha, true); + const androidLaunchMark = await sharp(join(root, "android/app/src/main/res/drawable-nodpi/splash_mark.png")).metadata(); + assert.equal(androidLaunchMark.width, 256); assert.equal(androidLaunchMark.height, 256); assert.equal(androidLaunchMark.hasAlpha, true); + const android12LaunchMark = await sharp(join(root, "android/app/src/main/res/drawable-nodpi/splash_android12.png")).metadata(); + assert.equal(android12LaunchMark.width, 256); assert.equal(android12LaunchMark.height, 256); assert.equal(android12LaunchMark.hasAlpha, true); assert.ok((await stat(join(root, "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png"))).size > 10_000); });