Skip to content

Commit 7a25fc8

Browse files
committed
feat(web): add Account Security section to settings page
Add new Account Security section with links to Clerk account management pages: - Account Profile page for managing email and social logins - Security Settings page for passkeys, passwords, and device management Features: - Uses Clerk's buildUserProfileUrl() with fallback URLs - External link styling with icons and proper security attributes - Consistent layout matching other settings sections - Positioned before Danger Zone as requested - Updated Download section to use consistent flex layout Tests: - Comprehensive test coverage for Account Security component - Updated existing settings tests to include new section - All 876 tests passing
1 parent 2492db8 commit 7a25fc8

5 files changed

Lines changed: 220 additions & 7 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { render, screen } from "@testing-library/react";
3+
import { useClerk } from "@clerk/clerk-react";
4+
import { AccountSecuritySection } from "./account-security-section";
5+
6+
// Mock Clerk
7+
const mockBuildUserProfileUrl = vi.fn();
8+
vi.mock("@clerk/clerk-react", () => ({
9+
useClerk: vi.fn(),
10+
}));
11+
12+
// Mock UI components
13+
vi.mock("@/components/ui/button", () => ({
14+
Button: ({ children, asChild, ...props }: any) => {
15+
if (asChild) {
16+
return (
17+
<div data-testid="button-wrapper" {...props}>
18+
{children}
19+
</div>
20+
);
21+
}
22+
return <button {...props}>{children}</button>;
23+
},
24+
}));
25+
26+
vi.mock("@/components/ui/card", () => ({
27+
CardHeader: ({ children }: any) => <div data-testid="card-header">{children}</div>,
28+
CardTitle: ({ children }: any) => <h2 data-testid="card-title">{children}</h2>,
29+
CardContent: ({ children, className }: any) => (
30+
<div data-testid="card-content" className={className}>
31+
{children}
32+
</div>
33+
),
34+
}));
35+
36+
// Mock lucide-react icon
37+
vi.mock("lucide-react", () => ({
38+
ExternalLink: ({ className }: any) => (
39+
<span data-testid="external-link-icon" className={className}>
40+
🔗
41+
</span>
42+
),
43+
}));
44+
45+
describe("AccountSecuritySection", () => {
46+
beforeEach(() => {
47+
vi.clearAllMocks();
48+
vi.mocked(useClerk).mockReturnValue({
49+
buildUserProfileUrl: mockBuildUserProfileUrl,
50+
});
51+
});
52+
53+
it("should render the section with title and description", () => {
54+
mockBuildUserProfileUrl.mockReturnValue("https://test.clerk.accounts.dev/user");
55+
56+
render(<AccountSecuritySection />);
57+
58+
expect(screen.getByTestId("card-title")).toHaveTextContent("Account Security");
59+
expect(screen.getByText(/TrendWeight uses Clerk to handle authentication/)).toBeInTheDocument();
60+
});
61+
62+
it("should render both account links with Clerk URLs when available", () => {
63+
const clerkProfileUrl = "https://test.clerk.accounts.dev/user";
64+
mockBuildUserProfileUrl.mockReturnValue(clerkProfileUrl);
65+
66+
render(<AccountSecuritySection />);
67+
68+
// Check Account Profile link
69+
const profileLink = screen.getByRole("link", { name: /Open Account Profile/ });
70+
expect(profileLink).toHaveAttribute("href", clerkProfileUrl);
71+
expect(profileLink).toHaveAttribute("target", "_blank");
72+
expect(profileLink).toHaveAttribute("rel", "noopener noreferrer");
73+
74+
// Check Security Settings link
75+
const securityLink = screen.getByRole("link", { name: /Open Security Settings/ });
76+
expect(securityLink).toHaveAttribute("href", "https://test.clerk.accounts.dev/user/security");
77+
expect(securityLink).toHaveAttribute("target", "_blank");
78+
expect(securityLink).toHaveAttribute("rel", "noopener noreferrer");
79+
});
80+
81+
it("should use fallback URLs when Clerk buildUserProfileUrl is not available", () => {
82+
// Mock useClerk to return an object without buildUserProfileUrl
83+
vi.mocked(useClerk).mockReturnValue({});
84+
85+
render(<AccountSecuritySection />);
86+
87+
// Check Account Profile link uses fallback
88+
const profileLink = screen.getByRole("link", { name: /Open Account Profile/ });
89+
expect(profileLink).toHaveAttribute("href", "https://accounts.trendweight.com/user");
90+
91+
// Check Security Settings link uses fallback
92+
const securityLink = screen.getByRole("link", { name: /Open Security Settings/ });
93+
expect(securityLink).toHaveAttribute("href", "https://accounts.trendweight.com/user/security");
94+
});
95+
96+
it("should use fallback URLs when Clerk is not available", () => {
97+
// Mock useClerk to return null
98+
vi.mocked(useClerk).mockReturnValue(null);
99+
100+
render(<AccountSecuritySection />);
101+
102+
// Check Account Profile link uses fallback
103+
const profileLink = screen.getByRole("link", { name: /Open Account Profile/ });
104+
expect(profileLink).toHaveAttribute("href", "https://accounts.trendweight.com/user");
105+
106+
// Check Security Settings link uses fallback
107+
const securityLink = screen.getByRole("link", { name: /Open Security Settings/ });
108+
expect(securityLink).toHaveAttribute("href", "https://accounts.trendweight.com/user/security");
109+
});
110+
111+
it("should render external link icons", () => {
112+
mockBuildUserProfileUrl.mockReturnValue("https://test.clerk.accounts.dev/user");
113+
114+
render(<AccountSecuritySection />);
115+
116+
const icons = screen.getAllByTestId("external-link-icon");
117+
expect(icons).toHaveLength(2);
118+
119+
// Check that icons have the correct styling
120+
icons.forEach((icon) => {
121+
expect(icon).toHaveClass("ml-1", "h-3", "w-3");
122+
});
123+
});
124+
125+
it("should render descriptive text for both sections", () => {
126+
mockBuildUserProfileUrl.mockReturnValue("https://test.clerk.accounts.dev/user");
127+
128+
render(<AccountSecuritySection />);
129+
130+
// Check Account Profile description
131+
expect(screen.getByText(/You can use the profile page to update your email address/)).toBeInTheDocument();
132+
133+
// Check Security Settings description
134+
expect(screen.getByText(/On the security page you can add passkeys/)).toBeInTheDocument();
135+
});
136+
137+
it("should call Clerk buildUserProfileUrl method when available", () => {
138+
mockBuildUserProfileUrl.mockReturnValue("https://test.clerk.accounts.dev/user");
139+
140+
render(<AccountSecuritySection />);
141+
142+
// The component calls buildUserProfileUrl during render to get the profile URL
143+
expect(mockBuildUserProfileUrl).toHaveBeenCalledTimes(1);
144+
});
145+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ExternalLink as ExternalLinkIcon } from "lucide-react";
2+
import { useClerk } from "@clerk/clerk-react";
3+
import { Button } from "@/components/ui/button";
4+
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5+
6+
export function AccountSecuritySection() {
7+
const clerk = useClerk();
8+
9+
// Use Clerk's built-in method if available, otherwise fallback
10+
const profileUrl = clerk?.buildUserProfileUrl?.() || "https://accounts.trendweight.com/user";
11+
const securityUrl = profileUrl.replace("/user", "/user/security");
12+
13+
return (
14+
<>
15+
<CardHeader>
16+
<CardTitle>Account Security</CardTitle>
17+
</CardHeader>
18+
<CardContent className="space-y-4">
19+
<p className="text-muted-foreground text-sm">
20+
TrendWeight uses Clerk to handle authentication and account security. The links below will open pages in a new tab where you can manage your account
21+
details and security settings.
22+
</p>
23+
<div className="border-border flex items-center justify-between space-x-4 rounded-lg border p-4">
24+
<div className="flex-1 pr-4">
25+
<h4 className="text-foreground font-medium">Account Profile</h4>
26+
<p className="text-muted-foreground text-sm">
27+
You can use the profile page to update your email address, add or remove social login methods (Google, Microsoft, Apple), and manage your basic
28+
account information.
29+
</p>
30+
</div>
31+
<Button asChild variant="default" size="sm">
32+
<a href={profileUrl} target="_blank" rel="noopener noreferrer">
33+
Open Account Profile
34+
<ExternalLinkIcon className="ml-1 h-3 w-3" />
35+
</a>
36+
</Button>
37+
</div>
38+
39+
<div className="border-border flex items-center justify-between space-x-4 rounded-lg border p-4">
40+
<div className="flex-1 pr-4">
41+
<h4 className="text-foreground font-medium">Security Settings</h4>
42+
<p className="text-muted-foreground text-sm">
43+
On the security page you can add passkeys for secure login, change your password (or add one if you signed up with social login), and manage your
44+
logged-in devices.
45+
</p>
46+
</div>
47+
<Button asChild variant="default" size="sm">
48+
<a href={securityUrl} target="_blank" rel="noopener noreferrer">
49+
Open Security Settings
50+
<ExternalLinkIcon className="ml-1 h-3 w-3" />
51+
</a>
52+
</Button>
53+
</div>
54+
</CardContent>
55+
</>
56+
);
57+
}

apps/web/src/components/settings/download-section.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Link } from "@tanstack/react-router";
2-
import { useProviderLinks } from "@/lib/api/queries";
31
import { Button } from "@/components/ui/button";
4-
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
2+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3+
import { useProviderLinks } from "@/lib/api/queries";
4+
import { Link } from "@tanstack/react-router";
55

