create-ab-app 0.1.1

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 (87) hide show
  1. package/README.md +60 -0
  2. package/index.js +380 -0
  3. package/package.json +36 -0
  4. package/template/.env.example +8 -0
  5. package/template/.oxlintrc.json +8 -0
  6. package/template/README.md +196 -0
  7. package/template/_gitignore +24 -0
  8. package/template/components.json +25 -0
  9. package/template/index.html +13 -0
  10. package/template/package-lock.json +6096 -0
  11. package/template/package.json +40 -0
  12. package/template/public/_redirects +1 -0
  13. package/template/public/favicon.svg +1 -0
  14. package/template/public/icons.svg +24 -0
  15. package/template/src/App.tsx +60 -0
  16. package/template/src/assets/hero.png +0 -0
  17. package/template/src/assets/vite.svg +1 -0
  18. package/template/src/components/app-sidebar.tsx +53 -0
  19. package/template/src/components/credit-balance.tsx +31 -0
  20. package/template/src/components/docs/doc-primitives.tsx +125 -0
  21. package/template/src/components/full-page-loader.tsx +14 -0
  22. package/template/src/components/header-user.tsx +64 -0
  23. package/template/src/components/layouts/app-layout.tsx +80 -0
  24. package/template/src/components/missing-env.tsx +24 -0
  25. package/template/src/components/nav-main.tsx +87 -0
  26. package/template/src/components/nav-user.tsx +69 -0
  27. package/template/src/components/theme-toggle.tsx +39 -0
  28. package/template/src/components/ui/avatar.tsx +106 -0
  29. package/template/src/components/ui/breadcrumb.tsx +124 -0
  30. package/template/src/components/ui/button.tsx +57 -0
  31. package/template/src/components/ui/card.tsx +102 -0
  32. package/template/src/components/ui/collapsible.tsx +19 -0
  33. package/template/src/components/ui/dropdown-menu.tsx +267 -0
  34. package/template/src/components/ui/field.tsx +238 -0
  35. package/template/src/components/ui/input.tsx +19 -0
  36. package/template/src/components/ui/label.tsx +19 -0
  37. package/template/src/components/ui/separator.tsx +22 -0
  38. package/template/src/components/ui/sheet.tsx +136 -0
  39. package/template/src/components/ui/sidebar.tsx +721 -0
  40. package/template/src/components/ui/skeleton.tsx +13 -0
  41. package/template/src/components/ui/sonner.tsx +47 -0
  42. package/template/src/components/ui/tooltip.tsx +65 -0
  43. package/template/src/components/user-menu.tsx +96 -0
  44. package/template/src/config/env.ts +14 -0
  45. package/template/src/config/navigation.ts +52 -0
  46. package/template/src/hooks/use-mobile.ts +19 -0
  47. package/template/src/hooks/use-user-identity.ts +27 -0
  48. package/template/src/index.css +134 -0
  49. package/template/src/lib/http.ts +40 -0
  50. package/template/src/lib/sso/account-settings.tsx +53 -0
  51. package/template/src/lib/sso/auth-context.ts +16 -0
  52. package/template/src/lib/sso/auth-provider.tsx +42 -0
  53. package/template/src/lib/sso/auth-screens.tsx +152 -0
  54. package/template/src/lib/sso/config.ts +13 -0
  55. package/template/src/lib/sso/entitlements.ts +101 -0
  56. package/template/src/lib/sso/index.ts +33 -0
  57. package/template/src/lib/sso/protected-route.tsx +72 -0
  58. package/template/src/lib/sso/queries.ts +55 -0
  59. package/template/src/lib/sso/require-plan.tsx +19 -0
  60. package/template/src/lib/sso/token-store.ts +62 -0
  61. package/template/src/lib/utils.ts +1 -0
  62. package/template/src/main.tsx +42 -0
  63. package/template/src/routes/app/example.tsx +125 -0
  64. package/template/src/routes/auth/forgot-password.tsx +5 -0
  65. package/template/src/routes/auth/login.tsx +5 -0
  66. package/template/src/routes/auth/signup.tsx +5 -0
  67. package/template/src/routes/auth/verify-email.tsx +5 -0
  68. package/template/src/routes/docs/api.tsx +66 -0
  69. package/template/src/routes/docs/auth.tsx +145 -0
  70. package/template/src/routes/docs/billing.tsx +103 -0
  71. package/template/src/routes/docs/configuration.tsx +113 -0
  72. package/template/src/routes/docs/overview.tsx +97 -0
  73. package/template/src/routes/docs/routing.tsx +126 -0
  74. package/template/src/routes/docs/structure.tsx +150 -0
  75. package/template/src/routes/docs/theming.tsx +88 -0
  76. package/template/src/routes/not-found.tsx +12 -0
  77. package/template/src/styles/aas-theme.css +21 -0
  78. package/template/src/vite-env.d.ts +12 -0
  79. package/template/tsconfig.app.json +30 -0
  80. package/template/tsconfig.json +12 -0
  81. package/template/tsconfig.node.json +23 -0
  82. package/template/vercel.json +3 -0
  83. package/template/vite.config.ts +17 -0
  84. package/variants/minimal/src/App.tsx +43 -0
  85. package/variants/minimal/src/components/app-sidebar.tsx +50 -0
  86. package/variants/minimal/src/config/navigation.ts +19 -0
  87. package/variants/minimal/src/routes/app/home.tsx +45 -0
