@carlonicora/nextjs-jsonapi 3.23.0 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/{AssistantMessageInterface-CuaMDs2i.d.ts → AssistantMessageInterface-1Kbz8RVs.d.ts} +34 -1
  2. package/dist/{AssistantMessageInterface-Du3ShThp.d.mts → AssistantMessageInterface-CoxJyuFM.d.mts} +34 -1
  3. package/dist/{BlockNoteEditor-BUSOUPQ7.js → BlockNoteEditor-PTD2V4CH.js} +19 -19
  4. package/dist/{BlockNoteEditor-BUSOUPQ7.js.map → BlockNoteEditor-PTD2V4CH.js.map} +1 -1
  5. package/dist/{BlockNoteEditor-W42IQTGL.mjs → BlockNoteEditor-Q2WEDMKJ.mjs} +4 -4
  6. package/dist/{BlockNoteViewer-FQ23TZ3J.mjs → BlockNoteViewer-WO6GTDSI.mjs} +2 -2
  7. package/dist/{BlockNoteViewer-MCX53AR4.js → BlockNoteViewer-WOXEJ6RM.js} +3 -3
  8. package/dist/{BlockNoteViewer-MCX53AR4.js.map → BlockNoteViewer-WOXEJ6RM.js.map} +1 -1
  9. package/dist/billing/index.js +401 -401
  10. package/dist/billing/index.mjs +3 -3
  11. package/dist/{chunk-O5MKEU7G.mjs → chunk-2O3SFHQ7.mjs} +36 -38
  12. package/dist/{chunk-O5MKEU7G.mjs.map → chunk-2O3SFHQ7.mjs.map} +1 -1
  13. package/dist/{chunk-FXWO5J2R.js → chunk-3D2MCVRZ.js} +7 -7
  14. package/dist/{chunk-FXWO5J2R.js.map → chunk-3D2MCVRZ.js.map} +1 -1
  15. package/dist/{chunk-VR65NRE4.js → chunk-3YJF7QY4.js} +1338 -1115
  16. package/dist/chunk-3YJF7QY4.js.map +1 -0
  17. package/dist/{chunk-GFLEFIAW.mjs → chunk-GJHKOG5L.mjs} +1399 -1176
  18. package/dist/chunk-GJHKOG5L.mjs.map +1 -0
  19. package/dist/{chunk-JZUBFAAY.mjs → chunk-HAI7G6MD.mjs} +2 -2
  20. package/dist/{chunk-YCHPWXHD.js → chunk-IV4DTYXN.js} +36 -38
  21. package/dist/chunk-IV4DTYXN.js.map +1 -0
  22. package/dist/client/index.d.mts +13 -2
  23. package/dist/client/index.d.ts +13 -2
  24. package/dist/client/index.js +4 -4
  25. package/dist/client/index.mjs +3 -3
  26. package/dist/components/index.d.mts +87 -28
  27. package/dist/components/index.d.ts +87 -28
  28. package/dist/components/index.js +8 -4
  29. package/dist/components/index.js.map +1 -1
  30. package/dist/components/index.mjs +7 -3
  31. package/dist/contexts/index.d.mts +11 -2
  32. package/dist/contexts/index.d.ts +11 -2
  33. package/dist/contexts/index.js +4 -4
  34. package/dist/contexts/index.mjs +3 -3
  35. package/dist/core/index.d.mts +27 -5
  36. package/dist/core/index.d.ts +27 -5
  37. package/dist/core/index.js +2 -2
  38. package/dist/core/index.mjs +1 -1
  39. package/dist/features/help/index.js +37 -37
  40. package/dist/features/help/index.mjs +3 -3
  41. package/dist/features/tokenusage/index.js +101 -101
  42. package/dist/features/tokenusage/index.mjs +3 -3
  43. package/dist/index.d.mts +2 -3
  44. package/dist/index.d.ts +2 -3
  45. package/dist/index.js +3 -3
  46. package/dist/index.mjs +2 -2
  47. package/dist/{oauth.interface-CD90ycYz.d.ts → oauth.interface-BZKZAPRw.d.ts} +8 -1
  48. package/dist/{oauth.interface-DRE3NAKc.d.mts → oauth.interface-nVwnzxJw.d.mts} +8 -1
  49. package/dist/server/index.js +3 -3
  50. package/dist/server/index.mjs +1 -1
  51. package/package.json +1 -1
  52. package/src/components/forms/__tests__/EditorSheet.test.tsx +5 -3
  53. package/src/core/utils/__tests__/translateResponse.test.ts +3 -1
  54. package/src/features/assistant/components/containers/AssistantContainer.tsx +105 -36
  55. package/src/features/assistant/components/containers/__tests__/AssistantContainer.spec.tsx +163 -1
  56. package/src/features/assistant/components/parts/AssistantComposer.tsx +38 -15
  57. package/src/features/assistant/components/parts/AssistantEmptyState.tsx +5 -3
  58. package/src/features/assistant/components/parts/AssistantSidebar.tsx +26 -7
  59. package/src/features/assistant/components/parts/__tests__/AssistantComposer.spec.tsx +8 -0
  60. package/src/features/assistant/components/parts/__tests__/AssistantSidebar.spec.tsx +7 -0
  61. package/src/features/assistant/contexts/AssistantContext.tsx +91 -7
  62. package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +196 -0
  63. package/src/features/assistant-action/components/AssistantContainerWithApprovals.tsx +23 -1
  64. package/src/features/assistant-message/components/EntityMentionLink.tsx +28 -0
  65. package/src/features/assistant-message/components/__tests__/EntityMentionLink.spec.tsx +58 -0
  66. package/src/features/assistant-message/components/index.ts +1 -0
  67. package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +1 -1
  68. package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +2 -2
  69. package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +3 -2
  70. package/src/features/oauth/components/OAuthClientForm.tsx +21 -4
  71. package/src/features/oauth/components/__tests__/OAuthClientForm.spec.tsx +63 -0
  72. package/src/features/oauth/components/consent/OAuthCompanySelector.tsx +54 -0
  73. package/src/features/oauth/components/consent/OAuthConsentActions.tsx +5 -2
  74. package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +10 -6
  75. package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +33 -13
  76. package/src/features/oauth/components/consent/OAuthScopeList.tsx +4 -1
  77. package/src/features/oauth/components/consent/__tests__/OAuthConsentHeader.spec.tsx +25 -0
  78. package/src/features/oauth/components/consent/__tests__/OAuthConsentScreen.spec.tsx +126 -0
  79. package/src/features/oauth/components/consent/index.ts +1 -0
  80. package/src/features/oauth/data/__tests__/oauth.service.spec.ts +103 -0
  81. package/src/features/oauth/data/oauth.service.ts +3 -13
  82. package/src/features/oauth/data/oauth.ts +30 -1
  83. package/src/features/oauth/hooks/useOAuthConsent.ts +34 -3
  84. package/src/features/oauth/interfaces/oauth.interface.ts +11 -25
  85. package/src/hooks/__tests__/useDataListRetriever.test.ts +1 -1
  86. package/dist/AssistantInterface-RvZzlcA_.d.ts +0 -36
  87. package/dist/AssistantInterface-jL56dyNc.d.mts +0 -36
  88. package/dist/chunk-GFLEFIAW.mjs.map +0 -1
  89. package/dist/chunk-VR65NRE4.js.map +0 -1
  90. package/dist/chunk-YCHPWXHD.js.map +0 -1
  91. /package/dist/{BlockNoteEditor-W42IQTGL.mjs.map → BlockNoteEditor-Q2WEDMKJ.mjs.map} +0 -0
  92. /package/dist/{BlockNoteViewer-FQ23TZ3J.mjs.map → BlockNoteViewer-WO6GTDSI.mjs.map} +0 -0
  93. /package/dist/{chunk-JZUBFAAY.mjs.map → chunk-HAI7G6MD.mjs.map} +0 -0
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi, beforeAll } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
4
+ import { configureI18n } from "../../../../i18n";
5
+ import { EntityMentionLink } from "../EntityMentionLink";
6
+
7
+ beforeAll(() => {
8
+ // The package Link resolves its inner component at runtime and throws if i18n
9
+ // was never configured (src/i18n/config.ts).
10
+ configureI18n({
11
+ useRouter: () => ({
12
+ push: vi.fn(),
13
+ replace: vi.fn(),
14
+ back: vi.fn(),
15
+ forward: vi.fn(),
16
+ refresh: vi.fn(),
17
+ prefetch: vi.fn(),
18
+ }),
19
+ useTranslations: () => (key: string) => key,
20
+ usePathname: () => "/",
21
+ Link: ({
22
+ href,
23
+ children,
24
+ prefetch: _prefetch,
25
+ ...rest
26
+ }: {
27
+ href: string;
28
+ children: React.ReactNode;
29
+ [key: string]: any;
30
+ }) => (
31
+ <a href={href} {...rest}>
32
+ {children}
33
+ </a>
34
+ ),
35
+ });
36
+ ModuleRegistry.register("Account" as any, { name: "accounts", pageUrl: "/accounts" } as any);
37
+ });
38
+
39
+ describe("EntityMentionLink", () => {
40
+ it("renders a link to the entity page for a registered module", () => {
41
+ render(<>{EntityMentionLink({ type: "accounts", id: "a1", alias: "Rossi" })}</>);
42
+ const link = screen.getByRole("link", { name: "Rossi" });
43
+ expect(link).toHaveAttribute("href", "/accounts/a1");
44
+ expect(link).toHaveTextContent("Rossi");
45
+ });
46
+
47
+ it("renders through the package Link (locale-aware, link typography role)", () => {
48
+ render(<>{EntityMentionLink({ type: "accounts", id: "a1", alias: "Rossi" })}</>);
49
+ const link = screen.getByRole("link", { name: "Rossi" });
50
+ expect(link.className).toContain("text-primary font-medium");
51
+ });
52
+
53
+ it("renders plain text with no link for an unknown type", () => {
54
+ render(<>{EntityMentionLink({ type: "unknown-things", id: "x1", alias: "Mystery" })}</>);
55
+ expect(screen.getByText("Mystery")).toBeInTheDocument();
56
+ expect(screen.queryByRole("link")).not.toBeInTheDocument();
57
+ });
58
+ });
@@ -1,3 +1,4 @@
1
1
  export * from "./MessageItem";
