diff --git a/components/connector/connector-client.tsx b/components/connector/connector-client.tsx index 464815918..f8c75f50c 100644 --- a/components/connector/connector-client.tsx +++ b/components/connector/connector-client.tsx @@ -1,9 +1,11 @@ 'use client'; import { useEffect, useMemo, useState } from 'react'; +import { useConfig } from '@/hooks/use-config'; import { resolveTarget, type ResolveResult } from '@/lib/connector/registry'; import { appPath } from '@/lib/deep-links'; import { replaceWindowLocation } from '@/lib/browser-navigation'; +import { productName } from '@/lib/product-name'; const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || ''; @@ -25,6 +27,7 @@ const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || ''; */ export function ConnectorClient({ target }: { target: string }) { const [result, setResult] = useState(null); + const name = productName(useConfig().appName); useEffect(() => { setResult(resolveTarget(target, new URLSearchParams(window.location.search))); @@ -52,12 +55,12 @@ export function ConnectorClient({ target }: { target: string }) { if (result.kind === 'unknown-target') { return ( -

This link is newer than this Bulwark

+

This link is newer than this {name}

It opens something {APP_VERSION ? `version ${APP_VERSION} ` : 'this version '} - does not know about. Updating Bulwark should teach it. + does not know about. Updating {name} should teach it.

- +
); } @@ -71,19 +74,19 @@ export function ConnectorClient({ target }: { target: string }) { {result.param} is missing or malformed. Nothing was opened.

- + ); } -function Actions() { +function Actions({ name }: { name: string }) { return (

- Open Bulwark + Open {name}

); diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx index 3de801e95..f062ec582 100644 --- a/components/mail/mail-app.tsx +++ b/components/mail/mail-app.tsx @@ -6,6 +6,8 @@ import { Sidebar } from "@/components/layout/sidebar"; import { EmailList } from "@/components/email/email-list"; import { MvComposeInline, MvComposeWindow } from "@/components/email/mv-compose-window"; import { usePinnedPerSession } from "@/hooks/use-pinned-per-session"; +import { useConfig } from "@/hooks/use-config"; +import { productName } from "@/lib/product-name"; import { MessageListTabs } from "@/components/email/message-list-tabs"; import dynamic from "next/dynamic"; import type { ComposerDraftData } from "@/components/email/email-composer"; @@ -200,6 +202,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { const linkSegments = useLiteLinkSegments('mail', routeSegments); const t = useTranslations(); const tCommon = useTranslations('common'); + const appName = productName(useConfig().appName); const tQuote = useTranslations('quote_header'); const mailLayout = useEffectiveMailLayout(); const interfaceLayout = useSettingsStore((state) => state.interfaceLayout); @@ -4494,7 +4497,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
{tCommon('rate_limited_title')} - {tCommon('rate_limited_detail', { seconds: rateLimitSecondsLeft })} + {tCommon('rate_limited_detail', { seconds: rateLimitSecondsLeft, appName })}
)} {connectionLost && ( diff --git a/components/plugins/__tests__/plugin-connect-dialog.test.tsx b/components/plugins/__tests__/plugin-connect-dialog.test.tsx index 86fa4894e..ec676144f 100644 --- a/components/plugins/__tests__/plugin-connect-dialog.test.tsx +++ b/components/plugins/__tests__/plugin-connect-dialog.test.tsx @@ -10,6 +10,8 @@ vi.mock('@/lib/browser-navigation', async (importOriginal) => ({ ...(await importOriginal()), apiFetch: (...args: unknown[]) => apiFetch(...args), })); +const appName = vi.hoisted(() => ({ value: 'Webmail' })); +vi.mock('@/hooks/use-config', () => ({ useConfig: () => ({ appName: appName.value }) })); import { PluginConnectDialog } from '../plugin-connect-dialog'; import { originFromInput, requestConnect, type ConnectResult } from '@/lib/plugin-sandbox/connect-dialog'; @@ -17,6 +19,7 @@ import { originFromInput, requestConnect, type ConnectResult } from '@/lib/plugi beforeEach(() => { apiFetch.mockReset(); apiFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + appName.value = 'Webmail'; }); function open(mode: 'password' | 'signin' = 'password') { @@ -62,6 +65,19 @@ describe('connect dialog', () => { expect(JSON.parse(apiFetch.mock.calls[0][1].body)).not.toHaveProperty('secret'); }); + it('names the app as configured, and Bulwark on a stock install', async () => { + const first = render(); + const result = open('signin'); + expect(screen.getByText(/intro_signin /)).toHaveTextContent('"appName":"Bulwark"'); + first.unmount(); + + appName.value = 'Acme Mail'; + render(); + expect(screen.getByText(/intro_signin /)).toHaveTextContent('"appName":"Acme Mail"'); + act(() => { fireEvent.keyDown(window, { key: 'Escape' }); }); + await expect(result).resolves.toEqual({ connected: false }); + }); + it('shows the server error and stays open', async () => { apiFetch.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'The server address must start with https://' }), { status: 400 })); render(); diff --git a/components/plugins/plugin-connect-dialog.tsx b/components/plugins/plugin-connect-dialog.tsx index 962e70c54..611d0c221 100644 --- a/components/plugins/plugin-connect-dialog.tsx +++ b/components/plugins/plugin-connect-dialog.tsx @@ -4,6 +4,8 @@ import { useEffect, useId, useState, useSyncExternalStore } from 'react'; import { useTranslations } from 'next-intl'; import { KeyRound } from '@/components/icons'; import { Button } from '@/components/ui/button'; +import { useConfig } from '@/hooks/use-config'; +import { productName } from '@/lib/product-name'; import { currentConnect, originFromInput, @@ -19,6 +21,7 @@ import { export function PluginConnectDialog() { const request = useSyncExternalStore(subscribeConnect, currentConnect, () => null); const t = useTranslations('plugins.connect'); + const appName = productName(useConfig().appName); const titleId = useId(); const [server, setServer] = useState(''); const [username, setUsername] = useState(''); @@ -78,10 +81,10 @@ export function PluginConnectDialog() { const host = origin ? new URL(origin).host : null; const intro = mode === 'signin' - ? t('intro_signin', { name: pluginName, label: spec.label }) + ? t('intro_signin', { name: pluginName, label: spec.label, appName }) : host - ? t('intro_password', { name: pluginName, label: spec.label, host }) - : t('intro_password_any', { name: pluginName, label: spec.label }); + ? t('intro_password', { name: pluginName, label: spec.label, host, appName }) + : t('intro_password_any', { name: pluginName, label: spec.label, appName }); return (
{t('none')}

; @@ -39,7 +42,7 @@ export function PluginPermissionList({ permissions, httpOrigins, apiPostPaths, c {risky && }
{t(`${key}.title`)}
- {!compact &&
{t(`${key}.body`)}
} + {!compact &&
{t(`${key}.body`, { appName })}
} {targets && targets.length > 0 && (
    {targets.map((target) => ( diff --git a/components/protocol/protocol-launch-handler-provider.tsx b/components/protocol/protocol-launch-handler-provider.tsx index 7e8651e2d..885203711 100644 --- a/components/protocol/protocol-launch-handler-provider.tsx +++ b/components/protocol/protocol-launch-handler-provider.tsx @@ -4,7 +4,9 @@ import { useEffect } from "react"; import type { ReactNode } from "react"; import { useTranslations } from "next-intl"; import { usePathname, useRouter } from "@/i18n/navigation"; +import { useConfig } from "@/hooks/use-config"; import { getPathPrefix } from "@/lib/browser-navigation"; +import { productName } from "@/lib/product-name"; import { parseMailto } from "@/lib/protocol-handlers/mailto"; import { parseWebcal } from "@/lib/protocol-handlers/webcal"; import { @@ -68,6 +70,7 @@ interface ProtocolLaunchHandlerProviderProps { export function ProtocolLaunchHandlerProvider({ children }: ProtocolLaunchHandlerProviderProps) { const t = useTranslations("protocol_handlers"); + const appName = productName(useConfig().appName); const router = useRouter(); const pathname = usePathname(); @@ -81,10 +84,10 @@ export function ProtocolLaunchHandlerProvider({ children }: ProtocolLaunchHandle }, () => ({ path: pathname, standalone: isStandaloneDisplayMode(), - focusNotificationTitle: t("focus_notification_title"), - focusNotificationBody: t("focus_notification_body"), + focusNotificationTitle: t("focus_notification_title", { appName }), + focusNotificationBody: t("focus_notification_body", { appName }), })); - }, [pathname, router, t]); + }, [pathname, router, t, appName]); useEffect(() => { if (typeof window === "undefined" || !window.launchQueue) return; diff --git a/components/providers/rate-limit-toast-provider.tsx b/components/providers/rate-limit-toast-provider.tsx index c3c0287c7..140fa2af9 100644 --- a/components/providers/rate-limit-toast-provider.tsx +++ b/components/providers/rate-limit-toast-provider.tsx @@ -2,6 +2,8 @@ import { useEffect } from 'react'; import { useTranslations } from 'next-intl'; +import { useConfig } from '@/hooks/use-config'; +import { productName } from '@/lib/product-name'; import { toast } from '@/stores/toast-store'; type RateLimitBlockedDetail = { @@ -10,6 +12,7 @@ type RateLimitBlockedDetail = { export function RateLimitToastProvider({ children }: { children: React.ReactNode }) { const tCommon = useTranslations('common'); + const appName = productName(useConfig().appName); useEffect(() => { const onRateLimitBlocked = (event: Event) => { @@ -18,13 +21,13 @@ export function RateLimitToastProvider({ children }: { children: React.ReactNode toast.warning( tCommon('rate_limited_action_title'), - tCommon('rate_limited_action_detail', { seconds }), + tCommon('rate_limited_action_detail', { seconds, appName }), ); }; window.addEventListener('bulwark:rate-limit-blocked', onRateLimitBlocked); return () => window.removeEventListener('bulwark:rate-limit-blocked', onRateLimitBlocked); - }, [tCommon]); + }, [tCommon, appName]); return <>{children}; } \ No newline at end of file diff --git a/components/settings/protocol-handler-settings.tsx b/components/settings/protocol-handler-settings.tsx index 13c554085..48f1e9d05 100644 --- a/components/settings/protocol-handler-settings.tsx +++ b/components/settings/protocol-handler-settings.tsx @@ -3,7 +3,9 @@ import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; +import { useConfig } from "@/hooks/use-config"; import { getPathPrefix } from "@/lib/browser-navigation"; +import { productName } from "@/lib/product-name"; import { useSettingsStore } from "@/stores/settings-store"; import type { ProtocolOpenMode } from "@/stores/settings-store"; import { toast } from "@/stores/toast-store"; @@ -35,6 +37,7 @@ interface ProtocolHandlerSettingsProps { export function ProtocolHandlerSettings({ supportsCalendar }: ProtocolHandlerSettingsProps) { const t = useTranslations("protocol_handlers"); + const appName = productName(useConfig().appName); const protocolOpenMode = useSettingsStore((state) => state.protocolOpenMode); const updateSetting = useSettingsStore((state) => state.updateSetting); const [supported, setSupported] = useState(false); @@ -74,24 +77,24 @@ export function ProtocolHandlerSettings({ supportsCalendar }: ProtocolHandlerSet }; return ( - + {!supported && (
    {t("unsupported")}
    )} - + {renderRegistrationControl("mailto")} {supportsCalendar && ( - + {renderRegistrationControl("webcal")} )} - +