From a9dd46f98f6ac9d3f3ff8a0f21b478842d3a3a32 Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Fri, 14 Aug 2026 21:04:10 +0200 Subject: [PATCH] FIX - Inline App Store bundle; CEF-compatible CSS Make the App Store part of the phone shell (remove preload helper/loadAppStoreComponent and App.vue preload) by importing AppStoreApp in PhoneAppWindow and selecting it when app.id === 'app-store'. Update the apps registry to use an inline import for the async component. Replace unsupported CSS features (:has, color-mix) with CEF-safe alternatives using semantic tokens (--sky-app-accent-soft, --sky-danger-soft) and add explicit .store-action-button/.store-action-button--icon and .store-detail__action classes. Add contract tests to assert the App Store is bundled with the shell and that styles remain compatible with the FiveM CEF target. --- frontend/src/App.vue | 5 +- frontend/src/config/apps.test.ts | 10 +-- frontend/src/config/apps.ts | 17 +---- .../src/views/PhoneAppWindow.contract.test.ts | 19 ++++++ frontend/src/views/PhoneAppWindow.vue | 6 +- .../apps/AppStoreAction.contract.test.ts | 1 + frontend/src/views/apps/AppStoreAction.vue | 6 +- .../views/apps/AppStoreApp.contract.test.ts | 14 +++-- frontend/src/views/apps/AppStoreApp.vue | 63 +++++++++++++------ .../apps/AppStoreDetail.contract.test.ts | 2 + frontend/src/views/apps/AppStoreDetail.vue | 4 +- 11 files changed, 90 insertions(+), 57 deletions(-) create mode 100644 frontend/src/views/PhoneAppWindow.contract.test.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 10e7e15..8fb380c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -47,7 +47,7 @@ import { useMarketplaceStore } from '@/stores/marketplace' import { useAppCatalogStore } from '@/stores/app-catalog' import { useAppStoreStore } from '@/stores/app-store' import { useWidgetsStore } from '@/stores/widgets' -import { isPhoneAppId, loadAppStoreComponent } from '@/config/apps' +import { isPhoneAppId } from '@/config/apps' import { useNotesStore } from '@/stores/notes' import { useMemosStore } from '@/stores/memos' import { useWeatherStore } from '@/stores/weather' @@ -1223,9 +1223,6 @@ onMounted(() => { window.addEventListener('resize', updateViewportScale) systemColorScheme.addEventListener('change', onSystemColorSchemeChange) phone.setSystemDarkMode(systemColorScheme.matches) - void loadAppStoreComponent().catch((error: unknown) => { - console.error('[App Store] Could not preload the phone app.', error) - }) void nuiCall('ui:ready', { protocolVersion: 1 }) clockTicker = setInterval(() => { const now = Date.now() diff --git a/frontend/src/config/apps.test.ts b/frontend/src/config/apps.test.ts index 6feca84..09fd166 100644 --- a/frontend/src/config/apps.test.ts +++ b/frontend/src/config/apps.test.ts @@ -1,16 +1,8 @@ import { describe, expect, it } from 'vitest' import { Newspaper } from 'lucide-vue-next' -import { isPhoneAppId, loadAppStoreComponent, PHONE_APPS } from './apps' +import { isPhoneAppId, PHONE_APPS } from './apps' describe('app registry', () => { - it('reuses the App Store module loaded by the phone shell', async () => { - const firstLoad = loadAppStoreComponent() - const secondLoad = loadAppStoreComponent() - - expect(secondLoad).toBe(firstLoad) - expect((await firstLoad).default).toBeTruthy() - }) - it('has unique ids and routes with the reference dock order', () => { expect(new Set(PHONE_APPS.map((app) => app.id)).size).toBe( PHONE_APPS.length, diff --git a/frontend/src/config/apps.ts b/frontend/src/config/apps.ts index f3a211d..dd63476 100644 --- a/frontend/src/config/apps.ts +++ b/frontend/src/config/apps.ts @@ -86,19 +86,6 @@ import type { PhoneAppDefinition, } from '@/types/apps' -let appStoreComponentPromise: - | ReturnType - | null = null - -function importAppStoreComponent() { - return import('@/views/apps/AppStoreApp.vue') -} - -export function loadAppStoreComponent() { - appStoreComponentPromise ??= importAppStoreComponent() - return appStoreComponentPromise -} - export const PHONE_APPS = shallowReactive([ { category: 'social', @@ -494,7 +481,9 @@ export const PHONE_APPS = shallowReactive([ }, { category: 'utilities', - component: markRaw(defineAsyncComponent(loadAppStoreComponent)), + component: markRaw( + defineAsyncComponent(() => import('@/views/apps/AppStoreApp.vue')), + ), dockOrder: null, gridOrder: 9, icon: markRaw(ShoppingBag), diff --git a/frontend/src/views/PhoneAppWindow.contract.test.ts b/frontend/src/views/PhoneAppWindow.contract.test.ts new file mode 100644 index 0000000..248ef5a --- /dev/null +++ b/frontend/src/views/PhoneAppWindow.contract.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from 'node:fs' + +import { describe, expect, it } from 'vitest' + +const source = readFileSync( + new URL('./PhoneAppWindow.vue', import.meta.url), + 'utf8', +) + +describe('PhoneAppWindow bundle contract', () => { + it('loads the App Store from the phone shell instead of a delayed chunk', () => { + expect(source).toContain( + "import AppStoreApp from '@/views/apps/AppStoreApp.vue'", + ) + expect(source).toContain("app.value?.id === 'app-store'") + expect(source).toContain('? AppStoreApp : app.value?.component') + expect(source).toContain('') + }) +}) diff --git a/frontend/src/views/PhoneAppWindow.vue b/frontend/src/views/PhoneAppWindow.vue index 9bdca8b..d1d9bd6 100644 --- a/frontend/src/views/PhoneAppWindow.vue +++ b/frontend/src/views/PhoneAppWindow.vue @@ -6,10 +6,14 @@ import CustomAppFrame from '@/components/CustomAppFrame.vue' import { getPhoneApp, isExternalPhoneApp } from '@/config/apps' import { usePhoneStore } from '@/stores/phone' import { getCustomAppFrameKey } from '@/utils/customAppLifecycle' +import AppStoreApp from '@/views/apps/AppStoreApp.vue' const route = useRoute() const phone = usePhoneStore() const app = computed(() => getPhoneApp(route.params.appId)) +const builtinAppComponent = computed(() => + app.value?.id === 'app-store' ? AppStoreApp : app.value?.component, +) const launchStyle = computed(() => { const origin = phone.launchOrigin return { @@ -30,7 +34,7 @@ const launchStyle = computed(() => { :app="app" /> - + diff --git a/frontend/src/views/apps/AppStoreAction.contract.test.ts b/frontend/src/views/apps/AppStoreAction.contract.test.ts index 011c803..295e9ef 100644 --- a/frontend/src/views/apps/AppStoreAction.contract.test.ts +++ b/frontend/src/views/apps/AppStoreAction.contract.test.ts @@ -25,5 +25,6 @@ describe('AppStoreAction contract', () => { expect(source).toMatch( /\.app-store-action__progress i\s*\{[^}]*width:\s*7px/s, ) + expect(source).not.toContain('color-mix(') }) }) diff --git a/frontend/src/views/apps/AppStoreAction.vue b/frontend/src/views/apps/AppStoreAction.vue index 15ef053..98ae513 100644 --- a/frontend/src/views/apps/AppStoreAction.vue +++ b/frontend/src/views/apps/AppStoreAction.vue @@ -50,9 +50,7 @@ const phone = usePhoneStore() } .app-store-action__download { - filter: drop-shadow( - 0 2px 5px color-mix(in srgb, var(--sky-app-accent) 30%, transparent) - ); + filter: drop-shadow(0 2px 5px var(--sky-app-accent-soft)); } .app-store-action__progress { @@ -74,7 +72,7 @@ const phone = usePhoneStore() } .app-store-action__track { - stroke: color-mix(in srgb, var(--sky-app-accent) 24%, transparent); + stroke: var(--sky-app-accent-soft); } .app-store-action__value { diff --git a/frontend/src/views/apps/AppStoreApp.contract.test.ts b/frontend/src/views/apps/AppStoreApp.contract.test.ts index 2d68d5f..1bb9de5 100644 --- a/frontend/src/views/apps/AppStoreApp.contract.test.ts +++ b/frontend/src/views/apps/AppStoreApp.contract.test.ts @@ -139,9 +139,7 @@ describe('AppStoreApp Sky navigation contract', () => { expect(source).toMatch( /button:not\(\.store-ranking__detail-link\):not\(:disabled\):hover\)\s*\{[^}]*brightness\(1\.08\)[^}]*translateY\(-1px\)/s, ) - expect(source).toContain( - '.app-store-page :deep(button:has(.app-store-action--icon):hover)', - ) + expect(source).toContain('.app-store-page .store-action-button--icon:hover') expect(source).toContain( '.store-account__primary-action:not(:disabled):hover', ) @@ -175,9 +173,7 @@ describe('AppStoreApp Sky navigation contract', () => { expect(source).toContain('@click="openAppDetail(finalHighlight)"') expect(source).toContain('@click.stop="handleApp(dailyHighlights[0])"') expect(source).toContain('@click.stop="handleApp(finalHighlight)"') - expect(source).toContain( - '.store-highlight:has(.store-highlight__detail-link:hover)', - ) + expect(source).toContain('.store-highlight:hover') }) it('opens Top Today apps while keeping their direct app actions separate', () => { @@ -193,6 +189,12 @@ describe('AppStoreApp Sky navigation contract', () => { ) }) + it('keeps App Store actions compatible with the FiveM CEF target', () => { + expect(source.match(/class="store-action-button"/g)).toHaveLength(8) + expect(source).not.toContain(':has(') + expect(source).not.toContain('color-mix(') + }) + it('builds clean Apps and Games pages with rotating features', () => { expect(source).not.toContain('class="store-browse__filters"') expect(source).not.toContain('browseFilter') diff --git a/frontend/src/views/apps/AppStoreApp.vue b/frontend/src/views/apps/AppStoreApp.vue index a873252..8e26c62 100644 --- a/frontend/src/views/apps/AppStoreApp.vue +++ b/frontend/src/views/apps/AppStoreApp.vue @@ -484,6 +484,11 @@ watch(