Skip to content

Commit 5db38b0

Browse files
authored
Merge pull request #1218 from firebase/@invertase/react-policies
2 parents a609c14 + e403d86 commit 5db38b0

File tree

8 files changed

+145
-73
lines changed

8 files changed

+145
-73
lines changed

packages/react/src/auth/screens/email-link-auth-screen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function EmailLinkAuthScreen({ children, onEmailSent }: EmailLinkAuthScre
4141
{children ? (
4242
<>
4343
<Divider>{getTranslation(ui, "messages", "dividerOr")}</Divider>
44-
<div className="space-y-4">{children}</div>
44+
<div className="fui-screen__children">{children}</div>
4545
</>
4646
) : null}
4747
</CardContent>

packages/react/src/auth/screens/oauth-screen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function OAuthScreen({ children }: OAuthScreenProps) {
3535
<CardTitle>{titleText}</CardTitle>
3636
<CardSubtitle>{subtitleText}</CardSubtitle>
3737
</CardHeader>
38-
<CardContent>
38+
<CardContent className="fui-screen__children">
3939
{children}
4040
<Policies />
4141
</CardContent>

packages/react/src/auth/screens/phone-auth-screen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function PhoneAuthScreen({ children, ...props }: PhoneAuthScreenProps) {
4141
{children ? (
4242
<>
4343
<Divider>{getTranslation(ui, "messages", "dividerOr")}</Divider>
44-
<div className="space-y-4">{children}</div>
44+
<div className="fui-screen__children">{children}</div>
4545
</>
4646
) : null}
4747
</CardContent>

packages/react/src/auth/screens/sign-in-auth-screen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function SignInAuthScreen({ children, ...props }: SignInAuthScreenProps)
4141
{children ? (
4242
<>
4343
<Divider>{getTranslation(ui, "messages", "dividerOr")}</Divider>
44-
<div className="space-y-4">{children}</div>
44+
<div className="fui-screen__children">{children}</div>
4545
</>
4646
) : null}
4747
</CardContent>

packages/react/src/auth/screens/sign-up-auth-screen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function SignUpAuthScreen({ children, ...props }: SignUpAuthScreenProps)
4141
{children ? (
4242
<>
4343
<Divider>{getTranslation(ui, "messages", "dividerOr")}</Divider>
44-
<div className="space-y-4">{children}</div>
44+
<div className="fui-screen__children">{children}</div>
4545
</>
4646
) : null}
4747
</CardContent>

packages/react/src/components/policies.test.tsx

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { describe, it, expect, vi, beforeEach } from "vitest";
18-
import { render, screen } from "@testing-library/react";
17+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
18+
import { render, fireEvent, cleanup } from "@testing-library/react";
1919
import { Policies } from "./policies";
2020
import { FirebaseUIProvider } from "~/context";
2121
import { createMockUI } from "~/tests/utils";
@@ -25,8 +25,12 @@ describe("<Policies />", () => {
2525
vi.clearAllMocks();
2626
});
2727

28-
it("renders component with terms and privacy links", () => {
29-
render(
28+
afterEach(() => {
29+
cleanup();
30+
});
31+
32+
it("renders component with terms and privacy links using anchor tags", () => {
33+
const { container } = render(
3034
<FirebaseUIProvider
3135
ui={createMockUI()}
3236
policies={{
@@ -39,27 +43,117 @@ describe("<Policies />", () => {
3943
);
4044

4145
// Check that the text and links are rendered
42-
expect(screen.getByText(/By continuing, you agree to our/)).toBeInTheDocument();
46+
expect(container.querySelector(".fui-policies")).toBeInTheDocument();
4347

44-
const tosLink = screen.getByText("Terms of Service");
48+
const tosLink = container.querySelector('a[href="https://example.com/terms"]');
4549
expect(tosLink).toBeInTheDocument();
46-
expect(tosLink.tagName).toBe("A");
50+
expect(tosLink?.tagName).toBe("A");
4751
expect(tosLink).toHaveAttribute("target", "_blank");
4852
expect(tosLink).toHaveAttribute("rel", "noopener noreferrer");
53+
expect(tosLink).toHaveTextContent("Terms of Service");
4954

50-
const privacyLink = screen.getByText("Privacy Policy");
55+
const privacyLink = container.querySelector('a[href="https://example.com/privacy"]');
5156
expect(privacyLink).toBeInTheDocument();
52-
expect(privacyLink.tagName).toBe("A");
57+
expect(privacyLink?.tagName).toBe("A");
5358
expect(privacyLink).toHaveAttribute("target", "_blank");
5459
expect(privacyLink).toHaveAttribute("rel", "noopener noreferrer");
60+
expect(privacyLink).toHaveTextContent("Privacy Policy");
61+
});
62+
63+
it("renders component with custom navigation handler using buttons", () => {
64+
const mockNavigate = vi.fn();
65+
const { container } = render(
66+
<FirebaseUIProvider
67+
ui={createMockUI()}
68+
policies={{
69+
termsOfServiceUrl: "https://example.com/terms",
70+
privacyPolicyUrl: "https://example.com/privacy",
71+
onNavigate: mockNavigate,
72+
}}
73+
>
74+
<Policies />
75+
</FirebaseUIProvider>
76+
);
77+
78+
// Check that the text and buttons are rendered
79+
expect(container.querySelector(".fui-policies")).toBeInTheDocument();
80+
81+
const tosButton = container.querySelector("button");
82+
expect(tosButton).toBeInTheDocument();
83+
expect(tosButton?.tagName).toBe("BUTTON");
84+
expect(tosButton).not.toHaveAttribute("href");
85+
expect(tosButton).not.toHaveAttribute("target");
86+
expect(tosButton).toHaveTextContent("Terms of Service");
87+
88+
const privacyButton = container.querySelectorAll("button")[1];
89+
expect(privacyButton).toBeInTheDocument();
90+
expect(privacyButton?.tagName).toBe("BUTTON");
91+
expect(privacyButton).not.toHaveAttribute("href");
92+
expect(privacyButton).not.toHaveAttribute("target");
93+
expect(privacyButton).toHaveTextContent("Privacy Policy");
94+
95+
fireEvent.click(tosButton!);
96+
expect(mockNavigate).toHaveBeenCalledWith("https://example.com/terms");
97+
98+
fireEvent.click(privacyButton!);
99+
expect(mockNavigate).toHaveBeenCalledWith("https://example.com/privacy");
100+
});
101+
102+
it("handles URL objects correctly", () => {
103+
const termsUrl = new URL("https://example.com/terms");
104+
const privacyUrl = new URL("https://example.com/privacy");
105+
const { container } = render(
106+
<FirebaseUIProvider
107+
ui={createMockUI()}
108+
policies={{
109+
termsOfServiceUrl: termsUrl,
110+
privacyPolicyUrl: privacyUrl,
111+
}}
112+
>
113+
<Policies />
114+
</FirebaseUIProvider>
115+
);
116+
117+
const tosLink = container.querySelector('a[href="https://example.com/terms"]');
118+
expect(tosLink).toHaveAttribute("href", "https://example.com/terms");
119+
120+
const privacyLink = container.querySelector('a[href="https://example.com/privacy"]');
121+
expect(privacyLink).toHaveAttribute("href", "https://example.com/privacy");
55122
});
56123

57-
it("returns null when both tosUrl and privacyPolicyUrl are not provided", () => {
124+
it("returns null when policies are not provided", () => {
58125
const { container } = render(
59126
<FirebaseUIProvider ui={createMockUI()} policies={undefined}>
60127
<Policies />
61128
</FirebaseUIProvider>
62129
);
63130
expect(container).toBeEmptyDOMElement();
64131
});
132+
133+
it("handles custom navigation with URL objects", () => {
134+
const mockNavigate = vi.fn();
135+
const termsUrl = new URL("https://example.com/terms");
136+
const privacyUrl = new URL("https://example.com/privacy");
137+
const { container } = render(
138+
<FirebaseUIProvider
139+
ui={createMockUI()}
140+
policies={{
141+
termsOfServiceUrl: termsUrl,
142+
privacyPolicyUrl: privacyUrl,
143+
onNavigate: mockNavigate,
144+
}}
145+
>
146+
<Policies />
147+
</FirebaseUIProvider>
148+
);
149+
150+
const tosButton = container.querySelector("button");
151+
const privacyButton = container.querySelectorAll("button")[1];
152+
153+
fireEvent.click(tosButton!);
154+
expect(mockNavigate).toHaveBeenCalledWith(termsUrl);
155+
156+
fireEvent.click(privacyButton!);
157+
expect(mockNavigate).toHaveBeenCalledWith(privacyUrl);
158+
});
65159
});

packages/react/src/components/policies.tsx

Lines changed: 21 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,15 @@
1515
*/
1616

1717
import { getTranslation } from "@firebase-ui/core";
18-
import { createContext, useContext } from "react";
18+
import { cloneElement, createContext, useContext } from "react";
1919
import { useUI } from "~/hooks";
2020

21-
export type PolicyURL =
22-
| string
23-
| URL
24-
| (() => string | URL | void)
25-
| Promise<string | URL | void>
26-
| (() => Promise<string | URL | void>);
21+
export type PolicyURL = string | URL;
2722

2823
export interface PolicyProps {
2924
termsOfServiceUrl: PolicyURL;
3025
privacyPolicyUrl: PolicyURL;
26+
onNavigate?: (url: PolicyURL) => void;
3127
}
3228

3329
const PolicyContext = createContext<PolicyProps | undefined>(undefined);
@@ -44,64 +40,33 @@ export function Policies() {
4440
return null;
4541
}
4642

47-
const { termsOfServiceUrl, privacyPolicyUrl } = policies;
48-
49-
async function handleUrl(urlOrFunction: PolicyURL) {
50-
let url: string | URL | void;
51-
52-
if (typeof urlOrFunction === "function") {
53-
const urlOrPromise = urlOrFunction();
54-
if (typeof urlOrPromise === "string" || urlOrPromise instanceof URL) {
55-
url = urlOrPromise;
56-
} else {
57-
url = await urlOrPromise;
58-
}
59-
} else if (urlOrFunction instanceof Promise) {
60-
url = await urlOrFunction;
61-
} else {
62-
url = urlOrFunction;
63-
}
64-
65-
if (url) {
66-
window.open(url.toString(), "_blank");
67-
}
68-
}
69-
70-
const termsText = getTranslation(ui, "labels", "termsOfService");
71-
const privacyText = getTranslation(ui, "labels", "privacyPolicy");
43+
const { termsOfServiceUrl, privacyPolicyUrl, onNavigate } = policies;
7244
const termsAndPrivacyText = getTranslation(ui, "messages", "termsAndPrivacy");
73-
7445
const parts = termsAndPrivacyText.split(/(\{tos\}|\{privacy\})/);
7546

47+
const Handler = onNavigate ? <button /> : <a target="_blank" rel="noopener noreferrer" />;
48+
7649
return (
77-
<div className="text-text-muted text-xs text-start">
50+
<div className="fui-policies">
7851
{parts.map((part: string, index: number) => {
7952
if (part === "{tos}") {
80-
return (
81-
<a
82-
key={index}
83-
onClick={() => handleUrl(termsOfServiceUrl)}
84-
target="_blank"
85-
rel="noopener noreferrer"
86-
className="text-text-muted hover:underline font-semibold"
87-
>
88-
{termsText}
89-
</a>
90-
);
53+
return cloneElement(Handler, {
54+
key: index,
55+
onClick: onNavigate ? () => onNavigate(termsOfServiceUrl) : undefined,
56+
href: onNavigate ? undefined : termsOfServiceUrl,
57+
children: getTranslation(ui, "labels", "termsOfService"),
58+
});
9159
}
60+
9261
if (part === "{privacy}") {
93-
return (
94-
<a
95-
key={index}
96-
onClick={() => handleUrl(privacyPolicyUrl)}
97-
target="_blank"
98-
rel="noopener noreferrer"
99-
className="text-text-muted hover:underline font-semibold"
100-
>
101-
{privacyText}
102-
</a>
103-
);
62+
return cloneElement(Handler, {
63+
key: index,
64+
onClick: onNavigate ? () => onNavigate(privacyPolicyUrl) : undefined,
65+
href: onNavigate ? undefined : privacyPolicyUrl,
66+
children: getTranslation(ui, "labels", "privacyPolicy"),
67+
});
10468
}
69+
10570
return <span key={index}>{part}</span>;
10671
})}
10772
</div>

packages/styles/src/base.css

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@
7979
@apply pt-24 max-w-md mx-auto;
8080
}
8181

82+
:where(.fui-screen .fui-screen__children) {
83+
@apply space-y-2;
84+
}
85+
8286
:where(.fui-card) {
8387
@apply bg-background p-10 border border-border rounded-card space-y-6;
8488
}
@@ -145,15 +149,15 @@
145149
}
146150

147151
:where(.fui-divider) {
148-
@apply flex items-center gap-3;
152+
@apply flex items-center gap-3 my-4;
149153
}
150154

151155
:where(.fui-divider__line) {
152156
@apply flex-1 h-px bg-border;
153157
}
154158

155159
:where(.fui-divider__text) {
156-
@apply text-text-muted text-xs my-2;
160+
@apply text-text-muted text-xs;
157161
}
158162

159163
:where(.fui-phone-input) {
@@ -188,6 +192,15 @@
188192
@apply absolute left-8 top-1/2 -translate-y-1/2 text-sm pointer-events-none text-text;
189193
}
190194

195+
:where(.fui-policies) {
196+
@apply text-text-muted text-center text-xs;
197+
}
198+
199+
:where(.fui-policies a, .fui-policies button) {
200+
@apply hover:underline font-semibold;
201+
}
202+
203+
191204
.fui-provider__button[data-provider="google.com"][data-themed="true"] {
192205
--google-primary: #131314;
193206
--color-primary: var(--google-primary);

0 commit comments

Comments
 (0)