@@ -0,0 +1,5 @@
1
+ import { SsoLogin } from "@/lib/sso"
2
+
3
+ export default function LoginPage() {
4
+ return <SsoLogin />
5
+ }
@@ -0,0 +1,5 @@
1
+ import { SsoSignup } from "@/lib/sso"
2
+
3
+ export default function SignupPage() {
4
+ return <SsoSignup />
5
+ }
@@ -0,0 +1,5 @@
1
+ import { SsoVerifyEmail } from "@/lib/sso"
2
+
3
+ export default function VerifyEmailPage() {
4
+ return <SsoVerifyEmail />
5
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ Code,
3
+ CodeBlock,
4
+ DocPage,
5
+ Note,
6
+ P,
7
+ Section,
8
+ } from "@/components/docs/doc-primitives"
9
+
10
+ export default function ApiPage() {
11
+ return (
12
+ <DocPage
13
+ title="Your own API"
14
+ summary="Calling your product backend with the same session."
15
+ >
16
+ <Section title="The client">
17
+ <P>
18
+ <Code>src/lib/http.ts</Code> is for your API — the SDK owns the auth
19
+ backend. It attaches the bearer token from the same store and treats a 401
20
+ the way the SDK treats an invalid session.
21
+ </P>
22
+ <CodeBlock>{`import { api } from "@/lib/http"
23
+
24
+ const projects = await api<Project[]>("/projects")
25
+
26
+ await api<Project>("/projects", {
27
+ method: "POST",
28
+ body: JSON.stringify({ name: "New project" }),
29
+ })`}</CodeBlock>
30
+ <P>
31
+ JSON content-type is set automatically when there is a body, and a
32
+ non-2xx response throws <Code>HttpError</Code> carrying{" "}
33
+ <Code>status</Code> and the parsed <Code>body</Code>.
34
+ </P>
35
+ </Section>
36
+
37
+ <Section title="401 handling">
38
+ <Note>
39
+ On a 401 the token is cleared. <Code>useAuth()</Code> is subscribed to the
40
+ token store, so the guard re-renders and redirects to{" "}
41
+ <Code>/login?next=…</Code> without any extra wiring at the call site.
42
+ </Note>
43
+ </Section>
44
+
45
+ <Section title="With TanStack Query">
46
+ <P>
47
+ Query is already installed and configured. Follow the pattern in{" "}
48
+ <Code>src/lib/sso/queries.ts</Code>: key by resource, gate on the token,
49
+ and let the 401 path handle expiry.
50
+ </P>
51
+ <CodeBlock>{`export function useProjects() {
52
+ const { token } = useAuth()
53
+ return useQuery({
54
+ queryKey: ["projects"],
55
+ queryFn: () => api<Project[]>("/projects"),
56
+ enabled: Boolean(token),
57
+ })
58
+ }`}</CodeBlock>
59
+ <P>
60
+ Signing out calls <Code>queryClient.clear()</Code>, so cached data from one
61
+ account can never be shown to the next.
62
+ </P>
63
+ </Section>
64
+ </DocPage>
65
+ )
66
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ Cell,
3
+ Code,
4
+ CodeBlock,
5
+ DocPage,
6
+ List,
7
+ Note,
8
+ P,
9
+ Row,
10
+ Section,
11
+ Table,
12
+ } from "@/components/docs/doc-primitives"
13
+
14
+ export default function AuthPage() {
15
+ return (
16
+ <DocPage
17
+ title="Auth"
18
+ summary="How a token gets from the login screen into every request."
19
+ >
20
+ <Section title="The screens are the API">
21
+ <P>
22
+ The SDK exports no <Code>loginApi</Code> or <Code>signupApi</Code>. Sign-in
23
+ and sign-up are reachable only through its components — which is also what
24
+ gives you the project's captcha config, the 2FA step and magic-link sign-in
25
+ for free.
26
+ </P>
27
+ <P>
28
+ So <Code>src/lib/sso/auth-screens.tsx</Code> wraps each SDK screen once,
29
+ binding it to config, the token store and the router. Route files are one
30
+ line:
31
+ </P>
32
+ <CodeBlock>{`export default function LoginPage() {
33
+ return <SsoLogin />
34
+ }`}</CodeBlock>
35
+ <Note>
36
+ The auth pages have no layout wrapper. Each SDK screen already renders a
37
+ full-page centered root plus its own card and branding, so wrapping it
38
+ would double-wrap. Restyle with the <Code>classNames</Code> prop from{" "}
39
+ <Code>auth-screens.tsx</Code>, or the <Code>--aas-*</Code> theme bridge.
40
+ </Note>
41
+ </Section>
42
+
43
+ <Section title="The response shape">
44
+ <P>
45
+ <Code>onSuccess</Code> receives the raw API envelope, not the{" "}
46
+ <Code>{"{ token, user }"}</Code> the SDK's <Code>AuthResponse</Code> type
47
+ declares:
48
+ </P>
49
+ <CodeBlock>{`{
50
+ "success": true,
51
+ "message": "Login successful",
52
+ "data": { "accessToken": "eyJhbGciOi…" }
53
+ }`}</CodeBlock>
54
+ <Note tone="warn" title="TypeScript will not catch this">
55
+ <Code>onSuccess</Code> is typed <Code>(data: any)</Code>, so reading the
56
+ wrong field compiles fine and silently does nothing.{" "}
57
+ <Code>extractToken()</Code> reads <Code>data.accessToken</Code> first and
58
+ falls back through the other spellings; a response with no token raises an
59
+ error toast rather than failing quietly.
60
+ </Note>
61
+ </Section>
62
+
63
+ <Section title="Token lifecycle">
64
+ <Table head={["Step", "What happens"]}>
65
+ <Row>
66
+ <Cell>Sign in</Cell>
67
+ <Cell>
68
+ <Code>signIn(token)</Code> writes to <Code>tokenStore</Code> (
69
+ <Code>localStorage</Code>, key <Code>authToken</Code>).
70
+ </Cell>
71
+ </Row>
72
+ <Row>
73
+ <Cell>Every render</Cell>
74
+ <Cell>
75
+ <Code>useAuth()</Code> reads it via{" "}
76
+ <Code>useSyncExternalStore</Code>, so a sign-out in another tab updates
77
+ this one.
78
+ </Cell>
79
+ </Row>
80
+ <Row>
81
+ <Cell>SDK calls</Cell>
82
+ <Cell>
83
+ The bound hooks in <Code>queries.ts</Code> pass it to the SDK getters.
84
+ </Cell>
85
+ </Row>
86
+ <Row>
87
+ <Cell>Your API</Cell>
88
+ <Cell>
89
+ <Code>api()</Code> attaches <Code>Authorization: Bearer …</Code>.
90
+ </Cell>
91
+ </Row>
92
+ <Row>
93
+ <Cell>Sign out</Cell>
94
+ <Cell>
95
+ <Code>logoutApi</Code> is called, then the token and the whole query
96
+ cache are cleared — local sign-out succeeds even if the network call
97
+ fails.
98
+ </Cell>
99
+ </Row>
100
+ </Table>
101
+ <Note tone="warn" title="Two things to know">
102
+ <List>
103
+ <li>
104
+ The token lives in <Code>localStorage</Code>, so an XSS bug exposes it.
105
+ The SDK has no refresh flow, which makes this the pragmatic choice.
106
+ Swapping strategy is a one-file change in <Code>token-store.ts</Code>.
107
+ </li>
108
+ <li>
109
+ Tokens are valid for 24 hours. There is no silent refresh — after that
110
+ the next call 401s and the guard bounces to <Code>/login</Code>.
111
+ </li>
112
+ </List>
113
+ </Note>
114
+ </Section>
115
+
116
+ <Section title="Session validation">
117
+ <P>
118
+ <Code>ProtectedRoute</Code> renders the SDK's <Code>SessionProvider</Code>,
119
+ which validates the token against <Code>/me</Code> and renders its children
120
+ only when the session is valid.
121
+ </P>
122
+ <Note tone="warn" title="Keep the callbacks stable">
123
+ <Code>onSessionInvalid</Code> is in that provider's validation effect
124
+ dependencies, and the effect starts by setting <Code>isValidating</Code> —
125
+ during which it renders <Code>null</Code>. Passing a fresh inline arrow
126
+ re-validates and blanks the page on every navigation. It is wrapped in{" "}
127
+ <Code>useCallback</Code> for exactly this reason.
128
+ </Note>
129
+ </Section>
130
+
131
+ <Section title="What has no callback">
132
+ <P>
133
+ Forgot-password (the request-a-link step) exposes only{" "}
134
+ <Code>onError</Code>, <Code>onResetSuccess</Code> and{" "}
135
+ <Code>onBackToLogin</Code> — there is no success hook, and no exported
136
+ function to call directly, so it shows its own inline confirmation.
137
+ </P>
138
+ <P>
139
+ Resending a verification email does not exist in the SDK at all: no
140
+ exported function, no endpoint, no UI.
141
+ </P>
142
+ </Section>
143
+ </DocPage>
144
+ )
145
+ }
@@ -0,0 +1,103 @@
1
+ import {
2
+ Cell,
3
+ Code,
4
+ CodeBlock,
5
+ DocPage,
6
+ Note,
7
+ P,
8
+ Row,
9
+ Section,
10
+ Table,
11
+ } from "@/components/docs/doc-primitives"
12
+
13
+ export default function BillingPage() {
14
+ return (
15
+ <DocPage
16
+ title="Billing & plans"
17
+ summary="Plans, add-ons, wallets and credits — and how to gate features on them."
18
+ >
19
+ <Section title="There is no billing page">
20
+ <P>
21
+ The SDK's Account Settings modal already covers profile, security, plans
22
+ and add-ons, wallet balance and credit wallets. A dedicated route would
23
+ either duplicate it or just be a button that opens it — so the user menus
24
+ in the sidebar and header open it directly.
25
+ </P>
26
+ <CodeBlock>{`const { open } = useAccountSettings()
27
+
28
+ open("plans") // "general" | "account" | "wallet" | "plans" | "credits"`}</CodeBlock>
29
+ <P>
30
+ The modal performs its own API calls. The template passes{" "}
31
+ <Code>onChangePlan</Code>, <Code>onAddFunds</Code> and{" "}
32
+ <Code>onSaveProfile</Code> as observers so cached copies — the sidebar
33
+ balance, feature gates — refresh when something changes inside it.
34
+ </P>
35
+ </Section>
36
+
37
+ <Section title="Two strengths of gating">
38
+ <Table head={["Level", "Use"]}>
39
+ <Row>
40
+ <Cell>
41
+ <Code>enforceActiveSubscription</Code>
42
+ </Cell>
43
+ <Cell>
44
+ Hard paywall on the whole shell. Add it to{" "}
45
+ <Code>{"<ProtectedRoute />"}</Code> in <Code>App.tsx</Code>; the SDK
46
+ opens Account Settings on Plans when no subscription is active.
47
+ </Cell>
48
+ </Row>
49
+ <Row>
50
+ <Cell>
51
+ <Code>{"<RequirePlan />"}</Code>
52
+ </Cell>
53
+ <Cell>Per-feature. Wrap a route, a panel, or a button.</Cell>
54
+ </Row>
55
+ <Row>
56
+ <Cell>
57
+ <Code>useEntitlements()</Code>
58
+ </Cell>
59
+ <Cell>
60
+ Imperative checks: <Code>isActive</Code>, <Code>has(id)</Code>,{" "}
61
+ <Code>countOf(id)</Code>.
62
+ </Cell>
63
+ </Row>
64
+ <Row>
65
+ <Cell>
66
+ <Code>requiresIds</Code>
67
+ </Cell>
68
+ <Cell>
69
+ On a nav item in <Code>navigation.ts</Code> — hides the entry unless
70
+ the user is entitled.
71
+ </Cell>
72
+ </Row>
73
+ </Table>
74
+ <CodeBlock>{`<RequirePlan ids={["addon-exports"]} fallback={<Upgrade />}>
75
+ <ExportButton />
76
+ </RequirePlan>`}</CodeBlock>
77
+ </Section>
78
+
79
+ <Section title="Check this first when gates misbehave">
80
+ <Note tone="warn" title="mapSubscription() is guessing">
81
+ <Code>getUserSubscription</Code> returns the API response unmapped — the
82
+ SDK maps its other getters but explicitly not this one. So{" "}
83
+ <Code>mapSubscription()</Code> in <Code>entitlements.ts</Code> accepts
84
+ several plausible field spellings (<Code>planId</Code>,{" "}
85
+ <Code>plan_id</Code>, <Code>renewsAt</Code>, <Code>expiryDate</Code>, …).
86
+ <br />
87
+ <br />
88
+ Log one real payload, correct the readers in that one function, and every
89
+ gate in the app follows.
90
+ </Note>
91
+ </Section>
92
+
93
+ <Section title="Credits">
94
+ <P>
95
+ Credit wallets are the metered-billing story: each has a balance, a unit, a
96
+ fund source and whether it can be topped up.{" "}
97
+ <Code>useCreditWallets()</Code> returns them, and the sidebar footer shows
98
+ the first one as a shortcut into the Credits section.
99
+ </P>
100
+ </Section>
101
+ </DocPage>
102
+ )
103
+ }
@@ -0,0 +1,113 @@
1
+ import {
2
+ Cell,
3
+ Code,
4
+ CodeBlock,
5
+ DocPage,
6
+ Note,
7
+ P,
8
+ Row,
9
+ Section,
10
+ Table,
11
+ } from "@/components/docs/doc-primitives"
12
+
13
+ export default function ConfigurationPage() {
14
+ return (
15
+ <DocPage
16
+ title="Configuration"
17
+ summary="Environment variables, the preflight check, and what to pin."
18
+ >
19
+ <Section title="Environment">
20
+ <CodeBlock>{`# Required — your project id in the partner ecosystem.
21
+ VITE_AUTH_PROJECT_ID=436582-forms.abprojects.com
22
+
23
+ # Auth-as-a-Service origin. The SDK also reads this var directly.
24
+ VITE_AUTH_API_BASE_URL=https://partner-api.example.com
25
+
26
+ # Your own product API (src/lib/http.ts). Unrelated to auth.
27
+ VITE_API_BASE_URL=
28
+
29
+ # Shown in the sidebar header and breadcrumb.
30
+ VITE_APP_NAME=Acme Inc`}</CodeBlock>
31
+ <P>
32
+ Copy <Code>.env.example</Code> to <Code>.env.local</Code>. Vite only
33
+ exposes variables prefixed with <Code>VITE_</Code>, and it reads them at
34
+ startup — restart the dev server after editing.
35
+ </P>
36
+ </Section>
37
+
38
+ <Section title="Preflight">
39
+ <P>
40
+ <Code>missingRequiredEnv()</Code> in <Code>src/config/env.ts</Code> runs
41
+ before the app mounts. If <Code>VITE_AUTH_PROJECT_ID</Code> is missing,{" "}
42
+ <Code>main.tsx</Code> renders a setup screen naming it instead of booting
43
+ an app whose every request will fail.
44
+ </P>
45
+ <P>Add your own required variables to that function as the app grows.</P>
46
+ </Section>
47
+
48
+ <Section title="Where the values are used">
49
+ <Table head={["Variable", "Read by"]}>
50
+ <Row>
51
+ <Cell>
52
+ <Code>VITE_AUTH_PROJECT_ID</Code>
53
+ </Cell>
54
+ <Cell>
55
+ <Code>ssoConfig</Code> — every SDK component and getter.
56
+ </Cell>
57
+ </Row>
58
+ <Row>
59
+ <Cell>
60
+ <Code>VITE_AUTH_API_BASE_URL</Code>
61
+ </Cell>
62
+ <Cell>Passed explicitly to the SDK so the value is traceable.</Cell>
63
+ </Row>
64
+ <Row>
65
+ <Cell>
66
+ <Code>VITE_API_BASE_URL</Code>
67
+ </Cell>
68
+ <Cell>
69
+ <Code>api()</Code> in <Code>src/lib/http.ts</Code>.
70
+ </Cell>
71
+ </Row>
72
+ <Row>
73
+ <Cell>
74
+ <Code>VITE_APP_NAME</Code>
75
+ </Cell>
76
+ <Cell>Sidebar header and breadcrumb root.</Cell>
77
+ </Row>
78
+ </Table>
79
+ <Note>
80
+ Branding on the auth screens comes from the backend's{" "}
81
+ <Code>auth-config</Code> (logo, card layout, terms and privacy links) — not
82
+ from <Code>VITE_APP_NAME</Code>. If those screens look unbranded, it is a
83
+ backend configuration gap.
84
+ </Note>
85
+ </Section>
86
+
87
+ <Section title="Version pinning">
88
+ <Note tone="warn" title="ab-ecosystem-sso is pinned exactly">
89
+ It is pre-1.0, so minor releases can break. The version in{" "}
90
+ <Code>package.json</Code> has no caret. Upgrade deliberately, and check the
91
+ auth screens and the Account Settings modal afterwards.
92
+ </Note>
93
+ </Section>
94
+
95
+ <Section title="Scripts">
96
+ <CodeBlock>{`npm run dev Vite dev server
97
+ npm run build tsc -b && vite build
98
+ npm run typecheck tsc -b
99
+ npm run lint oxlint
100
+ npm run preview Serve the production build`}</CodeBlock>
101
+ </Section>
102
+
103
+ <Section title="Deploying">
104
+ <P>
105
+ This is a single-page app, so deep links need a rewrite to{" "}
106
+ <Code>index.html</Code>. <Code>public/_redirects</Code> (Netlify) and{" "}
107
+ <Code>vercel.json</Code> are included; other hosts need the equivalent or
108
+ every refresh on a nested route 404s.
109
+ </P>
110
+ </Section>
111
+ </DocPage>
112
+ )
113
+ }
@@ -0,0 +1,97 @@
1
+ import { Link } from "react-router-dom"
2
+
3
+ import {
4
+ Cell,
5
+ Code,
6
+ CodeBlock,
7
+ DocPage,
8
+ List,
9
+ Note,
10
+ P,
11
+ Row,
12
+ Section,
13
+ Table,
14
+ } from "@/components/docs/doc-primitives"
15
+ import { Button } from "@/components/ui/button"
16
+
17
+ export default function OverviewPage() {
18
+ return (
19
+ <DocPage
20
+ title="Overview"
21
+ summary="A React + Vite starter with authentication and billing already wired up. This app is the documentation — replace it with your product."
22
+ >
23
+ <Section title="What you get">
24
+ <List>
25
+ <li>React 19, Vite, TypeScript (strict) and Tailwind v4.</li>
26
+ <li>
27
+ shadcn/ui components, with the <Code>sidebar-07</Code> block as the app
28
+ shell.
29
+ </li>
30
+ <li>
31
+ Auth and billing from <Code>ab-ecosystem-sso</Code> — login, signup, 2FA,
32
+ magic links, password reset, email verification, plans, wallets and
33
+ credits.
34
+ </li>
35
+ <li>Route guards, entitlement gating, toasts and a theme toggle.</li>
36
+ </List>
37
+ </Section>
38
+
39
+ <Section title="Start here">
40
+ <CodeBlock>{`cp .env.example .env.local # set VITE_AUTH_PROJECT_ID
41
+ npm install
42
+ npm run dev`}</CodeBlock>
43
+ <P>
44
+ Without a project id the app renders a setup screen naming the missing
45
+ variable, rather than failing on an opaque network error.
46
+ </P>
47
+ </Section>
48
+
49
+ <Section title="The one rule">
50
+ <Note title="Import from @/lib/sso, never from ab-ecosystem-sso">
51
+ Every SDK call takes <Code>(projectId, token, apiBaseUrl)</Code>. Those are
52
+ bound once in <Code>src/lib/sso/config.ts</Code>, so nothing downstream
53
+ repeats them — and upgrading the SDK is a change to one folder.
54
+ </Note>
55
+ </Section>
56
+
57
+ <Section title="What to delete first">
58
+ <P>
59
+ These docs pages exist to explain the template, not to ship with your
60
+ product. When you start building:
61
+ </P>
62
+ <Table head={["Delete", "Then"]}>
63
+ <Row>
64
+ <Cell>
65
+ <Code>src/routes/docs/</Code>
66
+ </Cell>
67
+ <Cell>Point the router at your own pages.</Cell>
68
+ </Row>
69
+ <Row>
70
+ <Cell>
71
+ <Code>src/routes/app/example.tsx</Code>
72
+ </Cell>
73
+ <Cell>Keep it while you learn the hooks; delete it after.</Cell>
74
+ </Row>
75
+ <Row>
76
+ <Cell>
77
+ <Code>src/config/navigation.ts</Code>
78
+ </Cell>
79
+ <Cell>Replace the three groups with your product's nav.</Cell>
80
+ </Row>
81
+ </Table>
82
+ </Section>
83
+
84
+ <Section title="Next">
85
+ <div className="flex flex-wrap gap-2">
86
+ <Button render={<Link to="/docs/structure" />}>Project structure</Button>
87
+ <Button variant="outline" render={<Link to="/docs/auth" />}>
88
+ How auth works
89
+ </Button>
90
+ <Button variant="outline" render={<Link to="/example" />}>
91
+ Live example
92
+ </Button>
93
+ </div>
94
+ </Section>
95
+ </DocPage>
96
+ )
97
+ }
@@ -0,0 +1,126 @@
1
+ import {
2
+ Cell,
3
+ Code,
4
+ CodeBlock,
5
+ DocPage,
6
+ Note,
7
+ P,
8
+ Row,
9
+ Section,
10
+ Table,
11
+ } from "@/components/docs/doc-primitives"
12
+
13
+ export default function RoutingPage() {
14
+ return (
15
+ <DocPage
16
+ title="Routing & guards"
17
+ summary="React Router v7, two guards, and the routes the SDK requires."
18
+ >
19
+ <Section title="Route map">
20
+ <Table head={["Route", "Guard", "Renders"]}>
21
+ <Row>
22
+ <Cell>
23
+ <Code>/login</Code>
24
+ </Cell>
25
+ <Cell>PublicOnlyRoute</Cell>
26
+ <Cell>
27
+ SsoLogin — also consumes a <Code>#access_token</Code> magic-link hash
28
+ </Cell>
29
+ </Row>
30
+ <Row>
31
+ <Cell>
32
+ <Code>/signup</Code>
33
+ </Cell>
34
+ <Cell>PublicOnlyRoute</Cell>
35
+ <Cell>SsoSignup</Cell>
36
+ </Row>
37
+ <Row>
38
+ <Cell>
39
+ <Code>/forgot-password</Code>
40
+ </Cell>
41
+ <Cell>PublicOnlyRoute</Cell>
42
+ <Cell>SsoForgotPassword — request a link</Cell>
43
+ </Row>
44
+ <Row>
45
+ <Cell>
46
+ <Code>/reset-password/:token</Code>
47
+ </Cell>
48
+ <Cell>none</Cell>
49
+ <Cell>SsoForgotPassword — set a new password</Cell>
50
+ </Row>
51
+ <Row>
52
+ <Cell>
53
+ <Code>/verify/:token</Code>
54
+ </Cell>
55
+ <Cell>none</Cell>
56
+ <Cell>SsoVerifyEmail</Cell>
57
+ </Row>
58
+ <Row>
59
+ <Cell>
60
+ <Code>/</Code>, <Code>/docs/*</Code>, <Code>/example</Code>
61
+ </Cell>
62
+ <Cell>ProtectedRoute</Cell>
63
+ <Cell>AppLayout shell</Cell>
64
+ </Row>
65
+ </Table>
66
+ <Note>
67
+ The two token routes sit outside <Code>PublicOnlyRoute</Code> on purpose: a
68
+ user who just signed up is already signed in, and must still be able to
69
+ verify or reset. Guarding them would make the emailed links silently
70
+ redirect to the dashboard.
71
+ </Note>
72
+ <P>
73
+ <Code>/reset-password?token=</Code> and <Code>/verify-email?token=</Code>{" "}
74
+ are kept as aliases in case a link carries the token as a query string.
75
+ </P>
76
+ </Section>
77
+
78
+ <Section title="ProtectedRoute">
79
+ <P>Two layers, deliberately:</P>
80
+ <CodeBlock>{`// 1. No token at all → redirect immediately, no network round-trip.
81
+ if (!token) return <Navigate to={\`/login?next=\${next}\`} replace />
82
+
83
+ // 2. Token present → SessionProvider validates it against /me
84
+ // and renders children only when the session is genuinely valid.`}</CodeBlock>
85
+ <P>
86
+ <Code>onSessionInvalid</Code> only calls <Code>signOut()</Code>. Clearing
87
+ the token falls through to the redirect above, which preserves the
88
+ attempted path in <Code>?next=</Code> — so there is one redirect path, not
89
+ two.
90
+ </P>
91
+ </Section>
92
+
93
+ <Section title="The loading screen">
94
+ <Note tone="warn" title="SessionProvider renders null while validating">
95
+ That is a blank page on every protected load unless something covers it.{" "}
96
+ <Code>ProtectedRoute</Code> renders a full-page loader as a sibling and
97
+ hides it via a <Code>SessionReady</Code> child that reports mount and
98
+ unmount — children only mount once the session validates, so no duplicate{" "}
99
+ <Code>/me</Code> call is needed to know when to hide it.
100
+ </Note>
101
+ </Section>
102
+
103
+ <Section title="Redirect safety">
104
+ <P>
105
+ <Code>?next=</Code> is only honoured for relative same-origin paths —{" "}
106
+ <Code>useNextPath()</Code> rejects anything not starting with a single{" "}
107
+ <Code>/</Code>, so the parameter cannot be used as an open redirect.
108
+ </P>
109
+ </Section>
110
+
111
+ <Section title="Adding a route">
112
+ <CodeBlock>{`// src/App.tsx — inside the ProtectedRoute / AppLayout block
113
+ <Route path="/reports" element={<ReportsPage />} />
114
+
115
+ // src/config/navigation.ts — to give it a sidebar entry
116
+ { title: "Reports", url: "/reports", icon: BarChartIcon,
117
+ requiresIds: ["addon-reports"] } // optional gating`}</CodeBlock>
118
+ <P>
119
+ Nav items render through the router's <Code>Link</Code>, not{" "}
120
+ <Code>{"<a href>"}</Code> — a plain anchor would full-page reload and throw
121
+ away in-memory state on every click.
122
+ </P>
123
+ </Section>
124
+ </DocPage>
125
+ )
126
+ }