diff --git a/.changeset/sign-in-marks.md b/.changeset/sign-in-marks.md new file mode 100644 index 0000000..95d2202 --- /dev/null +++ b/.changeset/sign-in-marks.md @@ -0,0 +1,29 @@ +--- +"seamless-templates": patch +--- + +feat(kit): mark the generated sign-in screen with Seamless Auth and Seamless Idea + +Every generated application is shared with the handful of people it was made +for, and every one of them signs in. That screen is the largest owned surface the +company has, it grows with the fleet, it costs nothing, and it said nothing at +all. + +`AuthFrame` now carries two lines under the form: + +- **Secured by Seamless Auth**, to `seamlessauth.com`. Most of the people + reading it are about to use passwordless sign in for the first time, and + naming it is the only distribution the auth product gets from the fleet. +- **Made with Seamless Idea**, to `seamlessidea.com`. + +Restraint is the whole design. They sit under the sign-in screens rather than +beside them, at footnote size in the muted ink role, centred, with no logo and no +lockup. A generated application has to feel like its owner's, and a banner across +the top would undo the thing that makes personal software worth having. Both +links open in a new tab so that nobody mid-sign-in loses the page, and they carry +`noopener` without `noreferrer`, because the referrer is the only way either +product ever sees that a generated application sent someone. + +There is no switch to turn them off. The templates are copied into the owner's +project, so the lines can always be deleted; whether a paid tier removes them is +a decision for the CLI rather than a prop on the kit. diff --git a/shared/react-app/components/kit/AuthFrame.test.tsx b/shared/react-app/components/kit/AuthFrame.test.tsx new file mode 100644 index 0000000..99ad01a --- /dev/null +++ b/shared/react-app/components/kit/AuthFrame.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import AuthFrame from "./AuthFrame"; + +function renderFrame() { + return render( + + + , + ); +} + +describe("AuthFrame", () => { + it("carries both marks, each linked out", () => { + renderFrame(); + + expect( + screen.getByRole("link", { name: "Secured by Seamless Auth" }), + ).toHaveAttribute("href", "https://seamlessauth.com"); + expect( + screen.getByRole("link", { name: "Made with Seamless Idea" }), + ).toHaveAttribute("href", "https://seamlessidea.com"); + }); + + it("keeps the marks behind the sign-in screens", () => { + renderFrame(); + + const form = screen.getByRole("button", { name: "Send me a link" }); + const mark = screen.getByRole("link", { name: "Secured by Seamless Auth" }); + + expect( + form.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + }); +}); diff --git a/shared/react-app/components/kit/AuthFrame.tsx b/shared/react-app/components/kit/AuthFrame.tsx index 3d094d8..e63a94b 100644 --- a/shared/react-app/components/kit/AuthFrame.tsx +++ b/shared/react-app/components/kit/AuthFrame.tsx @@ -5,9 +5,10 @@ import type { AuthFrameProps } from "./types"; * The signed-out surface: a pitch on one half, the sign-in screens on the other. * * `children` is the auth package's own screens. They are rendered once and - * untouched; nothing here adds an input, a button or a link of its own, because - * the form is the point of the page and a second call to action beside it is a - * way of losing people. + * untouched; nothing here adds an input or a button of its own, because the form + * is the point of the page and a second call to action beside it is a way of + * losing people. The two marks under the form are the one exception, and their + * weight is what keeps them from being one. * * This page is outside the application shell, so it mounts the backdrop itself. */ @@ -46,7 +47,35 @@ export default function AuthFrame({
-
{children}
+
+ {children} + + {/* Under the form and at footnote weight, because the application has + to read as the owner's rather than as a demo of ours. It is the + mark a printer leaves on an invitation, not a banner. + + `noopener` without `noreferrer`: the referrer is the only way + either product sees that a generated application sent someone. */} +

+ + Secured by Seamless Auth + + · + + Made with Seamless Idea + +

+
); diff --git a/templates/web/react-oauth/src/components/kit/AuthFrame.test.tsx b/templates/web/react-oauth/src/components/kit/AuthFrame.test.tsx new file mode 100644 index 0000000..99ad01a --- /dev/null +++ b/templates/web/react-oauth/src/components/kit/AuthFrame.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import AuthFrame from "./AuthFrame"; + +function renderFrame() { + return render( + + + , + ); +} + +describe("AuthFrame", () => { + it("carries both marks, each linked out", () => { + renderFrame(); + + expect( + screen.getByRole("link", { name: "Secured by Seamless Auth" }), + ).toHaveAttribute("href", "https://seamlessauth.com"); + expect( + screen.getByRole("link", { name: "Made with Seamless Idea" }), + ).toHaveAttribute("href", "https://seamlessidea.com"); + }); + + it("keeps the marks behind the sign-in screens", () => { + renderFrame(); + + const form = screen.getByRole("button", { name: "Send me a link" }); + const mark = screen.getByRole("link", { name: "Secured by Seamless Auth" }); + + expect( + form.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + }); +}); diff --git a/templates/web/react-oauth/src/components/kit/AuthFrame.tsx b/templates/web/react-oauth/src/components/kit/AuthFrame.tsx index 3d094d8..e63a94b 100644 --- a/templates/web/react-oauth/src/components/kit/AuthFrame.tsx +++ b/templates/web/react-oauth/src/components/kit/AuthFrame.tsx @@ -5,9 +5,10 @@ import type { AuthFrameProps } from "./types"; * The signed-out surface: a pitch on one half, the sign-in screens on the other. * * `children` is the auth package's own screens. They are rendered once and - * untouched; nothing here adds an input, a button or a link of its own, because - * the form is the point of the page and a second call to action beside it is a - * way of losing people. + * untouched; nothing here adds an input or a button of its own, because the form + * is the point of the page and a second call to action beside it is a way of + * losing people. The two marks under the form are the one exception, and their + * weight is what keeps them from being one. * * This page is outside the application shell, so it mounts the backdrop itself. */ @@ -46,7 +47,35 @@ export default function AuthFrame({
-
{children}
+
+ {children} + + {/* Under the form and at footnote weight, because the application has + to read as the owner's rather than as a demo of ours. It is the + mark a printer leaves on an invitation, not a banner. + + `noopener` without `noreferrer`: the referrer is the only way + either product sees that a generated application sent someone. */} +

+ + Secured by Seamless Auth + + · + + Made with Seamless Idea + +

+
); diff --git a/templates/web/react-vite/src/components/kit/AuthFrame.test.tsx b/templates/web/react-vite/src/components/kit/AuthFrame.test.tsx new file mode 100644 index 0000000..99ad01a --- /dev/null +++ b/templates/web/react-vite/src/components/kit/AuthFrame.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import AuthFrame from "./AuthFrame"; + +function renderFrame() { + return render( + + + , + ); +} + +describe("AuthFrame", () => { + it("carries both marks, each linked out", () => { + renderFrame(); + + expect( + screen.getByRole("link", { name: "Secured by Seamless Auth" }), + ).toHaveAttribute("href", "https://seamlessauth.com"); + expect( + screen.getByRole("link", { name: "Made with Seamless Idea" }), + ).toHaveAttribute("href", "https://seamlessidea.com"); + }); + + it("keeps the marks behind the sign-in screens", () => { + renderFrame(); + + const form = screen.getByRole("button", { name: "Send me a link" }); + const mark = screen.getByRole("link", { name: "Secured by Seamless Auth" }); + + expect( + form.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + }); +}); diff --git a/templates/web/react-vite/src/components/kit/AuthFrame.tsx b/templates/web/react-vite/src/components/kit/AuthFrame.tsx index 3d094d8..e63a94b 100644 --- a/templates/web/react-vite/src/components/kit/AuthFrame.tsx +++ b/templates/web/react-vite/src/components/kit/AuthFrame.tsx @@ -5,9 +5,10 @@ import type { AuthFrameProps } from "./types"; * The signed-out surface: a pitch on one half, the sign-in screens on the other. * * `children` is the auth package's own screens. They are rendered once and - * untouched; nothing here adds an input, a button or a link of its own, because - * the form is the point of the page and a second call to action beside it is a - * way of losing people. + * untouched; nothing here adds an input or a button of its own, because the form + * is the point of the page and a second call to action beside it is a way of + * losing people. The two marks under the form are the one exception, and their + * weight is what keeps them from being one. * * This page is outside the application shell, so it mounts the backdrop itself. */ @@ -46,7 +47,35 @@ export default function AuthFrame({
-
{children}
+
+ {children} + + {/* Under the form and at footnote weight, because the application has + to read as the owner's rather than as a demo of ours. It is the + mark a printer leaves on an invitation, not a banner. + + `noopener` without `noreferrer`: the referrer is the only way + either product sees that a generated application sent someone. */} +

+ + Secured by Seamless Auth + + · + + Made with Seamless Idea + +

+
);