2
2
  export * from "./MessageList";
3
3
  export { MessageSourcesPanel } from "./parts/MessageSourcesPanel";
4
+ export * from "./EntityMentionLink";
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import { useTranslations } from "next-intl";
4
- import Link from "next/link";
5
4
  import type { ApiDataInterface } from "../../../../../core";
6
5
  import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
7
6
  import { usePageUrlGenerator } from "../../../../../hooks";
8
7
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui/ui/table";
9
8
  import type { ChunkInterface, ChunkRelationshipMeta } from "../../../../chunk/data/ChunkInterface";
10
9
  import { useEntityLabel } from "./useEntityLabel";
10
+ import { Link } from "../../../../../shadcnui/custom/link";
11
11
 
12
12
  interface Props {
13
13
  citations: (ChunkInterface & ChunkRelationshipMeta)[];
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import { useTranslations } from "next-intl";
4
- import Link from "next/link";
5
4
  import type { ApiDataInterface } from "../../../../../core";
6
5
  import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
7
6
  import { usePageUrlGenerator } from "../../../../../hooks";
8
7
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui/ui/table";
9
8
  import { useEntityLabel } from "./useEntityLabel";
9
+ import { Link } from "../../../../../shadcnui/custom/link";
10
10
 
11
11
  interface Props {
12
12
  references: ApiDataInterface[];
@@ -42,7 +42,7 @@ export function ReferencesTab({ references }: Props) {
42
42
  return (
43
43
  <TableRow key={`${ref.type}/${ref.id}`}>
44
44
  <TableCell>
45
- <Link href={href} target="_blank" rel="noopener noreferrer" className="font-medium">
45
+ <Link href={href} target="_blank" rel="noopener noreferrer">
46
46
  {ref.identifier}
47
47
  </Link>
48
48
  </TableCell>
@@ -37,7 +37,8 @@ describe("Logout", () => {
37
37
  // We need to delete the original location first
38
38
  // @ts-expect-error - deleting window.location for mocking purposes
39
39
  delete window.location;
40
- window.location = {
40
+ // The DOM lib types `window.location` as accessor-only; jsdom lets the test replace it.
41
+ (window as unknown as { location: Location }).location = {
41
42
  ...originalLocation,
42
43
  get href() {
43
44
  return windowLocationHref;
@@ -50,7 +51,7 @@ describe("Logout", () => {
50
51
 
51
52
  afterEach(() => {
52
53
  // Restore original location
53
- window.location = originalLocation;
54
+ (window as unknown as { location: Location }).location = originalLocation;
54
55
  });
55
56
 
56
57
  describe("Scenario: Clears storage when storageKeys provided", () => {
@@ -17,7 +17,12 @@ import {
17
17
  } from "../../../shadcnui";
18
18
  import { OAuthRedirectUriInput } from "./OAuthRedirectUriInput";
19
19
  import { OAuthScopeSelector } from "./OAuthScopeSelector";
20
- import { OAuthClientCreateRequest, OAuthClientInterface, DEFAULT_GRANT_TYPES } from "../interfaces/oauth.interface";
20
+ import {
21
+ OAuthClientCreateRequest,
22
+ OAuthClientInterface,
23
+ OAuthScopeInfo,
24
+ DEFAULT_GRANT_TYPES,
25
+ } from "../interfaces/oauth.interface";
21
26
 
22
27
  export interface OAuthClientFormProps {
23
28
  /** Existing client for edit mode (undefined = create mode) */
@@ -28,6 +33,10 @@ export interface OAuthClientFormProps {
28
33
  onCancel: () => void;
29
34
  /** Whether form is submitting */
30
35
  isLoading?: boolean;
36
+ /** Scopes the app offers in the selector (defaults to AVAILABLE_OAUTH_SCOPES) */
37
+ availableScopes?: OAuthScopeInfo[];
38
+ /** Scopes preselected in create mode (edit mode uses client.allowedScopes) */
39
+ defaultScopes?: string[];
31
40
  }
32
41
 
33
42
  interface FormState {
@@ -47,14 +56,21 @@ interface FormErrors {
47
56
  /**
48
57
  * Form for creating or editing an OAuth client
49
58
  */
50
- export function OAuthClientForm({ client, onSubmit, onCancel, isLoading = false }: OAuthClientFormProps) {
59
+ export function OAuthClientForm({
60
+ client,
61
+ onSubmit,
62
+ onCancel,
63
+ isLoading = false,
64
+ availableScopes,
65
+ defaultScopes = [],
66
+ }: OAuthClientFormProps) {
51
67
  const isEditMode = !!client;
52
68
 
53
69
  const [formState, setFormState] = useState<FormState>({
54
70
  name: client?.name || "",
55
71
  description: client?.description || "",
56
72
  redirectUris: client?.redirectUris?.length ? client.redirectUris : [""],
57
- allowedScopes: client?.allowedScopes || [],
73
+ allowedScopes: client ? client.allowedScopes || [] : defaultScopes,
58
74
  isConfidential: client?.isConfidential ?? true,
59
75
  });
60
76
 
@@ -117,7 +133,7 @@ export function OAuthClientForm({ client, onSubmit, onCancel, isLoading = false
117
133
  id="name"
118
134
  value={formState.name}
119
135
  onChange={(e) => setFormState((s) => ({ ...s, name: e.target.value }))}
120
- placeholder="My Lightroom Plugin"
136
+ placeholder="My Application"
121
137
  disabled={isLoading}
122
138
  className={errors.name ? "border-destructive" : ""}
123
139
  />
@@ -149,6 +165,7 @@ export function OAuthClientForm({ client, onSubmit, onCancel, isLoading = false
149
165
  <OAuthScopeSelector
150
166
  value={formState.allowedScopes}
151
167
  onChange={(scopes) => setFormState((s) => ({ ...s, allowedScopes: scopes }))}
168
+ availableScopes={availableScopes}
152
169
  error={errors.allowedScopes}
153
170
  disabled={isLoading}
154
171
  />
@@ -0,0 +1,63 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { OAuthClientForm } from "../OAuthClientForm";
4
+ import { OAUTH_SCOPE_DISPLAY, OAuthClientInterface } from "../../interfaces/oauth.interface";
5
+
6
+ const scopeCheckbox = (name: string) => screen.getByRole("checkbox", { name: new RegExp(name) });
7
+
8
+ const makeClient = (allowedScopes: string[]): OAuthClientInterface =>
9
+ ({
10
+ name: "Existing app",
11
+ description: undefined,
12
+ redirectUris: ["https://example.com/callback"],
13
+ allowedScopes,
14
+ isConfidential: true,
15
+ }) as unknown as OAuthClientInterface;
16
+
17
+ describe("OAuthClientForm", () => {
18
+ it("renders only the scopes passed in availableScopes", () => {
19
+ render(<OAuthClientForm onSubmit={vi.fn()} onCancel={vi.fn()} availableScopes={[OAUTH_SCOPE_DISPLAY.mcp]} />);
20
+
21
+ expect(screen.getByText("MCP Server Access")).toBeTruthy();
22
+ expect(screen.queryByText("Read Access")).toBeNull();
23
+ expect(screen.queryByText("Write Access")).toBeNull();
24
+ });
25
+
26
+ it("preselects defaultScopes in create mode", () => {
27
+ render(
28
+ <OAuthClientForm
29
+ onSubmit={vi.fn()}
30
+ onCancel={vi.fn()}
31
+ availableScopes={[OAUTH_SCOPE_DISPLAY.read, OAUTH_SCOPE_DISPLAY.mcp]}
32
+ defaultScopes={["mcp"]}
33
+ />,
34
+ );
35
+
36
+ expect(scopeCheckbox("MCP Server Access").getAttribute("aria-checked")).toBe("true");
37
+ expect(scopeCheckbox("Read Access").getAttribute("aria-checked")).toBe("false");
38
+ });
39
+
40
+ it("uses client.allowedScopes instead of defaultScopes in edit mode", () => {
41
+ render(
42
+ <OAuthClientForm
43
+ client={makeClient(["read"])}
44
+ onSubmit={vi.fn()}
45
+ onCancel={vi.fn()}
46
+ availableScopes={[OAUTH_SCOPE_DISPLAY.read, OAUTH_SCOPE_DISPLAY.mcp]}
47
+ defaultScopes={["mcp"]}
48
+ />,
49
+ );
50
+
51
+ expect(scopeCheckbox("Read Access").getAttribute("aria-checked")).toBe("true");
52
+ expect(scopeCheckbox("MCP Server Access").getAttribute("aria-checked")).toBe("false");
53
+ });
54
+
55
+ it("offers the generic scope list by default, without Only35 scopes", () => {
56
+ render(<OAuthClientForm onSubmit={vi.fn()} onCancel={vi.fn()} />);
57
+
58
+ expect(screen.getByText("MCP Server Access")).toBeTruthy();
59
+ expect(screen.getByText("Read Access")).toBeTruthy();
60
+ expect(screen.queryByText("View Photographs")).toBeNull();
61
+ expect(screen.queryByText("View Rolls")).toBeNull();
62
+ });
63
+ });
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { useMemo } from "react";
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../shadcnui";
6
+ import { MicroLabel } from "../../../../components/typography";
7
+
8
+ export interface OAuthCompanySelectorProps {
9
+ /** The user's companies (studios) */
10
+ companies: Array<{ id: string; name: string }>;
11
+ /** The selected company id */
12
+ value: string | undefined;
13
+ /** Called with the newly selected company id */
14
+ onChange: (id: string) => void;
15
+ }
16
+
17
+ /**
18
+ * Studio (company) picker for the OAuth consent screen.
19
+ * Renders nothing when the user belongs to a single company.
20
+ */
21
+ export function OAuthCompanySelector({ companies, value, onChange }: OAuthCompanySelectorProps) {
22
+ const t = useTranslations();
23
+
24
+ const items = useMemo<Record<string, string>>(
25
+ () => Object.fromEntries(companies.map((company) => [company.id, company.name])),
26
+ [companies],
27
+ );
28
+
29
+ if (companies.length <= 1) return null;
30
+
31
+ return (
32
+ <div className="space-y-3">
33
+ <MicroLabel as="h3">{t("oauth.consent.company_label")}</MicroLabel>
34
+ <Select
35
+ items={items}
36
+ value={value ?? null}
37
+ onValueChange={(selected) => {
38
+ if (selected) onChange(String(selected));
39
+ }}
40
+ >
41
+ <SelectTrigger className="w-full" aria-label={t("oauth.consent.company_label")}>
42
+ <SelectValue placeholder={t("oauth.consent.company_label")} />
43
+ </SelectTrigger>
44
+ <SelectContent>
45
+ {companies.map((company) => (
46
+ <SelectItem key={company.id} value={company.id}>
47
+ {company.name}
48
+ </SelectItem>
49
+ ))}
50
+ </SelectContent>
51
+ </Select>
52
+ </div>
53
+ );
54
+ }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { Button } from "../../../../shadcnui";
4
5
 
5
6
  export interface OAuthConsentActionsProps {
@@ -15,13 +16,15 @@ export interface OAuthConsentActionsProps {
15
16
  * Action buttons for OAuth consent screen
16
17
  */
17
18
  export function OAuthConsentActions({ onApprove, onDeny, isLoading = false }: OAuthConsentActionsProps) {
19
+ const t = useTranslations();
20
+
18
21
  return (
19
22
  <div className="flex flex-col sm:flex-row gap-3">
20
23
  <Button variant="outline" onClick={onDeny} disabled={isLoading} className="flex-1">
21
- Deny
24
+ {t("oauth.consent.deny")}
22
25
  </Button>
23
26
  <Button onClick={onApprove} disabled={isLoading} className="flex-1">
24
- {isLoading ? "Authorizing..." : "Authorize"}
27
+ {isLoading ? t("oauth.consent.authorizing") : t("oauth.consent.authorize")}
25
28
  </Button>
26
29
  </div>
27
30
  );
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
 
3
3
  import { Shield } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { ContentTitle } from "../../../../components";
4
6
  import { OAuthClientInterface } from "../../interfaces/oauth.interface";
5
7
 
6
8
  export interface OAuthConsentHeaderProps {
@@ -8,15 +10,17 @@ export interface OAuthConsentHeaderProps {
8
10
  client: OAuthClientInterface;
9
11
  /** Optional logo URL override */
10
12
  logoUrl?: string;
11
- /** Application name (e.g., "Only35") */
12
- appName?: string;
13
+ /** Application name shown in the header */
14
+ appName: string;
13
15
  }
14
16
 
15
17
  /**
16
18
  * Header component for OAuth consent screen
17
19
  * Shows platform logo and requesting app information
18
20
  */
19
- export function OAuthConsentHeader({ client, logoUrl, appName = "Only35" }: OAuthConsentHeaderProps) {
21
+ export function OAuthConsentHeader({ client, logoUrl, appName }: OAuthConsentHeaderProps) {
22
+ const t = useTranslations();
23
+
20
24
  return (
21
25
  <div className="text-center space-y-4">
22
26
  {/* Platform Logo */}
@@ -32,9 +36,9 @@ export function OAuthConsentHeader({ client, logoUrl, appName = "Only35" }: OAut
32
36
 
33
37
  {/* Authorization Request */}
34
38
  <div className="space-y-2">
35
- <h1 className="text-primary text-3xl font-semibold">Authorize {client.name}</h1>
36
- <p className="text-muted-foreground">
37
- <span className="font-medium text-foreground">{client.name}</span> wants to access your {appName} account
39
+ <ContentTitle element={t("oauth.consent.title", { client: client.name })} className="mb-0 justify-center" />
40
+ <p className="text-muted-foreground text-sm">
41
+ {t("oauth.consent.wants_access", { client: client.name, appName })}
38
42
  </p>
39
43
  </div>
40
44
  </div>
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import { ExternalLink, AlertTriangle, Loader2 } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
4
5
  import { Card, CardContent, CardFooter, Separator, Alert, AlertDescription } from "../../../../shadcnui";
5
6
  import { OAuthConsentHeader } from "./OAuthConsentHeader";
6
7
  import { OAuthScopeList } from "./OAuthScopeList";
7
8
  import { OAuthConsentActions } from "./OAuthConsentActions";
9
+ import { OAuthCompanySelector } from "./OAuthCompanySelector";
8
10
  import { useOAuthConsent } from "../../hooks/useOAuthConsent";
9
11
  import { OAuthConsentRequest } from "../../interfaces/oauth.interface";
10
12
 
@@ -13,12 +15,17 @@ export interface OAuthConsentScreenProps {
13
15
  params: OAuthConsentRequest;
14
16
  /** Optional platform logo URL */
15
17
  logoUrl?: string;
16
- /** Platform name */
17
- appName?: string;
18
+ /** Platform name, shown on the consent screen */
19
+ appName: string;
18
20
  /** Terms of Service URL */
19
21
  termsUrl?: string;
20
22
  /** Privacy Policy URL */
21
23
  privacyUrl?: string;
24
+ /**
25
+ * The studio open in the host app's session, used as the default studio
26
+ * choice. Needed by apps that do not mount the package UserProvider.
27
+ */
28
+ defaultCompanyId?: string;
22
29
  }
23
30
 
24
31
  /**
@@ -40,11 +47,16 @@ export interface OAuthConsentScreenProps {
40
47
  export function OAuthConsentScreen({
41
48
  params,
42
49
  logoUrl,
43
- appName = "Only35",
50
+ appName,
44
51
  termsUrl = "/terms",
45
52
  privacyUrl = "/privacy",
53
+ defaultCompanyId,
46
54
  }: OAuthConsentScreenProps) {
47
- const { clientInfo, isLoading, error, approve, deny, isSubmitting } = useOAuthConsent(params);
55
+ const t = useTranslations();
56
+ const { clientInfo, isLoading, error, approve, deny, isSubmitting, companyId, setCompanyId } = useOAuthConsent(
57
+ params,
58
+ { defaultCompanyId },
59
+ );
48
60
 
49
61
  // Loading state
50
62
  if (isLoading) {
@@ -53,7 +65,7 @@ export function OAuthConsentScreen({
53
65
  <Card className="w-full max-w-md">
54
66
  <CardContent className="flex flex-col items-center justify-center py-12">
55
67
  <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
56
- <p className="mt-4 text-muted-foreground">Loading authorization request...</p>
68
+ <p className="mt-4 text-muted-foreground text-sm">{t("oauth.consent.loading")}</p>
57
69
  </CardContent>
58
70
  </Card>
59
71
  </div>
@@ -68,9 +80,7 @@ export function OAuthConsentScreen({
68
80
  <CardContent className="py-8">
69
81
  <Alert variant="destructive">
70
82
  <AlertTriangle className="h-4 w-4" />
71
- <AlertDescription>
72
- {error?.message || "Invalid authorization request. Please try again."}
73
- </AlertDescription>
83
+ <AlertDescription>{error?.message || t("oauth.consent.invalid_request")}</AlertDescription>
74
84
  </Alert>
75
85
  </CardContent>
76
86
  </Card>
@@ -79,6 +89,7 @@ export function OAuthConsentScreen({
79
89
  }
80
90
 
81
91
  const { client, scopes } = clientInfo;
92
+ const companies = clientInfo.companies ?? [];
82
93
 
83
94
  return (
84
95
  <div className="min-h-dvh flex items-center justify-center p-4 bg-muted/30">
@@ -94,11 +105,20 @@ export function OAuthConsentScreen({
94
105
 
95
106
  <Separator />
96
107
 
108
+ {/* Studio */}
109
+ {companies.length > 1 && (
110
+ <>
111
+ <OAuthCompanySelector companies={companies} value={companyId} onChange={setCompanyId} />
112
+
113
+ <Separator />
114
+ </>
115
+ )}
116
+
97
117
  {/* Redirect URI Notice */}
98
118
  <div className="flex items-start gap-2 text-sm text-muted-foreground">
99
119
  <ExternalLink className="h-4 w-4 mt-0.5 flex-shrink-0" />
100
120
  <div>
101
- <span>Authorizing will redirect you to:</span>
121
+ <span>{t("oauth.consent.redirect_notice")}</span>
102
122
  <p className="tabular-nums text-xs mt-1 break-all">{params.redirectUri}</p>
103
123
  </div>
104
124
  </div>
@@ -110,13 +130,13 @@ export function OAuthConsentScreen({
110
130
  {/* Footer */}
111
131
  <CardFooter className="justify-center">
112
132
  <p className="text-xs text-center text-muted-foreground">
113
- By authorizing, you agree to the app's{" "}
133
+ {t("oauth.consent.agreement")}{" "}
114
134
  <a href={termsUrl} className="hover:text-foreground" target="_blank" rel="noopener">
115
- Terms of Service
135
+ {t("oauth.consent.terms")}
116
136
  </a>{" "}
117
- and{" "}
137
+ {t("oauth.consent.and")}{" "}
118
138
  <a href={privacyUrl} className="hover:text-foreground" target="_blank" rel="noopener">
119
- Privacy Policy
139
+ {t("oauth.consent.privacy")}
120
140
  </a>
121
141
  .
122
142
  </p>
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { Eye, Pencil, Image, Upload, Film, FolderPlus, User, Shield, LucideIcon } from "lucide-react";
4
5
  import { OAuthScopeInfo } from "../../interfaces/oauth.interface";
5
6
  import { MicroLabel } from "../../../../components/typography";
@@ -25,13 +26,15 @@ const SCOPE_ICONS: Record<string, LucideIcon> = {
25
26
  * List of requested OAuth scopes for consent display
26
27
  */
27
28
  export function OAuthScopeList({ scopes }: OAuthScopeListProps) {
29
+ const t = useTranslations();
30
+
28
31
  if (scopes.length === 0) {
29
32
  return null;
30
33
  }
31
34
 
32
35
  return (
33
36
  <div className="space-y-3">
34
- <MicroLabel as="h3">This will allow the application to:</MicroLabel>
37
+ <MicroLabel as="h3">{t("oauth.consent.scopes_heading")}</MicroLabel>
35
38
  <ul className="space-y-3">
36
39
  {scopes.map((scope) => {
37
40
  const IconComponent = scope.icon ? SCOPE_ICONS[scope.icon] : Eye;
@@ -0,0 +1,25 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { OAuthConsentHeader } from "../OAuthConsentHeader";
4
+
5
+ const CLIENT = { id: "client-1", name: "Claude" } as any;
6
+
7
+ describe("OAuthConsentHeader", () => {
8
+ it("renders the consent title through ContentTitle (page-title recipe), not a raw <h1>", () => {
9
+ const { container } = render(<OAuthConsentHeader client={CLIENT} appName="Test" />);
10
+
11
+ expect(container.querySelector("h1")).toBeNull();
12
+ const title = screen.getByText("oauth.consent.title");
13
+ expect(title.className).toContain("text-primary text-3xl font-semibold");
14
+ });
15
+
16
+ it("keeps the title centred with no extra bottom margin", () => {
17
+ const { container } = render(<OAuthConsentHeader client={CLIENT} appName="Test" />);
18
+
19
+ const wrapper = screen.getByText("oauth.consent.title").parentElement?.parentElement as HTMLElement;
20
+ expect(wrapper.className).toContain("justify-center");
21
+ expect(wrapper.className).toContain("mb-0");
22
+ expect(wrapper.className).not.toContain("mb-4");
23
+ expect(container.firstElementChild?.className).toContain("text-center");
24
+ });
25
+ });