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
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # create-ab-app
2
+
3
+ Scaffold a React + Vite app with authentication and billing already wired up via
4
+ [`ab-ecosystem-sso`](https://www.npmjs.com/package/ab-ecosystem-sso).
5
+
6
+ ```bash
7
+ npm create ab-app@latest my-app
8
+ ```
9
+
10
+ ```
11
+ ◇ Where should the project be created? my-app
12
+ ◇ App display name My App
13
+ ◇ Auth project id 459412-review-ai.abprojects.com
14
+ ◇ Auth API base URL https://partner-api.example.com
15
+ ◇ Your own API base URL (optional)
16
+ ◇ Start with the in-app documentation? Yes
17
+ ◇ Package manager npm
18
+ ◇ Install dependencies now? Yes
19
+ ◇ Initialise a git repository? Yes
20
+ ```
21
+
22
+ You get React 19, TypeScript, Tailwind v4, shadcn/ui with the `sidebar-07` shell,
23
+ login / signup / 2FA / magic links / password reset / email verification, plans,
24
+ wallets, credits, route guards, entitlement gating, toasts and a theme toggle.
25
+
26
+ ## Options
27
+
28
+ Every prompt has a flag, so CI can scaffold non-interactively:
29
+
30
+ ```bash
31
+ npx create-ab-app my-app \
32
+ --project-id=459412-review-ai.abprojects.com \
33
+ --auth-url=https://partner-api.example.com \
34
+ --app-name="Review AI" \
35
+ --minimal --no-install --no-git -y
36
+ ```
37
+
38
+ | Flag | Effect |
39
+ | --- | --- |
40
+ | `--project-id=<id>` | `VITE_AUTH_PROJECT_ID` (required) |
41
+ | `--auth-url=<url>` | `VITE_AUTH_API_BASE_URL` |
42
+ | `--api-url=<url>` | `VITE_API_BASE_URL` — your own backend |
43
+ | `--app-name=<name>` | Sidebar and browser-tab name |
44
+ | `--minimal` | Skip the in-app documentation pages |
45
+ | `--pm=<npm\|pnpm\|yarn\|bun>` | Package manager |
46
+ | `--no-install` / `--no-git` | Skip those steps |
47
+ | `-y`, `--yes` | Accept defaults for everything optional |
48
+
49
+ ## After scaffolding
50
+
51
+ ```bash
52
+ cd my-app
53
+ npm run dev
54
+ ```
55
+
56
+ Sign in and the sidebar carries the documentation: project structure, auth,
57
+ billing, routing, theming, and calling your own API. The one rule it opens with:
58
+ **import from `@/lib/sso`, never from `ab-ecosystem-sso` directly.**
59
+
60
+ Requires Node 20+.
package/index.js ADDED
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-ab-app — scaffolds the React + Vite template with auth and billing.
4
+ *
5
+ * Asks for the handful of things the template cannot guess (project id, API
6
+ * origin, app name), writes .env.local, then optionally installs and inits git.
7
+ */
8
+ import { spawn } from "node:child_process"
9
+ import { access, cp, readdir, readFile, rename, rm, writeFile } from "node:fs/promises"
10
+ import path from "node:path"
11
+ import process from "node:process"
12
+ import { fileURLToPath } from "node:url"
13
+
14
+ import {
15
+ cancel,
16
+ confirm,
17
+ intro,
18
+ isCancel,
19
+ log,
20
+ note,
21
+ outro,
22
+ select,
23
+ spinner,
24
+ text,
25
+ } from "@clack/prompts"
26
+ import pc from "picocolors"
27
+
28
+ const cliDir = path.dirname(fileURLToPath(import.meta.url))
29
+ const templateDir = path.join(cliDir, "template")
30
+ const variantsDir = path.join(cliDir, "variants")
31
+
32
+ // ─── args ────────────────────────────────────────────────────────────────────
33
+
34
+ const argv = process.argv.slice(2)
35
+ const flags = new Map()
36
+ const positionals = []
37
+
38
+ for (const arg of argv) {
39
+ if (arg.startsWith("--")) {
40
+ const [key, value] = arg.slice(2).split("=")
41
+ flags.set(key, value ?? true)
42
+ } else if (arg.startsWith("-") && arg !== "-") {
43
+ flags.set(arg.slice(1), true)
44
+ } else {
45
+ positionals.push(arg)
46
+ }
47
+ }
48
+
49
+ if (flags.has("help") || flags.has("h")) {
50
+ console.log(`
51
+ ${pc.bold("create-ab-app")} — React + Vite starter with auth and billing
52
+
53
+ ${pc.dim("npm create ab-app@latest my-app")}
54
+
55
+ Options
56
+ --project-id=<id> VITE_AUTH_PROJECT_ID
57
+ --auth-url=<url> VITE_AUTH_API_BASE_URL
58
+ --api-url=<url> VITE_API_BASE_URL (your own backend)
59
+ --app-name=<name> Display name in the sidebar
60
+ --minimal Skip the in-app documentation pages
61
+ --pm=<npm|pnpm|yarn|bun>
62
+ --no-install Do not install dependencies
63
+ --no-git Do not initialise a git repository
64
+ -y, --yes Accept defaults for everything optional
65
+ `)
66
+ process.exit(0)
67
+ }
68
+
69
+ const assumeYes = flags.has("yes") || flags.has("y")
70
+
71
+ // ─── helpers ─────────────────────────────────────────────────────────────────
72
+
73
+ /** Every prompt goes through here so Ctrl+C exits cleanly everywhere. */
74
+ async function ask(promptPromise) {
75
+ const value = await promptPromise
76
+ if (isCancel(value)) {
77
+ cancel("Cancelled. Nothing was written.")
78
+ process.exit(0)
79
+ }
80
+ return value
81
+ }
82
+
83
+ async function exists(target) {
84
+ try {
85
+ await access(target)
86
+ return true
87
+ } catch {
88
+ return false
89
+ }
90
+ }
91
+
92
+ function toPackageName(input) {
93
+ return (
94
+ input
95
+ .trim()
96
+ .toLowerCase()
97
+ .replace(/^[._]+/, "")
98
+ .replace(/[^a-z0-9-~._]+/g, "-")
99
+ .replace(/^-+|-+$/g, "") || "app"
100
+ )
101
+ }
102
+
103
+ function toTitle(input) {
104
+ return input
105
+ .replace(/[-_]+/g, " ")
106
+ .trim()
107
+ .replace(/\b\w/g, (character) => character.toUpperCase())
108
+ }
109
+
110
+ function detectPackageManager() {
111
+ const agent = process.env.npm_config_user_agent ?? ""
112
+ if (agent.startsWith("pnpm")) return "pnpm"
113
+ if (agent.startsWith("yarn")) return "yarn"
114
+ if (agent.startsWith("bun")) return "bun"
115
+ return "npm"
116
+ }
117
+
118
+ function run(command, args, cwd) {
119
+ return new Promise((resolve, reject) => {
120
+ const child = spawn(command, args, {
121
+ cwd,
122
+ stdio: "pipe",
123
+ shell: process.platform === "win32",
124
+ })
125
+ let stderr = ""
126
+ child.stderr.on("data", (chunk) => {
127
+ stderr += chunk
128
+ })
129
+ child.on("error", reject)
130
+ child.on("close", (code) =>
131
+ code === 0 ? resolve() : reject(new Error(stderr.trim() || `exit ${code}`)),
132
+ )
133
+ })
134
+ }
135
+
136
+ // ─── prompts ─────────────────────────────────────────────────────────────────
137
+
138
+ intro(pc.bgCyan(pc.black(" create-ab-app ")))
139
+
140
+ const targetInput =
141
+ positionals[0] ??
142
+ (await ask(
143
+ text({
144
+ message: "Where should the project be created?",
145
+ placeholder: "my-app",
146
+ defaultValue: "my-app",
147
+ validate: (value) =>
148
+ value && value.trim().startsWith("-")
149
+ ? "Directory name cannot start with a dash"
150
+ : undefined,
151
+ }),
152
+ ))
153
+
154
+ const targetDir = path.resolve(process.cwd(), targetInput || "my-app")
155
+ const projectName = toPackageName(path.basename(targetDir))
156
+
157
+ if (await exists(targetDir)) {
158
+ const entries = await readdir(targetDir)
159
+ if (entries.length > 0) {
160
+ const overwrite = await ask(
161
+ confirm({
162
+ message: `${pc.yellow(path.basename(targetDir))} is not empty. Delete its contents and continue?`,
163
+ initialValue: false,
164
+ }),
165
+ )
166
+ if (!overwrite) {
167
+ cancel("Cancelled. Nothing was written.")
168
+ process.exit(0)
169
+ }
170
+ await rm(targetDir, { recursive: true, force: true })
171
+ }
172
+ }
173
+
174
+ const appName =
175
+ flags.get("app-name") ??
176
+ (assumeYes
177
+ ? toTitle(projectName)
178
+ : await ask(
179
+ text({
180
+ message: "App display name",
181
+ placeholder: toTitle(projectName),
182
+ defaultValue: toTitle(projectName),
183
+ }),
184
+ ))
185
+
186
+ const authProjectId =
187
+ flags.get("project-id") ??
188
+ (await ask(
189
+ text({
190
+ message: "Auth project id",
191
+ placeholder: "459412-review-ai.abprojects.com",
192
+ validate: (value) =>
193
+ value && value.trim() ? undefined : "Required — the SDK cannot resolve a project without it",
194
+ }),
195
+ ))
196
+
197
+ const authApiBaseUrl =
198
+ flags.get("auth-url") ??
199
+ (assumeYes
200
+ ? ""
201
+ : await ask(
202
+ text({
203
+ message: "Auth API base URL",
204
+ placeholder: "https://partner-api.example.com",
205
+ defaultValue: "",
206
+ validate: (value) =>
207
+ !value || /^https?:\/\//.test(value.trim())
208
+ ? undefined
209
+ : "Must start with http:// or https://",
210
+ }),
211
+ ))
212
+
213
+ const apiBaseUrl =
214
+ flags.get("api-url") ??
215
+ (assumeYes
216
+ ? ""
217
+ : await ask(
218
+ text({
219
+ message: `Your own API base URL ${pc.dim("(optional, press enter to skip)")}`,
220
+ placeholder: "https://api.example.com",
221
+ defaultValue: "",
222
+ }),
223
+ ))
224
+
225
+ const includeDocs = flags.has("minimal")
226
+ ? false
227
+ : assumeYes
228
+ ? true
229
+ : await ask(
230
+ select({
231
+ message: "Start with the in-app documentation?",
232
+ initialValue: true,
233
+ options: [
234
+ {
235
+ value: true,
236
+ label: "Yes — keep the guides and the live example",
237
+ hint: "recommended for a first project",
238
+ },
239
+ {
240
+ value: false,
241
+ label: "No — minimal starter",
242
+ hint: "one page, empty nav",
243
+ },
244
+ ],
245
+ }),
246
+ )
247
+
248
+ const packageManager =
249
+ flags.get("pm") ??
250
+ (assumeYes
251
+ ? detectPackageManager()
252
+ : await ask(
253
+ select({
254
+ message: "Package manager",
255
+ initialValue: detectPackageManager(),
256
+ options: [
257
+ { value: "npm", label: "npm" },
258
+ { value: "pnpm", label: "pnpm" },
259
+ { value: "yarn", label: "yarn" },
260
+ { value: "bun", label: "bun" },
261
+ ],
262
+ }),
263
+ ))
264
+
265
+ const shouldInstall = flags.has("no-install")
266
+ ? false
267
+ : assumeYes
268
+ ? true
269
+ : await ask(confirm({ message: "Install dependencies now?", initialValue: true }))
270
+
271
+ const shouldGit = flags.has("no-git")
272
+ ? false
273
+ : assumeYes
274
+ ? true
275
+ : await ask(confirm({ message: "Initialise a git repository?", initialValue: true }))
276
+
277
+ // ─── scaffold ────────────────────────────────────────────────────────────────
278
+
279
+ const progress = spinner()
280
+ progress.start("Creating project")
281
+
282
+ await cp(templateDir, targetDir, { recursive: true })
283
+
284
+ // npm strips .gitignore from tarballs, so the staged copy ships it renamed.
285
+ const stagedGitignore = path.join(targetDir, "_gitignore")
286
+ if (await exists(stagedGitignore)) {
287
+ await rename(stagedGitignore, path.join(targetDir, ".gitignore"))
288
+ }
289
+
290
+ const pkgPath = path.join(targetDir, "package.json")
291
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"))
292
+ pkg.name = projectName
293
+ pkg.version = "0.1.0"
294
+ pkg.private = true
295
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
296
+
297
+ // The lockfile carries the old project name in its root entry.
298
+ const lockPath = path.join(targetDir, "package-lock.json")
299
+ if (await exists(lockPath)) {
300
+ const lock = JSON.parse(await readFile(lockPath, "utf8"))
301
+ lock.name = projectName
302
+ if (lock.packages?.[""]) lock.packages[""].name = projectName
303
+ await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}
304
+ `)
305
+ }
306
+
307
+ const envLines = [
308
+ "# Required — the SDK cannot resolve a project without it.",
309
+ `VITE_AUTH_PROJECT_ID=${authProjectId.trim()}`,
310
+ "",
311
+ "# Auth-as-a-Service origin.",
312
+ `VITE_AUTH_API_BASE_URL=${(authApiBaseUrl || "").trim()}`,
313
+ "",
314
+ "# Your own product API (src/lib/http.ts). Unrelated to auth.",
315
+ `VITE_API_BASE_URL=${(apiBaseUrl || "").trim()}`,
316
+ "",
317
+ "# Shown in the sidebar header and breadcrumb.",
318
+ `VITE_APP_NAME=${appName}`,
319
+ "",
320
+ ]
321
+ await writeFile(path.join(targetDir, ".env.local"), envLines.join("\n"))
322
+
323
+ if (!includeDocs) {
324
+ await rm(path.join(targetDir, "src/routes/docs"), { recursive: true, force: true })
325
+ await rm(path.join(targetDir, "src/components/docs"), { recursive: true, force: true })
326
+ await rm(path.join(targetDir, "src/routes/app/example.tsx"), { force: true })
327
+ await cp(path.join(variantsDir, "minimal"), targetDir, { recursive: true })
328
+ }
329
+
330
+ // index.html carries the tab title.
331
+ const htmlPath = path.join(targetDir, "index.html")
332
+ const html = await readFile(htmlPath, "utf8")
333
+ await writeFile(htmlPath, html.replace(/<title>.*?<\/title>/, `<title>${appName}</title>`))
334
+
335
+ progress.stop("Project created")
336
+
337
+ if (shouldInstall) {
338
+ progress.start(`Installing dependencies with ${packageManager}`)
339
+ try {
340
+ await run(packageManager, ["install"], targetDir)
341
+ progress.stop("Dependencies installed")
342
+ } catch (error) {
343
+ progress.stop("Dependency install failed")
344
+ log.warn(`${error.message}\nRun ${pc.cyan(`${packageManager} install`)} yourself.`)
345
+ }
346
+ }
347
+
348
+ if (shouldGit) {
349
+ try {
350
+ await run("git", ["init"], targetDir)
351
+ await run("git", ["add", "-A"], targetDir)
352
+ await run("git", ["commit", "-m", "Initial commit from create-ab-app"], targetDir)
353
+ log.success("Git repository initialised")
354
+ } catch {
355
+ // Usually an unconfigured user.email — the repo still exists.
356
+ log.warn("Git repository created, but the initial commit failed.")
357
+ }
358
+ }
359
+
360
+ // ─── done ────────────────────────────────────────────────────────────────────
361
+
362
+ const relative = path.relative(process.cwd(), targetDir) || "."
363
+ const runCmd = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`
364
+
365
+ note(
366
+ [
367
+ `cd ${relative}`,
368
+ shouldInstall ? null : `${packageManager} install`,
369
+ runCmd,
370
+ ]
371
+ .filter(Boolean)
372
+ .join("\n"),
373
+ "Next steps",
374
+ )
375
+
376
+ log.info(
377
+ `${pc.dim("Auth and billing live in")} src/lib/sso ${pc.dim("— import from")} @/lib/sso${pc.dim(", never from the SDK directly.")}`,
378
+ )
379
+
380
+ outro(pc.green("Done."))
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "create-ab-app",
3
+ "version": "0.1.1",
4
+ "description": "Scaffold a React + Vite app with ab-ecosystem-sso auth and billing already wired up.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "private": false,
8
+ "bin": {
9
+ "create-ab-app": "index.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "template",
14
+ "variants",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "stage": "node scripts/prepare-template.js",
22
+ "prepack": "node scripts/prepare-template.js",
23
+ "check": "node scripts/prepare-template.js && npm pack --dry-run"
24
+ },
25
+ "keywords": [
26
+ "react",
27
+ "vite",
28
+ "template",
29
+ "auth",
30
+ "billing"
31
+ ],
32
+ "dependencies": {
33
+ "@clack/prompts": "^0.11.0",
34
+ "picocolors": "^1.1.1"
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ # Required — your project id in the partner ecosystem.
2
+ VITE_AUTH_PROJECT_ID=436582-forms.abprojects.com
3
+
4
+ # Auth-as-a-Service origin. The SDK also reads this var directly.
5
+ VITE_AUTH_API_BASE_URL=https://sso.1automations.com
6
+
7
+ # Your own product API (used by src/lib/http.ts, unrelated to auth).
8
+ VITE_API_BASE_URL=
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["react", "typescript", "oxc"],
4
+ "rules": {
5
+ "react/rules-of-hooks": "error",
6
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
7
+ }
8
+ }
@@ -0,0 +1,196 @@
1
+ # React + Vite starter — auth & billing included
2
+
3
+ A team template: React 19, Vite, TypeScript, Tailwind v4, shadcn/ui, and
4
+ [`ab-ecosystem-sso`](https://www.npmjs.com/package/ab-ecosystem-sso) wired up for
5
+ authentication **and** billing.
6
+
7
+ ## Use it
8
+
9
+ ```bash
10
+ npm create ab-app@latest my-app
11
+ ```
12
+
13
+ The CLI (`cli/`) asks for the project id, API origins and app name, writes
14
+ `.env.local`, and optionally installs and inits git. See [PUBLISHING.md](PUBLISHING.md)
15
+ to publish it.
16
+
17
+ ## Or run this repo directly
18
+
19
+ ```bash
20
+ cp .env.example .env.local # set VITE_AUTH_PROJECT_ID
21
+ npm install
22
+ npm run dev
23
+ ```
24
+
25
+ **The running app is the documentation.** Sign in and the sidebar has Overview,
26
+ Project structure, Configuration, and guides for Auth, Billing, Routing, Theming
27
+ and calling your own API — plus a Live example page showing the hooks against
28
+ real data. Source: `src/routes/docs/`.
29
+
30
+ When you start building your product, delete `src/routes/docs/` and
31
+ `src/routes/app/example.tsx`, and replace the groups in `src/config/navigation.ts`.
32
+ This README stays as the offline copy.
33
+
34
+ Missing config fails loudly: without `VITE_AUTH_PROJECT_ID` the app renders a
35
+ setup screen instead of an opaque network error.
36
+
37
+ ## The one rule
38
+
39
+ **Product code imports from `@/lib/sso`, never from `ab-ecosystem-sso`.**
40
+
41
+ Every SDK call takes `(projectId, token, apiBaseUrl)`. Those are bound once in
42
+ `src/lib/sso/config.ts`, so nothing downstream repeats them — and an SDK upgrade
43
+ is a change to one folder.
44
+
45
+ ## What's in `src/lib/sso`
46
+
47
+ | File | Responsibility |
48
+ | --- | --- |
49
+ | `config.ts` | The three SDK inputs, bound once. Spread `ssoProps` into any SDK component. |
50
+ | `token-store.ts` | The **only** place that touches token storage. Cross-tab aware; falls back to memory when storage is blocked. |
51
+ | `auth-provider.tsx` / `auth-context.ts` | Owns the token app-wide. `useAuth() → { token, isAuthenticated, signIn, signOut }`. |
52
+ | `auth-screens.tsx` | The SDK's `Login` / `Signup` / `ForgotPassword` / `VerifyEmail`, bound to config, token store and router. |
53
+ | `protected-route.tsx` | `<ProtectedRoute>` (guard + `SessionProvider`) and `<PublicOnlyRoute>`. |
54
+ | `queries.ts` | Cached, pre-bound `useProfile` / `useWallet` / `useCreditWallets` / `useRawSubscription`. |
55
+ | `entitlements.ts` | Normalises the **raw** `/subscription` payload into `useEntitlements() → { isActive, has(id), countOf(id) }`. |
56
+ | `require-plan.tsx` | `<RequirePlan>` — per-feature gating. |
57
+ | `account-settings.tsx` | `useAccountSettings().open(section)` — the billing surface. |
58
+
59
+ ### Auth screens are central
60
+
61
+ The SDK exposes no `loginApi` / `signupApi` — sign-in and sign-up are reachable
62
+ *only* through its components, which is also what gives you the project's
63
+ captcha config, the 2FA OTP step and magic-link sign-in for free.
64
+
65
+ **The auth pages have no layout of their own.** Each SDK screen already renders a
66
+ full-page centered root (`flex min-h-screen items-center justify-center p-4`)
67
+ plus its own card, branding and legal links, driven by the project's
68
+ `auth-config` — so any wrapper would just double-wrap it. Route files are one
69
+ line each:
70
+
71
+ ```tsx
72
+ export default function LoginPage() {
73
+ return <SsoLogin />
74
+ }
75
+ ```
76
+
77
+ Routes exist because the emailed links need them:
78
+
79
+ | Route | Screen |
80
+ | --- | --- |
81
+ | `/login` | `Login` — also consumes a `#<access_token>` magic-link hash |
82
+ | `/signup` | `Signup` |
83
+ | `/forgot-password` | `ForgotPassword` — request a reset link |
84
+ | `/reset-password/:token` | `ForgotPassword` — set a new password |
85
+ | `/verify/:token` | `VerifyEmail` |
86
+
87
+ `/reset-password?token=` and `/verify-email?token=` are kept as aliases in case a
88
+ link carries the token as a query string.
89
+
90
+ The two token routes sit **outside** `<PublicOnlyRoute>`: a user who just signed
91
+ up is already signed in, and must still be able to verify or reset.
92
+
93
+ To restyle these screens, pass `classNames` (`root`, `card`, `button`, `input`, …)
94
+ from `auth-screens.tsx`, or retheme via the `--aas-*` bridge below — not by
95
+ wrapping them in app markup.
96
+
97
+ ### The login response shape
98
+
99
+ `onSuccess` receives the **raw API envelope**, not the `{ token, user }` that the
100
+ SDK's `AuthResponse` type declares:
101
+
102
+ ```json
103
+ { "success": true, "message": "Login successful",
104
+ "data": { "accessToken": "eyJhbGciOi…" } }
105
+ ```
106
+
107
+ `extractToken()` in `auth-screens.tsx` reads `data.accessToken` first and falls
108
+ back through the other spellings, so a change on either side cannot silently
109
+ break sign-in — and a response with no token now raises an error toast instead
110
+ of doing nothing. Because `onSuccess` is typed `(data: any)`, TypeScript will not
111
+ catch this for you.
112
+
113
+ The token then goes to `tokenStore` (localStorage), which is what `useAuth()`,
114
+ the SDK getters in `queries.ts`, and the `Authorization: Bearer` header in
115
+ `src/lib/http.ts` all read from.
116
+
117
+ ### Notifications
118
+
119
+ Each completed auth call raises one sonner toast from `auth-screens.tsx`, all
120
+ sharing a single toast id so a retry replaces the previous one rather than
121
+ stacking. Errors are echoed the same way, alongside the screens' own inline
122
+ error state.
123
+
124
+ | Flow | Callback | Toast |
125
+ | --- | --- | --- |
126
+ | Login | `onSuccess` | Signed in. |
127
+ | Signup (session returned) | `onSuccess` | Account created. |
128
+ | Signup (verification required) | `onSuccess`, no token | Account created. Check your email to verify it. |
129
+ | Reset password | `onResetSuccess` | Password updated. |
130
+ | Verify email | `onVerified` | Email verified. |
131
+ | Any failure | `onError` | The error message. |
132
+
133
+ **Two gaps, both in the SDK rather than here:**
134
+
135
+ - *Forgot password (request a link)* fires no success callback — `ForgotPassword`
136
+ accepts only `onError`, `onResetSuccess` and `onBackToLogin`, and there is no
137
+ exported `forgotPasswordApi` to call directly. The screen shows its own inline
138
+ confirmation; a toast would need the SDK to expose `onSuccess` on that step.
139
+ - *Resend verification email* does not exist — no exported function, no endpoint,
140
+ no UI anywhere in the package.
141
+
142
+ ### Billing has no route — by design
143
+
144
+ Plans, add-ons, wallet and credit wallets all live in the SDK's Account Settings
145
+ modal. The sidebar user menu opens it on the right section:
146
+
147
+ ```tsx
148
+ const { open } = useAccountSettings()
149
+ open("plans") // "general" | "account" | "wallet" | "plans" | "credits"
150
+ ```
151
+
152
+ Gating comes in two strengths:
153
+
154
+ ```tsx
155
+ // Hard paywall for the whole app shell — src/App.tsx
156
+ <ProtectedRoute enforceActiveSubscription />
157
+
158
+ // Per-feature
159
+ <RequirePlan ids={["addon-exports"]} fallback={<Upgrade />}>…</RequirePlan>
160
+ ```
161
+
162
+ > **Check this first when gates misbehave.** `getUserSubscription` returns the
163
+ > API response unmapped, so `mapSubscription()` in `entitlements.ts` guesses at
164
+ > field spellings. Log one real payload and correct the readers there — every
165
+ > gate in the app follows from that one function.
166
+
167
+ ## Dashboard
168
+
169
+ `sidebar-07`, rewired: nav comes from `src/config/navigation.ts` (with optional
170
+ `requiresIds` for entitlement-gated items), links use the router instead of
171
+ `<a href>`, the user card reads the live profile, and the footer shows the first
172
+ credit-wallet balance. The block's `team-switcher` was dropped — add it back as a
173
+ project switcher if one app ever serves multiple `projectId`s.
174
+
175
+ ## Theming
176
+
177
+ `src/styles/aas-theme.css` maps the SDK's `--aas-*` variables onto the shadcn
178
+ tokens, so the auth screens and the settings modal match the app in light and
179
+ dark. The SDK's CSS is scoped and unlayered — it cannot leak into your app, and
180
+ your preflight cannot override it — which is exactly why this bridge is needed.
181
+
182
+ ## Your own API
183
+
184
+ `src/lib/http.ts` attaches the same bearer token and, on a 401, clears the token
185
+ so the route guard bounces to `/login`. Set `VITE_API_BASE_URL`.
186
+
187
+ ## Notes
188
+
189
+ - `ab-ecosystem-sso` is **pinned exactly**. It is pre-1.0; upgrade deliberately.
190
+ - Tokens live in `localStorage` (the SDK exposes no refresh flow), so an XSS bug
191
+ exposes them. Swapping strategy is a one-file change in `token-store.ts`.
192
+ - Deep links need an SPA fallback — `public/_redirects` and `vercel.json` are included.
193
+
194
+ ## Scripts
195
+
196
+ `npm run dev` · `npm run build` · `npm run typecheck` · `npm run lint` · `npm run preview`