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,152 @@
1
+ import { useNavigate, useParams, useSearchParams } from "react-router-dom"
2
+ import { ForgotPassword, Login, Signup, VerifyEmail } from "ab-ecosystem-sso"
3
+ import { toast } from "sonner"
4
+
5
+ import { useAuth } from "./auth-context"
6
+ import { ssoProps } from "./config"
7
+
8
+ /**
9
+ * The SDK's auth screens, bound once to this project's config, the token store
10
+ * and the router. The only components that render SDK auth UI.
11
+ */
12
+
13
+ /** Narrows the SDK card from its default max-w-md. Restyle the screens here. */
14
+ const authClassNames = { card: "max-w-sm!" }
15
+
16
+ type AuthPayload = {
17
+ token?: string
18
+ accessToken?: string
19
+ data?: { token?: string; accessToken?: string }
20
+ }
21
+
22
+ /**
23
+ * onSuccess receives the raw API envelope — { success, message, data:
24
+ * { accessToken } } — not the { token, user } the SDK's AuthResponse type
25
+ * declares. Read every shape so a backend change cannot silently break sign-in.
26
+ */
27
+ function extractToken(payload: AuthPayload | undefined): string | undefined {
28
+ return (
29
+ payload?.data?.accessToken ??
30
+ payload?.data?.token ??
31
+ payload?.accessToken ??
32
+ payload?.token
33
+ )
34
+ }
35
+
36
+ /** One shared id, so a retry replaces the toast instead of stacking a second one. */
37
+ const TOAST_ID = "auth"
38
+
39
+ function notify(message: string) {
40
+ toast.success(message, { id: TOAST_ID })
41
+ }
42
+
43
+ /** The screens also render an inline error; the toast is the app-level echo. */
44
+ function reportAuthError(error: Error) {
45
+ toast.error(error.message || "Something went wrong. Please try again.", {
46
+ id: TOAST_ID,
47
+ })
48
+ }
49
+
50
+ /** Honours ?next= from a guarded route; relative same-origin paths only. */
51
+ function useNextPath() {
52
+ const [params] = useSearchParams()
53
+ const next = params.get("next")
54
+ return next && next.startsWith("/") && !next.startsWith("//") ? next : "/"
55
+ }
56
+
57
+ export function SsoLogin() {
58
+ const navigate = useNavigate()
59
+ const { signIn } = useAuth()
60
+ const next = useNextPath()
61
+
62
+ return (
63
+ <Login
64
+ {...ssoProps}
65
+ classNames={authClassNames}
66
+ onSuccess={(data) => {
67
+ const token = extractToken(data)
68
+ if (!token) {
69
+ reportAuthError(new Error("Signed in, but no access token was returned."))
70
+ return
71
+ }
72
+ signIn(token)
73
+ notify("Signed in.")
74
+ navigate(next, { replace: true })
75
+ }}
76
+ onError={reportAuthError}
77
+ onForgotPassword={() => navigate("/forgot-password")}
78
+ onSignupClick={() => navigate("/signup")}
79
+ />
80
+ )
81
+ }
82
+
83
+ export function SsoSignup() {
84
+ const navigate = useNavigate()
85
+ const { signIn } = useAuth()
86
+ const next = useNextPath()
87
+
88
+ return (
89
+ <Signup
90
+ {...ssoProps}
91
+ classNames={authClassNames}
92
+ onSuccess={(data) => {
93
+ const token = extractToken(data)
94
+ // Projects requiring email verification return no session here.
95
+ if (!token) {
96
+ notify("Account created. Check your email to verify it.")
97
+ navigate("/login", { replace: true })
98
+ return
99
+ }
100
+ signIn(token)
101
+ notify("Account created.")
102
+ navigate(next, { replace: true })
103
+ }}
104
+ onError={reportAuthError}
105
+ onLoginClick={() => navigate("/login")}
106
+ />
107
+ )
108
+ }
109
+
110
+ /** Serves /forgot-password and /reset-password/:token — resetToken switches the step. */
111
+ export function SsoForgotPassword() {
112
+ const navigate = useNavigate()
113
+ const { token: paramToken } = useParams()
114
+ const [params] = useSearchParams()
115
+ const resetToken = paramToken ?? params.get("token") ?? undefined
116
+
117
+ return (
118
+ <ForgotPassword
119
+ {...ssoProps}
120
+ classNames={authClassNames}
121
+ resetToken={resetToken}
122
+ onResetSuccess={() => {
123
+ notify("Password updated. Sign in with your new password.")
124
+ navigate("/login", { replace: true })
125
+ }}
126
+ onBackToLogin={() => navigate("/login")}
127
+ onError={reportAuthError}
128
+ />
129
+ )
130
+ }
131
+
132
+ /** Serves /verify/:token. */
133
+ export function SsoVerifyEmail() {
134
+ const navigate = useNavigate()
135
+ const { token: paramToken } = useParams()
136
+ const [params] = useSearchParams()
137
+
138
+ return (
139
+ <VerifyEmail
140
+ {...ssoProps}
141
+ classNames={authClassNames}
142
+ token={paramToken ?? params.get("token") ?? undefined}
143
+ onVerified={() => {
144
+ notify("Email verified. You can sign in now.")
145
+ navigate("/login", { replace: true })
146
+ }}
147
+ onLoginClick={() => navigate("/login")}
148
+ onSignupClick={() => navigate("/signup")}
149
+ onError={reportAuthError}
150
+ />
151
+ )
152
+ }
@@ -0,0 +1,13 @@
1
+ import { env } from "@/config/env"
2
+
3
+ /** The three inputs every SDK component and function takes, bound once. */
4
+ export const ssoConfig = {
5
+ projectId: env.authProjectId,
6
+ apiBaseUrl: env.authApiBaseUrl,
7
+ } as const
8
+
9
+ /** Spread into any SDK component: <Login {...ssoProps} /> */
10
+ export const ssoProps = {
11
+ projectId: ssoConfig.projectId,
12
+ apiBaseUrl: ssoConfig.apiBaseUrl,
13
+ } as const
@@ -0,0 +1,101 @@
1
+ import { useMemo } from "react"
2
+
3
+ import { useRawSubscription } from "./queries"
4
+
5
+ export type Entitlements = {
6
+ isActive: boolean
7
+ planId: string
8
+ status: string
9
+ renewsAt: string
10
+ /** Main plan + packages + add-ons. */
11
+ subscribedIds: string[]
12
+ /** How many times each add-on id appears. */
13
+ packageCounts: Record<string, number>
14
+ }
15
+
16
+ const EMPTY: Entitlements = {
17
+ isActive: false,
18
+ planId: "",
19
+ status: "",
20
+ renewsAt: "",
21
+ subscribedIds: [],
22
+ packageCounts: {},
23
+ }
24
+
25
+ const ACTIVE_STATUSES = new Set(["paid", "active", "trialing", "trial"])
26
+
27
+ function asRecord(value: unknown): Record<string, unknown> {
28
+ return typeof value === "object" && value !== null
29
+ ? (value as Record<string, unknown>)
30
+ : {}
31
+ }
32
+
33
+ function str(source: Record<string, unknown>, ...keys: string[]): string {
34
+ for (const key of keys) {
35
+ const value = source[key]
36
+ if (typeof value === "string" && value) return value
37
+ if (typeof value === "number") return String(value)
38
+ }
39
+ return ""
40
+ }
41
+
42
+ function ids(value: unknown): string[] {
43
+ if (!Array.isArray(value)) return []
44
+ return value
45
+ .map((item) =>
46
+ typeof item === "string"
47
+ ? item
48
+ : str(asRecord(item), "id", "planId", "packageId", "addOnId"),
49
+ )
50
+ .filter(Boolean)
51
+ }
52
+
53
+ /**
54
+ * `getUserSubscription` returns the API response unmapped, so the field
55
+ * spellings are guessed here. Fix them HERE once and every gate follows.
56
+ */
57
+ export function mapSubscription(raw: unknown): Entitlements {
58
+ const outer = asRecord(raw)
59
+ const source = "data" in outer ? asRecord(outer.data) : outer
60
+ if (Object.keys(source).length === 0) return EMPTY
61
+
62
+ const planId = str(source, "planId", "plan_id", "plan", "id")
63
+ const status = str(source, "status", "type", "state").toLowerCase()
64
+ const renewsAt = str(source, "renewsAt", "renews_at", "expiryDate", "expiry", "endDate")
65
+
66
+ const subscribedIds = [
67
+ ...(planId ? [planId] : []),
68
+ ...ids(source.packages),
69
+ ...ids(source.addOns ?? source.addons ?? source.add_ons),
70
+ ...ids(source.subscribedIds ?? source.subscribed_ids),
71
+ ]
72
+
73
+ const packageCounts: Record<string, number> = {}
74
+ for (const id of subscribedIds) {
75
+ packageCounts[id] = (packageCounts[id] ?? 0) + 1
76
+ }
77
+
78
+ const expiry = renewsAt ? Date.parse(renewsAt) : Number.NaN
79
+ const notExpired = Number.isNaN(expiry) || expiry > Date.now()
80
+
81
+ return {
82
+ isActive: Boolean(planId) && ACTIVE_STATUSES.has(status) && notExpired,
83
+ planId,
84
+ status,
85
+ renewsAt,
86
+ subscribedIds: [...new Set(subscribedIds)],
87
+ packageCounts,
88
+ }
89
+ }
90
+
91
+ export function useEntitlements() {
92
+ const query = useRawSubscription()
93
+ const entitlements = useMemo(() => mapSubscription(query.data), [query.data])
94
+
95
+ return {
96
+ ...entitlements,
97
+ isLoading: query.isPending,
98
+ has: (id: string) => entitlements.subscribedIds.includes(id),
99
+ countOf: (id: string) => entitlements.packageCounts[id] ?? 0,
100
+ }
101
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The single entry point for auth and billing. Product code imports from here,
3
+ * never from "ab-ecosystem-sso" directly.
4
+ */
5
+ export { ssoConfig, ssoProps } from "./config"
6
+ export { tokenStore } from "./token-store"
7
+ export { AuthProvider } from "./auth-provider"
8
+ export { useAuth, type AuthContextValue } from "./auth-context"
9
+ export { ProtectedRoute, PublicOnlyRoute } from "./protected-route"
10
+ export {
11
+ SsoLogin,
12
+ SsoSignup,
13
+ SsoForgotPassword,
14
+ SsoVerifyEmail,
15
+ } from "./auth-screens"
16
+ export { useAccountSettings, useRestoreAccountSettings } from "./account-settings"
17
+ export {
18
+ ssoKeys,
19
+ useProfile,
20
+ useWallet,
21
+ useCreditWallets,
22
+ useRawSubscription,
23
+ } from "./queries"
24
+ export { useEntitlements, mapSubscription, type Entitlements } from "./entitlements"
25
+ export { RequirePlan } from "./require-plan"
26
+
27
+ export type {
28
+ AccountSettingsSection,
29
+ CreditWallet,
30
+ MappedProfile,
31
+ UserWallets,
32
+ WalletInfo,
33
+ } from "ab-ecosystem-sso"
@@ -0,0 +1,72 @@
1
+ import { useCallback, useEffect, useState } from "react"
2
+ import { Navigate, Outlet, useLocation } from "react-router-dom"
3
+ import { SessionProvider } from "ab-ecosystem-sso"
4
+
5
+ import { FullPageLoader } from "@/components/full-page-loader"
6
+ import { useAuth } from "./auth-context"
7
+ import { ssoProps } from "./config"
8
+
9
+ type ProtectedRouteProps = {
10
+ /** Hard paywall: opens Account Settings on Plans when no subscription is active. */
11
+ enforceActiveSubscription?: boolean
12
+ }
13
+
14
+ /**
15
+ * Reports whether SessionProvider is currently rendering its children. It
16
+ * renders null while validating, so this is how we know to show the loader.
17
+ */
18
+ function SessionReady({
19
+ onChange,
20
+ children,
21
+ }: {
22
+ onChange: (ready: boolean) => void
23
+ children: React.ReactNode
24
+ }) {
25
+ useEffect(() => {
26
+ onChange(true)
27
+ return () => onChange(false)
28
+ }, [onChange])
29
+
30
+ return <>{children}</>
31
+ }
32
+
33
+ /** No token → redirect without a round-trip. Token → SessionProvider validates it. */
34
+ export function ProtectedRoute({ enforceActiveSubscription }: ProtectedRouteProps) {
35
+ const { token, signOut } = useAuth()
36
+ const location = useLocation()
37
+ const [ready, setReady] = useState(false)
38
+
39
+ // Both callbacks must keep a stable identity: onSessionInvalid is in
40
+ // SessionProvider's validation effect deps, so a new function on every
41
+ // render would re-validate — and blank the page — on every navigation.
42
+ const handleSessionInvalid = useCallback(() => void signOut(), [signOut])
43
+ const handleReadyChange = useCallback((next: boolean) => setReady(next), [])
44
+
45
+ if (!token) {
46
+ const next = encodeURIComponent(`${location.pathname}${location.search}`)
47
+ return <Navigate to={`/login?next=${next}`} replace />
48
+ }
49
+
50
+ return (
51
+ <>
52
+ {!ready && <FullPageLoader label="Checking your session…" />}
53
+ <SessionProvider
54
+ {...ssoProps}
55
+ token={token}
56
+ enforceActiveSubscription={enforceActiveSubscription}
57
+ // Clearing the token falls through to the redirect above, keeping ?next=.
58
+ onSessionInvalid={handleSessionInvalid}
59
+ >
60
+ <SessionReady onChange={handleReadyChange}>
61
+ <Outlet />
62
+ </SessionReady>
63
+ </SessionProvider>
64
+ </>
65
+ )
66
+ }
67
+
68
+ /** Keeps signed-in users off the auth screens. */
69
+ export function PublicOnlyRoute() {
70
+ const { isAuthenticated } = useAuth()
71
+ return isAuthenticated ? <Navigate to="/" replace /> : <Outlet />
72
+ }
@@ -0,0 +1,55 @@
1
+ import { useQuery } from "@tanstack/react-query"
2
+ import {
3
+ getUserCreditWallet,
4
+ getUserProfile,
5
+ getUserSubscription,
6
+ getUserWallet,
7
+ } from "ab-ecosystem-sso"
8
+
9
+ import { useAuth } from "./auth-context"
10
+ import { ssoConfig } from "./config"
11
+
12
+ /** The SDK's getters, pre-bound and cached. */
13
+ export const ssoKeys = {
14
+ all: ["sso", ssoConfig.projectId] as const,
15
+ profile: () => [...ssoKeys.all, "profile"] as const,
16
+ wallet: () => [...ssoKeys.all, "wallet"] as const,
17
+ creditWallets: () => [...ssoKeys.all, "credit-wallets"] as const,
18
+ subscription: () => [...ssoKeys.all, "subscription"] as const,
19
+ }
20
+
21
+ function useSsoQuery<T>(key: readonly unknown[], fn: (token: string) => Promise<T>) {
22
+ const { token } = useAuth()
23
+ return useQuery({
24
+ queryKey: key,
25
+ queryFn: () => fn(token as string),
26
+ enabled: Boolean(token),
27
+ staleTime: 60_000,
28
+ retry: 1,
29
+ })
30
+ }
31
+
32
+ export function useProfile() {
33
+ return useSsoQuery(ssoKeys.profile(), (token) =>
34
+ getUserProfile(ssoConfig.projectId, token, ssoConfig.apiBaseUrl),
35
+ )
36
+ }
37
+
38
+ export function useWallet() {
39
+ return useSsoQuery(ssoKeys.wallet(), (token) =>
40
+ getUserWallet(ssoConfig.projectId, token, ssoConfig.apiBaseUrl),
41
+ )
42
+ }
43
+
44
+ export function useCreditWallets() {
45
+ return useSsoQuery(ssoKeys.creditWallets(), (token) =>
46
+ getUserCreditWallet(ssoConfig.projectId, token, ssoConfig.apiBaseUrl),
47
+ )
48
+ }
49
+
50
+ /** Raw and unmapped — prefer useEntitlements(). */
51
+ export function useRawSubscription() {
52
+ return useSsoQuery(ssoKeys.subscription(), (token) =>
53
+ getUserSubscription(ssoConfig.projectId, token, ssoConfig.apiBaseUrl),
54
+ )
55
+ }
@@ -0,0 +1,19 @@
1
+ import { useEntitlements } from "./entitlements"
2
+
3
+ type RequirePlanProps = {
4
+ /** Item ids that unlock this area. Omit to require any active subscription. */
5
+ ids?: string[]
6
+ pending?: React.ReactNode
7
+ fallback?: React.ReactNode
8
+ children: React.ReactNode
9
+ }
10
+
11
+ /** Per-feature gate — the finer-grained version of enforceActiveSubscription. */
12
+ export function RequirePlan({ ids, pending = null, fallback = null, children }: RequirePlanProps) {
13
+ const { isActive, isLoading, has } = useEntitlements()
14
+
15
+ if (isLoading) return <>{pending}</>
16
+
17
+ const entitled = ids?.length ? ids.some((id) => has(id)) : isActive
18
+ return <>{entitled ? children : fallback}</>
19
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The only place that touches token storage. Swap the strategy here and the
3
+ * whole app follows.
4
+ */
5
+ const STORAGE_KEY = "authToken"
6
+
7
+ /** Fallback when localStorage throws (private mode, blocked site data). */
8
+ let memoryToken: string | null = null
9
+ let usingMemory = false
10
+
11
+ const listeners = new Set<() => void>()
12
+
13
+ function emit() {
14
+ for (const listener of listeners) listener()
15
+ }
16
+
17
+ function read(): string | null {
18
+ if (usingMemory) return memoryToken
19
+ try {
20
+ return window.localStorage.getItem(STORAGE_KEY)
21
+ } catch {
22
+ usingMemory = true
23
+ return memoryToken
24
+ }
25
+ }
26
+
27
+ export const tokenStore = {
28
+ get: read,
29
+
30
+ set(token: string) {
31
+ memoryToken = token
32
+ try {
33
+ window.localStorage.setItem(STORAGE_KEY, token)
34
+ } catch {
35
+ usingMemory = true
36
+ }
37
+ emit()
38
+ },
39
+
40
+ clear() {
41
+ memoryToken = null
42
+ try {
43
+ window.localStorage.removeItem(STORAGE_KEY)
44
+ } catch {
45
+ usingMemory = true
46
+ }
47
+ emit()
48
+ },
49
+
50
+ /** For useSyncExternalStore. Also fires when another tab signs in or out. */
51
+ subscribe(listener: () => void) {
52
+ listeners.add(listener)
53
+ const onStorage = (event: StorageEvent) => {
54
+ if (event.key === STORAGE_KEY || event.key === null) listener()
55
+ }
56
+ window.addEventListener("storage", onStorage)
57
+ return () => {
58
+ listeners.delete(listener)
59
+ window.removeEventListener("storage", onStorage)
60
+ }
61
+ },
62
+ }
@@ -0,0 +1 @@
1
+ export { cn } from "cn"
@@ -0,0 +1,42 @@
1
+ import { StrictMode } from "react"
2
+ import { createRoot } from "react-dom/client"
3
+ import { BrowserRouter } from "react-router-dom"
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
5
+ import { ThemeProvider } from "next-themes"
6
+
7
+ import App from "@/App"
8
+ import { MissingEnv } from "@/components/missing-env"
9
+ import { Toaster } from "@/components/ui/sonner"
10
+ import { TooltipProvider } from "@/components/ui/tooltip"
11
+ import { missingRequiredEnv } from "@/config/env"
12
+ import { AuthProvider } from "@/lib/sso"
13
+ import "@/index.css"
14
+
15
+ const queryClient = new QueryClient({
16
+ defaultOptions: {
17
+ queries: { refetchOnWindowFocus: false, retry: 1 },
18
+ },
19
+ })
20
+
21
+ const missing = missingRequiredEnv()
22
+
23
+ createRoot(document.getElementById("root")!).render(
24
+ <StrictMode>
25
+ {missing.length > 0 ? (
26
+ <MissingEnv missing={missing} />
27
+ ) : (
28
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
29
+ <QueryClientProvider client={queryClient}>
30
+ <BrowserRouter>
31
+ <AuthProvider>
32
+ <TooltipProvider>
33
+ <App />
34
+ <Toaster richColors closeButton />
35
+ </TooltipProvider>
36
+ </AuthProvider>
37
+ </BrowserRouter>
38
+ </QueryClientProvider>
39
+ </ThemeProvider>
40
+ )}
41
+ </StrictMode>,
42
+ )
@@ -0,0 +1,125 @@
1
+ import {
2
+ Card,
3
+ CardContent,
4
+ CardDescription,
5
+ CardHeader,
6
+ CardTitle,
7
+ } from "@/components/ui/card"
8
+ import { Button } from "@/components/ui/button"
9
+ import { Code, Note } from "@/components/docs/doc-primitives"
10
+ import {
11
+ RequirePlan,
12
+ useAccountSettings,
13
+ useCreditWallets,
14
+ useEntitlements,
15
+ useProfile,
16
+ useWallet,
17
+ } from "@/lib/sso"
18
+
19
+ /** Live data from the SDK — the hooks in the guides, actually running. */
20
+ export default function ExamplePage() {
21
+ const { data: profile } = useProfile()
22
+ const { data: wallets } = useWallet()
23
+ const { data: credits } = useCreditWallets()
24
+ const { isActive, planId, status, renewsAt } = useEntitlements()
25
+ const { open } = useAccountSettings()
26
+
27
+ return (
28
+ <div className="mx-auto flex w-full max-w-3xl flex-col gap-6 pb-16">
29
+ <div>
30
+ <h1 className="text-2xl font-semibold tracking-tight">
31
+ Welcome back{profile?.firstName ? `, ${profile.firstName}` : ""}
32
+ </h1>
33
+ <p className="text-muted-foreground text-sm">
34
+ Every value below comes from <Code>@/lib/sso</Code> against your live
35
+ project.
36
+ </p>
37
+ </div>
38
+
39
+ <div className="grid gap-4 md:grid-cols-3">
40
+ <Card>
41
+ <CardHeader>
42
+ <CardTitle>Subscription</CardTitle>
43
+ <CardDescription>
44
+ {isActive
45
+ ? `Active${renewsAt ? ` · renews ${renewsAt}` : ""}`
46
+ : "No active plan"}
47
+ </CardDescription>
48
+ </CardHeader>
49
+ <CardContent className="flex flex-col gap-3">
50
+ <div className="truncate text-2xl font-semibold">{planId || "—"}</div>
51
+ <div className="text-muted-foreground text-xs">
52
+ status: {status || "none"}
53
+ </div>
54
+ <Button
55
+ variant={isActive ? "outline" : "default"}
56
+ onClick={() => open("plans")}
57
+ >
58
+ {isActive ? "Manage plan" : "Choose a plan"}
59
+ </Button>
60
+ </CardContent>
61
+ </Card>
62
+
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle>Wallet</CardTitle>
66
+ <CardDescription>Main balance</CardDescription>
67
+ </CardHeader>
68
+ <CardContent className="flex flex-col gap-3">
69
+ <div className="text-2xl font-semibold">
70
+ {wallets?.mainWallet?.balanceFormatted ?? "—"}
71
+ </div>
72
+ <Button variant="outline" onClick={() => open("wallet")}>
73
+ Add funds
74
+ </Button>
75
+ </CardContent>
76
+ </Card>
77
+
78
+ <Card>
79
+ <CardHeader>
80
+ <CardTitle>Credits</CardTitle>
81
+ <CardDescription>{credits?.length ?? 0} wallet(s)</CardDescription>
82
+ </CardHeader>
83
+ <CardContent className="flex flex-col gap-3">
84
+ <div className="text-2xl font-semibold">
85
+ {credits?.[0]?.balanceFormatted ?? "—"}
86
+ </div>
87
+ <Button variant="outline" onClick={() => open("credits")}>
88
+ Top up
89
+ </Button>
90
+ </CardContent>
91
+ </Card>
92
+ </div>
93
+
94
+ <RequirePlan
95
+ fallback={
96
+ <Card>
97
+ <CardHeader>
98
+ <CardTitle>Pro reports</CardTitle>
99
+ <CardDescription>Available on a paid plan.</CardDescription>
100
+ </CardHeader>
101
+ <CardContent>
102
+ <Button onClick={() => open("plans")}>Upgrade</Button>
103
+ </CardContent>
104
+ </Card>
105
+ }
106
+ >
107
+ <Card>
108
+ <CardHeader>
109
+ <CardTitle>Pro reports</CardTitle>
110
+ <CardDescription>Unlocked by your subscription.</CardDescription>
111
+ </CardHeader>
112
+ <CardContent className="text-muted-foreground text-sm">
113
+ Replace this with real product content.
114
+ </CardContent>
115
+ </Card>
116
+ </RequirePlan>
117
+
118
+ <Note>
119
+ The card above is wrapped in <Code>{"<RequirePlan>"}</Code> with no{" "}
120
+ <Code>ids</Code>, so it requires any active subscription. Pass{" "}
121
+ <Code>ids={"{[\"addon-x\"]}"}</Code> to gate on a specific add-on instead.
122
+ </Note>
123
+ </div>
124
+ )
125
+ }
@@ -0,0 +1,5 @@
1
+ import { SsoForgotPassword } from "@/lib/sso"
2
+
3
+ export default function ForgotPasswordPage() {
4
+ return <SsoForgotPassword />
5
+ }