From f390e610b999727c1e5381715bf95356a10fb6ba Mon Sep 17 00:00:00 2001 From: Hampus Date: Fri, 9 Oct 2026 01:38:34 +0200 Subject: [PATCH] fix(tooling): repair test, i18n and docs verification gates (#3331) --- .github/workflows/i18n-source-sync.yaml | 2 + .github/workflows/tests.yaml | 4 +- .gitignore | 2 + .../api/admin/tests/AdminGuildRoutes.test.ts | 35 +- .../audit_coverage/GuildAdminAuditCases.ts | 2 +- .../guild/tests/GuildMemberManagement.test.ts | 30 +- fluxer_api/src/api/test/NoopGatewayService.ts | 28 +- fluxer_api/vitest.config.ts | 1 + fluxer_app/rspack.config.mjs | 24 + .../components/BootstrapErrorScreen.test.tsx | 22 +- .../components/AllSettingsRenderer.tsx | 11 +- .../layout/NativeWindowControls.tsx | 2 +- .../app/components/setup/SetupWizardSteps.tsx | 7 +- .../hooks/useBottomSheetBackHandler.test.tsx | 4 +- fluxer_app/src/features/app/state/Updater.ts | 69 +- .../commands/AccountSwitcherModalCommands.tsx | 7 +- .../auth/components/InstanceBrandMark.tsx | 10 +- .../auth/flow/HandoffApprovalFlow.tsx | 12 +- .../auth_login_core/AuthLoginBrowserStep.tsx | 43 +- .../channel/components/ChannelMessages.tsx | 8 +- .../components/MentionEveryonePopout.tsx | 28 +- .../UnaddableRecipientsConfirmModal.tsx | 8 +- .../hooks/useComposerShowAllButtons.test.tsx | 9 +- .../src/features/i18n/locales/ar/messages.po | 94 +- .../locales/auto-i18n-reviewed-unchanged.json | 464 ----- .../src/features/i18n/locales/bg/messages.po | 94 +- .../src/features/i18n/locales/cs/messages.po | 94 +- .../src/features/i18n/locales/da/messages.po | 94 +- .../src/features/i18n/locales/de/messages.po | 94 +- .../src/features/i18n/locales/el/messages.po | 94 +- .../features/i18n/locales/en-GB/messages.po | 94 +- .../features/i18n/locales/en-US/messages.po | 94 +- .../features/i18n/locales/es-419/messages.po | 94 +- .../features/i18n/locales/es-ES/messages.po | 94 +- .../src/features/i18n/locales/fi/messages.po | 94 +- .../src/features/i18n/locales/fr/messages.po | 94 +- .../src/features/i18n/locales/he/messages.po | 94 +- .../src/features/i18n/locales/hi/messages.po | 94 +- .../src/features/i18n/locales/hr/messages.po | 94 +- .../src/features/i18n/locales/hu/messages.po | 94 +- .../src/features/i18n/locales/id/messages.po | 94 +- .../src/features/i18n/locales/it/messages.po | 94 +- .../src/features/i18n/locales/ja/messages.po | 94 +- .../src/features/i18n/locales/ko/messages.po | 94 +- .../src/features/i18n/locales/lt/messages.po | 94 +- .../src/features/i18n/locales/nl/messages.po | 94 +- .../src/features/i18n/locales/no/messages.po | 94 +- .../src/features/i18n/locales/pl/messages.po | 94 +- .../features/i18n/locales/pt-BR/messages.po | 94 +- .../src/features/i18n/locales/ro/messages.po | 94 +- .../src/features/i18n/locales/ru/messages.po | 94 +- .../features/i18n/locales/sv-SE/messages.po | 94 +- .../src/features/i18n/locales/th/messages.po | 94 +- .../src/features/i18n/locales/tr/messages.po | 94 +- .../src/features/i18n/locales/uk/messages.po | 94 +- .../src/features/i18n/locales/vi/messages.po | 94 +- .../features/i18n/locales/zh-CN/messages.po | 94 +- .../features/i18n/locales/zh-TW/messages.po | 94 +- .../markdown/renderers/LinkRenderer.tsx | 19 +- .../hooks/useTextareaDraftAndTyping.test.tsx | 9 +- .../commands/MacPermissionsModalCommands.tsx | 24 +- .../useAccountTransitionDismissal.test.tsx | 31 +- .../components/modals/GiftPlutoniumModal.tsx | 13 +- .../plutonium_page/PlutoniumPage.tsx | 126 +- .../PlutoniumPageComparison.tsx | 163 +- .../plutonium_page/PlutoniumPageIcons.tsx | 35 +- .../plutonium_page/PlutoniumPageLinkCard.tsx | 17 +- .../plutonium_page/PlutoniumPageShowcase.tsx | 165 +- .../theme/hooks/useThemeCssVariables.test.tsx | 68 +- .../components/modals/PrivacySetupModal.tsx | 1 + .../components/modals/tabs/UserVoiceTab.tsx | 7 +- .../AudioProcessingModal.tsx | 4 +- fluxer_docs/scripts/VerifyDocsCoverage.ts | 94 +- fluxer_docs/scripts/VerifyDocsSchemas.test.ts | 666 +++++++ fluxer_docs/scripts/VerifyDocsSchemas.ts | 1559 ++++++++++++----- fluxer_docs/scripts/VerifyDocsStyle.ts | 4 +- .../src/content/docs/admin-api/blocklists.mdx | 18 +- .../src/content/docs/admin-api/bulk-jobs.mdx | 1 + .../src/content/docs/admin-api/discovery.mdx | 2 +- .../src/content/docs/admin-api/gateway.mdx | 36 + .../src/content/docs/admin-api/guilds.mdx | 2 + .../src/content/docs/admin-api/index.mdx | 1 + .../src/content/docs/admin-api/instance.mdx | 2 +- .../src/content/docs/admin-api/reports.mdx | 4 + .../src/content/docs/admin-api/users.mdx | 31 +- .../content/docs/http-api/authentication.mdx | 7 +- .../src/content/docs/http-api/billing.mdx | 1 + .../src/content/docs/http-api/instance.mdx | 8 + .../src/content/docs/http-api/invites.mdx | 2 +- .../src/content/docs/http-api/messages.mdx | 7 +- .../src/content/docs/http-api/premium.mdx | 34 +- .../docs/http-api/users/data-harvest.mdx | 4 +- .../content/docs/http-api/users/settings.mdx | 4 +- .../src/content/docs/http-api/webhooks.mdx | 38 +- .../content/docs/operator/configuration.mdx | 2 +- package.json | 2 +- .../i18n/scripts/SyncStaticI18nCatalogs.ts | 52 +- packages/openapi/package.json | 1 + .../src/__tests__/OpenAPIGenerator.test.ts | 6 + packages/schema/proto/buf.gen.yaml | 2 +- pnpm-lock.yaml | 3 + tools/ci/src/app_dev_server.rs | 2 +- tools/ci/src/app_wasm.rs | 100 +- tools/dev/src/tasks.rs | 4 + tools/i18n_auto/src/po.rs | 45 +- tools/i18n_auto/src/reviewed_unchanged.rs | 74 + tools/i18n_auto/src/runner.rs | 620 +++++-- 107 files changed, 5144 insertions(+), 3004 deletions(-) create mode 100644 fluxer_docs/scripts/VerifyDocsSchemas.test.ts diff --git a/.github/workflows/i18n-source-sync.yaml b/.github/workflows/i18n-source-sync.yaml index 40cf2f500..1c6e36a49 100644 --- a/.github/workflows/i18n-source-sync.yaml +++ b/.github/workflows/i18n-source-sync.yaml @@ -61,6 +61,8 @@ jobs: --step install_dependencies - name: Refresh source catalogs + env: + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} run: pnpm i18n:source-sync - name: Format generated catalogs diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 75751cf6d..d4bd8f809 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -476,6 +476,9 @@ jobs: - name: Lint JSX for browser-translation safety run: pnpm exec eslint . --max-warnings 0 + - name: Check data-flx attributes + run: pnpm --filter fluxer_app theming:data-flx:check + i18n: runs-on: ubuntu-24.04 timeout-minutes: 25 @@ -502,7 +505,6 @@ jobs: run: | if ! git diff --exit-code -- \ packages/errors/src/i18n/locales \ - packages/errors/src/i18n/ErrorI18nTypes.generated.ts \ fluxer_api/pkgs/email/src/email_i18n/locales \ fluxer_api/pkgs/email/src/email_i18n/EmailI18nTypes.generated.ts \ fluxer_api/src/api/content_i18n/locales; then diff --git a/.gitignore b/.gitignore index 50fa77bd2..d6c996031 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ /fluxer_app/pkgs/libfluxcore/ /fluxer_app/pkgs/libfluxwebp/ /fluxer_app/src/features/i18n/locales/*/messages.mjs +**/auto-i18n-reviewed-unchanged.json.lock +**/weblate/locales/auto-i18n-reviewed-unchanged.json /fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts /fluxer_app/src/features/theme/styles/generated/ /fluxer_app/src/features/ui/components/SVGMasks.tsx diff --git a/fluxer_api/src/api/admin/tests/AdminGuildRoutes.test.ts b/fluxer_api/src/api/admin/tests/AdminGuildRoutes.test.ts index b49437c8c..c91da8297 100644 --- a/fluxer_api/src/api/admin/tests/AdminGuildRoutes.test.ts +++ b/fluxer_api/src/api/admin/tests/AdminGuildRoutes.test.ts @@ -25,6 +25,13 @@ interface AdminGuildUpdate { }; } +interface AdminGuildMemberList { + members: Array<{user: {id: string}}>; + total: number; + limit: number; + offset: number; +} + describe('Admin guild routes', () => { let harness: ApiTestHarness; beforeEach(async () => { @@ -123,19 +130,33 @@ describe('Admin guild routes', () => { }); test('GET /admin/guilds/{guild_id}/members lists members', async () => { const admin = await createTestAccount(harness); - await setUserACLs(harness, admin, ['admin:authenticate', 'guild:list:members']); + await setUserACLs(harness, admin, ['admin:authenticate', 'guild:list:members', 'guild:force_add_member']); const guild = await createGuild(harness, admin.token, `Member List Guild ${Date.now()}`); - const result = await createBuilder<{ - members: Array; - total: number; - limit: number; - offset: number; - }>(harness, `${admin.token}`) + const joined = [await createTestAccount(harness), await createTestAccount(harness)]; + for (const account of joined) { + await createBuilder(harness, `${admin.token}`) + .put(`/admin/guilds/${guild.id}/members/${account.userId}`) + .body(null) + .expect(HTTP_STATUS.OK) + .execute(); + } + const expectedIds = [admin.userId, ...joined.map((account) => account.userId)].sort((a, b) => + BigInt(a) < BigInt(b) ? -1 : BigInt(a) > BigInt(b) ? 1 : 0, + ); + const result = await createBuilder(harness, `${admin.token}`) .get(`/admin/guilds/${guild.id}/members?limit=10&offset=0`) .expect(HTTP_STATUS.OK) .execute(); expect(result.limit).toBe(10); expect(result.offset).toBe(0); + expect(result.total).toBe(3); + expect(result.members.map((member) => member.user.id)).toEqual(expectedIds); + const page = await createBuilder(harness, `${admin.token}`) + .get(`/admin/guilds/${guild.id}/members?limit=1&offset=1`) + .expect(HTTP_STATUS.OK) + .execute(); + expect(page.total).toBe(3); + expect(page.members.map((member) => member.user.id)).toEqual([expectedIds[1]]); }); test('GET /admin/guilds/{guild_id}/members requires guild:list:members', async () => { const admin = await createTestAccount(harness); diff --git a/fluxer_api/src/api/admin/tests/audit_coverage/GuildAdminAuditCases.ts b/fluxer_api/src/api/admin/tests/audit_coverage/GuildAdminAuditCases.ts index c584b0859..ac183f527 100644 --- a/fluxer_api/src/api/admin/tests/audit_coverage/GuildAdminAuditCases.ts +++ b/fluxer_api/src/api/admin/tests/audit_coverage/GuildAdminAuditCases.ts @@ -140,7 +140,7 @@ export const GuildAdminAuditCases: ReadonlyArray = [ action: 'list_guild_members', targetType: 'guild', targetId: guild.id, - metadata: {limit: '10', offset: '5', result_count: '0', total: '0'}, + metadata: {limit: '10', offset: '5', result_count: '0', total: '1'}, }, }; }, diff --git a/fluxer_api/src/api/guild/tests/GuildMemberManagement.test.ts b/fluxer_api/src/api/guild/tests/GuildMemberManagement.test.ts index 0d1f3dabb..09ebacc3a 100644 --- a/fluxer_api/src/api/guild/tests/GuildMemberManagement.test.ts +++ b/fluxer_api/src/api/guild/tests/GuildMemberManagement.test.ts @@ -23,6 +23,10 @@ import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import type {GuildBanResponse, GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas'; import {afterEach, beforeEach, describe, expect, test} from 'vitest'; +function sortSnowflakes(ids: Array): Array { + return [...ids].sort((a, b) => (BigInt(a) < BigInt(b) ? -1 : BigInt(a) > BigInt(b) ? 1 : 0)); +} + describe('Guild Member Management', () => { let harness: ApiTestHarness; beforeEach(async () => { @@ -377,23 +381,27 @@ describe('Guild Member Management', () => { .execute(); }); test('should allow regular member to list guild members', async () => { - const {members, guild} = await setupTestGuildWithMembers(harness, 1); + const {owner, members, guild} = await setupTestGuildWithMembers(harness, 1); const member = members[0]; - await createBuilder(harness, member.token).get(`/guilds/${guild.id}/members`).expect(HTTP_STATUS.OK).execute(); + const memberList = await createBuilder>(harness, member.token) + .get(`/guilds/${guild.id}/members?limit=10`) + .expect(HTTP_STATUS.OK) + .execute(); + expect(memberList.map((entry) => entry.user.id)).toEqual(sortSnowflakes([owner.userId, member.userId])); }); test('should support limit parameter for listing members', async () => { - const {owner, guild} = await setupTestGuildWithMembers(harness, 3); - const memberList = await createBuilder< - Array<{ - user: { - id: string; - }; - }> - >(harness, owner.token) + const {owner, members, guild} = await setupTestGuildWithMembers(harness, 3); + const expectedIds = sortSnowflakes([owner.userId, ...members.map((member) => member.userId)]); + const firstPage = await createBuilder>(harness, owner.token) .get(`/guilds/${guild.id}/members?limit=2`) .expect(HTTP_STATUS.OK) .execute(); - expect(memberList.length).toBeLessThanOrEqual(2); + expect(firstPage.map((entry) => entry.user.id)).toEqual(expectedIds.slice(0, 2)); + const secondPage = await createBuilder>(harness, owner.token) + .get(`/guilds/${guild.id}/members?limit=2&after=${firstPage[1]!.user.id}`) + .expect(HTTP_STATUS.OK) + .execute(); + expect(secondPage.map((entry) => entry.user.id)).toEqual(expectedIds.slice(2)); }); }); describe('Get Member Permission Checks', () => { diff --git a/fluxer_api/src/api/test/NoopGatewayService.ts b/fluxer_api/src/api/test/NoopGatewayService.ts index 3ef70d399..9453a2597 100644 --- a/fluxer_api/src/api/test/NoopGatewayService.ts +++ b/fluxer_api/src/api/test/NoopGatewayService.ts @@ -656,18 +656,38 @@ export class NoopGatewayService extends IGatewayService { return members?.has(params.userId) ?? false; } - async listGuildMembers(_params: {guildId: GuildID; limit: number; offset: number}): Promise<{ + async listGuildMembers(params: {guildId: GuildID; limit: number; offset: number}): Promise<{ members: Array; total: number; }> { - return {members: [], total: 0}; + const userIds = await this.sortedMemberIds(params.guildId); + const page = params.limit > 0 ? userIds.slice(params.offset, params.offset + params.limit) : []; + return {members: await this.memberResponses(params.guildId, page), total: userIds.length}; } - async listGuildMembersCursor(_params: {guildId: GuildID; limit: number; after?: UserID}): Promise<{ + async listGuildMembersCursor(params: {guildId: GuildID; limit: number; after?: UserID}): Promise<{ members: Array; total: number; }> { - return {members: [], total: 0}; + const userIds = await this.sortedMemberIds(params.guildId); + const after = params.after; + const remaining = after === undefined ? userIds : userIds.filter((userId) => userId > after); + const page = params.limit > 0 ? remaining.slice(0, params.limit) : []; + return {members: await this.memberResponses(params.guildId, page), total: userIds.length}; + } + + private async sortedMemberIds(guildId: GuildID): Promise> { + const members = await guildMemberRepository.listMembers(guildId); + return members.map((member) => member.userId).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + } + + private async memberResponses(guildId: GuildID, userIds: Array): Promise> { + const members: Array = []; + for (const userId of userIds) { + const {memberData} = await this.getGuildMember({guildId, userId}); + if (memberData) members.push(memberData); + } + return members; } async checkPermission(params: { diff --git a/fluxer_api/vitest.config.ts b/fluxer_api/vitest.config.ts index 59a563cbb..fd08fcf7f 100644 --- a/fluxer_api/vitest.config.ts +++ b/fluxer_api/vitest.config.ts @@ -57,6 +57,7 @@ const sharedTestConfig = { testTimeout: 40000, hookTimeout: 20000, maxConcurrency: configuredMaxConcurrency, + execArgv: ['--no-experimental-webstorage', '--disable-warning=ExperimentalWarning'], }; export default defineConfig({ diff --git a/fluxer_app/rspack.config.mjs b/fluxer_app/rspack.config.mjs index de0d5e323..7af01c996 100644 --- a/fluxer_app/rspack.config.mjs +++ b/fluxer_app/rspack.config.mjs @@ -66,6 +66,29 @@ function resolveMode() { return 'production'; } +const ACCEPTED_CSS_ORDER_CONFLICTS = [ + ['app/components/dialogs/ConfirmModal', 'ui/button/Button'], + ['app/components/dialogs/Modal', 'ui/button/Button'], + ['app/components/dialogs/components/SettingsModalHeader', 'ui/button/Button'], + ['app/components/dialogs/shared/SettingsHeadingLinkButton', 'ui/button/Button'], + ['ui/tooltip/Tooltip', 'ui/button/Button'], +]; + +function isAcceptedCssOrderConflict(warning) { + const message = warning.message ?? ''; + if (!message.includes('Conflicting order between')) { + return false; + } + return ACCEPTED_CSS_ORDER_CONFLICTS.some(([first, second]) => { + const firstPath = `./src/features/${first}.module.css`; + const secondPath = `./src/features/${second}.module.css`; + return ( + message.includes(`between ${firstPath} and ${secondPath}`) || + message.includes(`between ${secondPath} and ${firstPath}`) + ); + }); +} + function isMainRuntimeChunk(chunk) { const runtime = chunk.runtime; if (runtime == null) { @@ -401,6 +424,7 @@ export default () => { target: ['web', 'browserslist'], lazyCompilation: false, performance: false, + ignoreWarnings: [isAcceptedCssOrderConflict], resolve: { alias: { ...resolveArboriumWasmAliases(), diff --git a/fluxer_app/src/features/app/components/BootstrapErrorScreen.test.tsx b/fluxer_app/src/features/app/components/BootstrapErrorScreen.test.tsx index 27cc93f1d..951f5d2d9 100644 --- a/fluxer_app/src/features/app/components/BootstrapErrorScreen.test.tsx +++ b/fluxer_app/src/features/app/components/BootstrapErrorScreen.test.tsx @@ -33,7 +33,9 @@ test('an unreachable instance offers a retry and never a reset of app data', () "Error invoking remote method 'desktop-runtime-config:resolve': DesktopRuntimeDiscoveryUnreachableError: No usable instance discovery document was served", ); - const markup = renderToStaticMarkup(); + const markup = renderToStaticMarkup( + , + ); expect(markup).toContain(RETRY_BUTTON); expect(markup).not.toContain(RESET_BUTTON); @@ -45,7 +47,9 @@ test('an unreachable instance keeps the raw error out of the page and offers it "Error invoking remote method 'desktop-runtime-config:resolve': DesktopRuntimeDiscoveryUnreachableError: connect ECONNREFUSED 127.0.0.1:9", ); - const markup = renderToStaticMarkup(); + const markup = renderToStaticMarkup( + , + ); expect(markup).not.toContain('DesktopRuntimeDiscoveryUnreachableError'); expect(markup).not.toContain('ECONNREFUSED'); @@ -55,13 +59,23 @@ test('an unreachable instance keeps the raw error out of the page and offers it test('a failure while the device is offline never offers a reset of app data', () => { vi.spyOn(navigator, 'onLine', 'get').mockReturnValue(false); - const markup = renderToStaticMarkup(); + const markup = renderToStaticMarkup( + , + ); expect(markup).not.toContain(RESET_BUTTON); }); test('a local startup failure still offers the reset of app data', () => { - const markup = renderToStaticMarkup(); + const markup = renderToStaticMarkup( + , + ); expect(markup).toContain(RETRY_BUTTON); expect(markup).toContain(RESET_BUTTON); diff --git a/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx b/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx index 0cddedbda..00cc766b1 100644 --- a/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx +++ b/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx @@ -265,8 +265,15 @@ export const AllSettingsRenderer: React.FC = observer( >
- Found in{' '} - + Found{' '} + {' '} + in{' '} +
{searchResults.map((result) => ( diff --git a/fluxer_app/src/features/app/components/layout/NativeWindowControls.tsx b/fluxer_app/src/features/app/components/layout/NativeWindowControls.tsx index 9f527b177..ceafa5185 100644 --- a/fluxer_app/src/features/app/components/layout/NativeWindowControls.tsx +++ b/fluxer_app/src/features/app/components/layout/NativeWindowControls.tsx @@ -38,7 +38,7 @@ interface ControlIconProps { const ControlIcon: React.FC = ({icon, 'data-flx': dataFlx}) => ( - + ); diff --git a/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx b/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx index cf5a3abef..6ff2d1364 100644 --- a/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx +++ b/fluxer_app/src/features/app/components/setup/SetupWizardSteps.tsx @@ -650,7 +650,12 @@ export const SignInMethodStep = observer( )} {!usernameSignIn && (
-

{i18n._(TAG_STYLE_TITLE_DESCRIPTOR)}

+

+ {i18n._(TAG_STYLE_TITLE_DESCRIPTOR)} +

{ test('closing a sheet by hand steps back over its history entry', () => { const history = requireHistory(); const back = vi.spyOn(history, 'back'); - act(() => root?.render()); + act(() => root?.render()); expect(sheetMarker()).toBeTypeOf('string'); act(() => closeSheet?.()); expect(back).toHaveBeenCalledTimes(1); @@ -73,7 +73,7 @@ test('closing a sheet by hand steps back over its history entry', () => { test('a sheet dismissed by an account switch leaves navigation to the switch', async () => { const history = requireHistory(); const back = vi.spyOn(history, 'back'); - act(() => root?.render()); + act(() => root?.render()); expect(sheetMarker()).toBeTypeOf('string'); (globalThis as {IS_REACT_ACT_ENVIRONMENT?: boolean}).IS_REACT_ACT_ENVIRONMENT = false; await AccountScopedWork.runSuspended(AccountScopedWorkTransitionReason.ACCOUNT_SWITCH, async () => { diff --git a/fluxer_app/src/features/app/state/Updater.ts b/fluxer_app/src/features/app/state/Updater.ts index c97915479..a34ca1665 100644 --- a/fluxer_app/src/features/app/state/Updater.ts +++ b/fluxer_app/src/features/app/state/Updater.ts @@ -16,15 +16,7 @@ import { isElectron, openExternalUrl, } from '@app/features/ui/utils/NativeUtils'; -import { - pushDesktopUpdateAvailableModal, - pushDesktopUpdateDownloadFailedModal, - pushManualUpdateAvailableModal, - pushUnsupportedUpdateModal, - pushUpdateAvailableModal, - pushUpdateCheckFailedModal, - pushUpToDateModal, -} from '@app/features/updater/commands/UpdaterModalCommands'; +import type * as UpdaterModalCommands from '@app/features/updater/commands/UpdaterModalCommands'; import { createUpdaterMachineSnapshot, getUpdaterDisplayVersion, @@ -61,6 +53,16 @@ const ALLOWED_WEB_UPDATE_HOSTS = new Set([ 'canary.fluxer.com', ]); +function loadUpdaterModals(): Promise { + return import('@app/features/updater/commands/UpdaterModalCommands'); +} + +function showUpdaterModal(show: (modals: typeof UpdaterModalCommands) => void): void { + void loadUpdaterModals().then(show, (error) => { + logger.warn('Failed to load the updater modals', error); + }); +} + async function dropCachedAppShell(): Promise { const browserCaches = getProtectedCacheStorage(); if (!browserCaches) return; @@ -251,6 +253,9 @@ class Updater { logger.warn('Failed to read desktop info', error); } this.subscribeToNativeEvents(); + void loadUpdaterModals().catch((error) => { + logger.debug('Failed to preload the updater modals', error); + }); } private async bootstrapDesktopUpdate(): Promise { @@ -399,7 +404,7 @@ class Updater { } this.transition({type: 'native.error'}); if (isUserCheck) { - pushUpdateCheckFailedModal(); + showUpdaterModal((modals) => modals.pushUpdateCheckFailedModal()); } break; case 'unsupported': @@ -410,7 +415,9 @@ class Updater { now: Date.now(), }); if (shouldShowImmediateUserResult) { - pushUnsupportedUpdateModal(event.reason ?? 'platform', event.downloadUrl ?? null); + const reason = event.reason ?? 'platform'; + const downloadUrl = event.downloadUrl ?? null; + showUpdaterModal((modals) => modals.pushUnsupportedUpdateModal(reason, downloadUrl)); } break; } @@ -505,7 +512,7 @@ class Updater { failed = true; logger.debug('Update check failed silently:', err); if (userInitiated) { - pushUpdateCheckFailedModal(); + showUpdaterModal((modals) => modals.pushUpdateCheckFailedModal()); } } finally { this.checkInProgress = false; @@ -576,7 +583,7 @@ class Updater { return; } if (this.isNative && this.nativeUnsupported?.reason === 'managed-package' && !this.updateInfo.web.available) { - pushUnsupportedUpdateModal('managed-package'); + showUpdaterModal((modals) => modals.pushUnsupportedUpdateModal('managed-package')); return; } if (this.isNative && this.nativeManualUpdateAvailable && !this.nativeUnsupported) { @@ -595,7 +602,8 @@ class Updater { if (url) { await openExternalUrl(url); } else if (this.nativeUnsupported) { - pushUnsupportedUpdateModal(this.nativeUnsupported.reason, this.nativeUnsupported.downloadUrl); + const {reason, downloadUrl} = this.nativeUnsupported; + showUpdaterModal((modals) => modals.pushUnsupportedUpdateModal(reason, downloadUrl)); } else { await openExternalUrl(DESKTOP_DOWNLOAD_URL); } @@ -604,7 +612,7 @@ class Updater { private showCurrentUpdateState(): void { if (this.desktopUpdateAvailable) { - pushDesktopUpdateAvailableModal(() => this.startDesktopUpdate()); + showUpdaterModal((modals) => modals.pushDesktopUpdateAvailableModal(() => this.startDesktopUpdate())); return; } if (this.nativeManualUpdateAvailable) { @@ -615,10 +623,12 @@ class Updater { return; } if (this.nativeUnsupported) { - pushUnsupportedUpdateModal(this.nativeUnsupported.reason, this.nativeUnsupported.downloadUrl); + const {reason, downloadUrl} = this.nativeUnsupported; + showUpdaterModal((modals) => modals.pushUnsupportedUpdateModal(reason, downloadUrl)); return; } - pushUpToDateModal(this.currentVersion); + const currentVersion = this.currentVersion; + showUpdaterModal((modals) => modals.pushUpToDateModal(currentVersion)); } private getManualUpdateSuggestedName(url: string): string { @@ -633,17 +643,24 @@ class Updater { } private showManualNativeUpdateModal(): void { - if (this.nativeManualDownloadOptions.length > 0) { - pushManualUpdateAvailableModal({ - currentVersion: this.currentVersion, - version: this.updateInfo.native.version, - options: this.nativeManualDownloadOptions, - onDownload: (option) => this.downloadManualNativeUpdateOption(option), - }); + const version = this.updateInfo.native.version; + const options = this.nativeManualDownloadOptions; + if (options.length > 0) { + const currentVersion = this.currentVersion; + showUpdaterModal((modals) => + modals.pushManualUpdateAvailableModal({ + currentVersion, + version, + options, + onDownload: (option) => this.downloadManualNativeUpdateOption(option), + }), + ); return; } const url = this.nativeManualDownloadUrl ?? this.nativeUnsupported?.downloadUrl ?? DESKTOP_DOWNLOAD_URL; - pushUpdateAvailableModal(this.updateInfo.native.version, () => this.downloadManualNativeUpdateOrOpen(url)); + showUpdaterModal((modals) => + modals.pushUpdateAvailableModal(version, () => this.downloadManualNativeUpdateOrOpen(url)), + ); } private async refreshManualNativeDownloadOption(option: UpdaterDownloadOption): Promise { @@ -704,7 +721,7 @@ class Updater { } if (outcome === 'checksum-mismatch') { logger.error('Native manual update download did not match its published checksum', {url}); - pushDesktopUpdateDownloadFailedModal(); + showUpdaterModal((modals) => modals.pushDesktopUpdateDownloadFailedModal()); return; } logger.warn('Native manual update download unavailable; opening update URL externally', {outcome}); diff --git a/fluxer_app/src/features/auth/commands/AccountSwitcherModalCommands.tsx b/fluxer_app/src/features/auth/commands/AccountSwitcherModalCommands.tsx index a9692bcbf..70ce47903 100644 --- a/fluxer_app/src/features/auth/commands/AccountSwitcherModalCommands.tsx +++ b/fluxer_app/src/features/auth/commands/AccountSwitcherModalCommands.tsx @@ -11,7 +11,12 @@ const logger = new Logger('AccountSwitcherModalCommands'); export function openAccountSwitcherModal(props: AccountSwitcherModalProps, key: string | null): void { void loadLazyModule(() => import('@app/features/auth/components/accounts/AccountSwitcherModal')) .then(({default: AccountSwitcherModal}) => { - const render = modal(() => ); + const render = modal(() => ( + + )); if (key === null) { ModalCommands.push(render); return; diff --git a/fluxer_app/src/features/auth/components/InstanceBrandMark.tsx b/fluxer_app/src/features/auth/components/InstanceBrandMark.tsx index 59b0e0797..faaf12670 100644 --- a/fluxer_app/src/features/auth/components/InstanceBrandMark.tsx +++ b/fluxer_app/src/features/auth/components/InstanceBrandMark.tsx @@ -56,5 +56,13 @@ export function InstanceBrandMark({ if (iconUrl == null) { return globe; } - return ; + return ( + + ); } diff --git a/fluxer_app/src/features/auth/flow/HandoffApprovalFlow.tsx b/fluxer_app/src/features/auth/flow/HandoffApprovalFlow.tsx index 0d0fdb730..67e167f67 100644 --- a/fluxer_app/src/features/auth/flow/HandoffApprovalFlow.tsx +++ b/fluxer_app/src/features/auth/flow/HandoffApprovalFlow.tsx @@ -239,14 +239,18 @@ export function HandoffApprovalFlow({handoff, onRetry}: HandoffApprovalFlowProps return (

- +

Only continue if you started this sign-in on a device you control.

- + {matchCode != null ? (
@@ -254,10 +258,10 @@ export function HandoffApprovalFlow({handoff, onRetry}: HandoffApprovalFlowProps Make sure this code matches the one in the {PRODUCT_NAME} app. - +
) : null} - +
) : ( {handoff.hasStoppedWaiting ? ( ) : ( @@ -292,6 +307,7 @@ export const PlutoniumPage = observer(function PlutoniumPage() { badge={savingsBadge} directional flx={`premium.plutonium-page.${flxSuffix}.subscribe-yearly`} + data-flx="premium.plutonium-page.plutonium-page.subscribe-buttons.page-button.start-checkout" > {i18n._(SUBSCRIBE_YEARLY_DESCRIPTOR)} @@ -301,6 +317,7 @@ export const PlutoniumPage = observer(function PlutoniumPage() { disabled={purchaseDisabled} loading={loadingCheckout && pendingPlan === 'monthly'} flx={`premium.plutonium-page.${flxSuffix}.subscribe-monthly`} + data-flx="premium.plutonium-page.plutonium-page.subscribe-buttons.page-button.start-checkout--2" > {i18n._(SUBSCRIBE_MONTHLY_DESCRIPTOR)} @@ -312,6 +329,7 @@ export const PlutoniumPage = observer(function PlutoniumPage() { variant={variant} onClick={PlutoniumPageCommands.openGiftPlutoniumModal} flx={`premium.plutonium-page.${flxSuffix}.gift`} + data-flx="premium.plutonium-page.plutonium-page.gift-button.page-button.open-gift-plutonium-modal" > {i18n._(GIFT_PLUTONIUM_DESCRIPTOR, {premiumProductName: getPremiumProductName()})} @@ -351,27 +369,39 @@ export const PlutoniumPage = observer(function PlutoniumPage() { const secondaryLinks = (
{giftPurchasesAvailable && ( - + )} - +
@@ -385,9 +415,18 @@ export const PlutoniumPage = observer(function PlutoniumPage() { )} {pricesLoaded && (

- {monthlyLabel} - {i18n._(PRICE_OR_DESCRIPTOR)} - {yearlyLabel} + + {monthlyLabel} + + + {i18n._(PRICE_OR_DESCRIPTOR)} + + + {yearlyLabel} +

)}
@@ -415,6 +454,7 @@ export const PlutoniumPage = observer(function PlutoniumPage() { onClick={() => scrollToId(MANAGE_ID)} directional flx="premium.plutonium-page.hero.manage" + data-flx="premium.plutonium-page.plutonium-page.render-hero-body.page-button.scroll-to-id" > {i18n._(MANAGE_SUBSCRIPTION_DESCRIPTOR)} @@ -435,6 +475,7 @@ export const PlutoniumPage = observer(function PlutoniumPage() { onClick={PlutoniumPageCommands.openGiftInventorySettings} directional flx="premium.plutonium-page.hero.redeem" + data-flx="premium.plutonium-page.plutonium-page.render-hero-body.page-button.open-gift-inventory-settings" > {i18n._(REDEEM_GIFT_CODE_DESCRIPTOR)} @@ -452,10 +493,14 @@ export const PlutoniumPage = observer(function PlutoniumPage() { data-flx="premium.plutonium-page.root" >
-