66
export function DownloadSection() {
77
const { data: providerLinks } = useProviderLinks();
@@ -16,12 +16,12 @@ export function DownloadSection() {
1616
<Card className="mb-6">
1717
<CardHeader>
1818
<CardTitle>Download</CardTitle>
19-
<CardDescription>
19+
</CardHeader>
20+
<CardContent className="flex items-center justify-between space-x-4">
21+
<p className="text-muted-foreground flex-1 pr-4 text-sm">
2022
You can view and download all your historical scale readings from your connected providers. Export your data as a CSV file for backup or analysis in
2123
other applications.
22-
</CardDescription>
23-
</CardHeader>
24-
<CardContent>
24+
</p>
2525
<Button asChild variant="default" size="sm">
2626
<Link to="/download">View / Download Your Data</Link>
2727
</Button>

apps/web/src/components/settings/settings.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ vi.mock("@/components/ui/button", () => ({
4747
}));
4848

4949
// Mock section components
50+
vi.mock("./account-security-section", () => ({
51+
AccountSecuritySection: () => <div data-testid="account-security">Account Security</div>,
52+
}));
53+
5054
vi.mock("./advanced-section", () => ({
5155
AdvancedSection: ({ register }: any) => (
5256
<div data-testid="advanced-section">
@@ -128,6 +132,7 @@ describe("Settings", () => {
128132
expect(screen.getByTestId("sharing-section")).toBeInTheDocument();
129133
expect(screen.getByTestId("connected-accounts")).toBeInTheDocument();
130134
expect(screen.getByTestId("download-section")).toBeInTheDocument();
135+
expect(screen.getByTestId("account-security")).toBeInTheDocument();
131136
expect(screen.getByTestId("danger-zone")).toBeInTheDocument();
132137
});
133138

apps/web/src/components/settings/settings.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useNavigationGuard } from "@/lib/hooks/use-navigation-guard";
77
import { NewVersionNotice } from "@/components/notices/new-version-notice";
88
import { Button } from "@/components/ui/button";
99
import { Card } from "@/components/ui/card";
10+
import { AccountSecuritySection } from "./account-security-section";
1011
import { AdvancedSection } from "./advanced-section";
1112
import { ConnectedAccountsSection } from "./connected-accounts-section";
1213
import { DangerZoneSection } from "./danger-zone-section";
@@ -140,6 +141,11 @@ export function Settings() {
140141
{/* Download Card */}
141142
<DownloadSection />
142143

144+
{/* Account Security Card */}
145+
<Card className="mb-6">
146+
<AccountSecuritySection />
147+
</Card>
148+
143149
{/* Danger Zone Card */}
144150
<Card className="border-destructive">
145151
<DangerZoneSection />

0 commit comments

Comments
 (0)