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.
- package/README.md +60 -0
- package/index.js +380 -0
- package/package.json +36 -0
- package/template/.env.example +8 -0
- package/template/.oxlintrc.json +8 -0
- package/template/README.md +196 -0
- package/template/_gitignore +24 -0
- package/template/components.json +25 -0
- package/template/index.html +13 -0
- package/template/package-lock.json +6096 -0
- package/template/package.json +40 -0
- package/template/public/_redirects +1 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/icons.svg +24 -0
- package/template/src/App.tsx +60 -0
- package/template/src/assets/hero.png +0 -0
- package/template/src/assets/vite.svg +1 -0
- package/template/src/components/app-sidebar.tsx +53 -0
- package/template/src/components/credit-balance.tsx +31 -0
- package/template/src/components/docs/doc-primitives.tsx +125 -0
- package/template/src/components/full-page-loader.tsx +14 -0
- package/template/src/components/header-user.tsx +64 -0
- package/template/src/components/layouts/app-layout.tsx +80 -0
- package/template/src/components/missing-env.tsx +24 -0
- package/template/src/components/nav-main.tsx +87 -0
- package/template/src/components/nav-user.tsx +69 -0
- package/template/src/components/theme-toggle.tsx +39 -0
- package/template/src/components/ui/avatar.tsx +106 -0
- package/template/src/components/ui/breadcrumb.tsx +124 -0
- package/template/src/components/ui/button.tsx +57 -0
- package/template/src/components/ui/card.tsx +102 -0
- package/template/src/components/ui/collapsible.tsx +19 -0
- package/template/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/src/components/ui/field.tsx +238 -0
- package/template/src/components/ui/input.tsx +19 -0
- package/template/src/components/ui/label.tsx +19 -0
- package/template/src/components/ui/separator.tsx +22 -0
- package/template/src/components/ui/sheet.tsx +136 -0
- package/template/src/components/ui/sidebar.tsx +721 -0
- package/template/src/components/ui/skeleton.tsx +13 -0
- package/template/src/components/ui/sonner.tsx +47 -0
- package/template/src/components/ui/tooltip.tsx +65 -0
- package/template/src/components/user-menu.tsx +96 -0
- package/template/src/config/env.ts +14 -0
- package/template/src/config/navigation.ts +52 -0
- package/template/src/hooks/use-mobile.ts +19 -0
- package/template/src/hooks/use-user-identity.ts +27 -0
- package/template/src/index.css +134 -0
- package/template/src/lib/http.ts +40 -0
- package/template/src/lib/sso/account-settings.tsx +53 -0
- package/template/src/lib/sso/auth-context.ts +16 -0
- package/template/src/lib/sso/auth-provider.tsx +42 -0
- package/template/src/lib/sso/auth-screens.tsx +152 -0
- package/template/src/lib/sso/config.ts +13 -0
- package/template/src/lib/sso/entitlements.ts +101 -0
- package/template/src/lib/sso/index.ts +33 -0
- package/template/src/lib/sso/protected-route.tsx +72 -0
- package/template/src/lib/sso/queries.ts +55 -0
- package/template/src/lib/sso/require-plan.tsx +19 -0
- package/template/src/lib/sso/token-store.ts +62 -0
- package/template/src/lib/utils.ts +1 -0
- package/template/src/main.tsx +42 -0
- package/template/src/routes/app/example.tsx +125 -0
- package/template/src/routes/auth/forgot-password.tsx +5 -0
- package/template/src/routes/auth/login.tsx +5 -0
- package/template/src/routes/auth/signup.tsx +5 -0
- package/template/src/routes/auth/verify-email.tsx +5 -0
- package/template/src/routes/docs/api.tsx +66 -0
- package/template/src/routes/docs/auth.tsx +145 -0
- package/template/src/routes/docs/billing.tsx +103 -0
- package/template/src/routes/docs/configuration.tsx +113 -0
- package/template/src/routes/docs/overview.tsx +97 -0
- package/template/src/routes/docs/routing.tsx +126 -0
- package/template/src/routes/docs/structure.tsx +150 -0
- package/template/src/routes/docs/theming.tsx +88 -0
- package/template/src/routes/not-found.tsx +12 -0
- package/template/src/styles/aas-theme.css +21 -0
- package/template/src/vite-env.d.ts +12 -0
- package/template/tsconfig.app.json +30 -0
- package/template/tsconfig.json +12 -0
- package/template/tsconfig.node.json +23 -0
- package/template/vercel.json +3 -0
- package/template/vite.config.ts +17 -0
- package/variants/minimal/src/App.tsx +43 -0
- package/variants/minimal/src/components/app-sidebar.tsx +50 -0
- package/variants/minimal/src/config/navigation.ts +19 -0
- package/variants/minimal/src/routes/app/home.tsx +45 -0
|
@@ -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
|
+
}
|