diff --git a/.changeset/align-status-and-sender-naming.md b/.changeset/align-status-and-sender-naming.md
new file mode 100644
index 0000000..8fe1bf0
--- /dev/null
+++ b/.changeset/align-status-and-sender-naming.md
@@ -0,0 +1,19 @@
+---
+"@timonwa/firebase-hooks": minor
+---
+
+**Breaking: `useVerifyEmail`'s `status` uses the standard async vocabulary.** `"processing"` is now `"pending"` and `"failed"` is now `"error"`, matching TanStack Query's `QueryStatus` rather than spelling the same three states differently.
+
+```tsx
+- if (status === 'processing') return ;
+- if (status === 'failed') return ;
++ if (status === 'pending') return ;
++ if (status === 'error') return ;
+```
+
+**Breaking: `useEmailLinkSignIn`'s `sendLink` option is now `sendEmail`.** All three emailed-link hooks take the same option name. The returned `sendLink` function is unchanged.
+
+```tsx
+-useEmailLinkSignIn({ sendLink: email => api.send(email) });
++useEmailLinkSignIn({ sendEmail: ({ email }) => api.send(email) });
+```
diff --git a/.changeset/config.json b/.changeset/config.json
index 7fb5a92..7f95297 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -1,6 +1,9 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
- "changelog": ["@changesets/changelog-github", { "repo": "Timonwa/firebase-hooks" }],
+ "changelog": [
+ "@changesets/changelog-github",
+ { "repo": "Timonwa/firebase-hooks" }
+ ],
"commit": false,
"access": "public",
"baseBranch": "main",
diff --git a/.changeset/delegate-email-sends.md b/.changeset/delegate-email-sends.md
new file mode 100644
index 0000000..be144f7
--- /dev/null
+++ b/.changeset/delegate-email-sends.md
@@ -0,0 +1,14 @@
+---
+"@timonwa/firebase-hooks": minor
+---
+
+`useSendPasswordResetEmail` and `useSendEmailVerification` gain a `sendEmail` option, matching `useEmailLinkSignIn`'s `sendLink`. Both hooks previously sent from the browser with no way to delegate, which put an email-sending path outside an app's own rate limiter — awkward when the same app already rate-limits the sign-in link server-side.
+
+```tsx
+const { send } = useSendPasswordResetEmail({
+ sendEmail: ({ email, actionCodeSettings }) =>
+ requestPasswordReset(email, actionCodeSettings),
+});
+```
+
+The hook keeps its own bookkeeping either way: `loading`, `error`, `success` and `resetState` behave identically, and a throwing sender surfaces as an ordinary failure result. The sender receives `{ email, actionCodeSettings }` — the same inputs Firebase's client send takes and the Admin SDK's `generate*Link` wants, so a provider-level `actionCodeSettings` still reaches your server. On `useSendEmailVerification`, `email` is the signed-in user's address, since `send()` takes no arguments; an account without one fails clearly rather than calling your sender with nothing.
diff --git a/.changeset/export-result-types.md b/.changeset/export-result-types.md
new file mode 100644
index 0000000..d443c19
--- /dev/null
+++ b/.changeset/export-result-types.md
@@ -0,0 +1,12 @@
+---
+"@timonwa/firebase-hooks": minor
+---
+
+Two result types are no longer module-private:
+
+- `CompleteSignInResult` — what `useEmailLinkSignIn`'s `completeSignIn` resolves to, so a callback page can annotate the function that handles it
+- `VerifyEmailStatus` — `useVerifyEmail`'s status, instead of re-declaring the three states in app code
+
+Both ship from `@timonwa/firebase-hooks/auth`.
+
+The status vocabulary is also now shared. `AsyncStatus` (`"processing" | "success" | "failed"`) ships from the root entry, and `VerifyEmailStatus` is an alias of it, so hooks that act on mount report the same three states rather than each spelling them differently. There is no idle state: the work starts before a render, which is why `processing` is the initial value.
diff --git a/.changeset/export-wrapper-types.md b/.changeset/export-wrapper-types.md
new file mode 100644
index 0000000..f4944f6
--- /dev/null
+++ b/.changeset/export-wrapper-types.md
@@ -0,0 +1,18 @@
+---
+"@timonwa/firebase-hooks": minor
+---
+
+Every type needed to write a wrapper around a hook is now reachable from `@timonwa/firebase-hooks/auth`:
+
+- all 13 `Use*Options` interfaces, so a wrapper can accept and forward a hook's options without restating them
+- `AuthProviderProps` and `UseAuthResult` (what `useAuth` returns), for an app provider layered over this one
+
+```tsx
+import { useLogin, type UseLoginOptions } from "@timonwa/firebase-hooks/auth";
+
+export function useAppLogin(options?: UseLoginOptions) {
+ return useLogin(options);
+}
+```
+
+`HookResult`, `HookErrorOptions` and `HookErrorContext` are unchanged and still ship from the root entry — every service returns them, so they keep one home rather than being re-exported per service.
diff --git a/.changeset/named-result-types.md b/.changeset/named-result-types.md
new file mode 100644
index 0000000..83d9947
--- /dev/null
+++ b/.changeset/named-result-types.md
@@ -0,0 +1,19 @@
+---
+"@timonwa/firebase-hooks": minor
+---
+
+Every hook now exports the type it returns as `UseResult` — `UseLoginResult`, `UseSignupResult`, `UseVerifyEmailResult`, and so on, from `@timonwa/firebase-hooks/auth`. A wrapper can state its return type instead of re-deriving it with `ReturnType`.
+
+```tsx
+import {
+ useLogin,
+ type UseLoginOptions,
+ type UseLoginResult,
+} from "@timonwa/firebase-hooks/auth";
+
+export function useAppLogin(options?: UseLoginOptions): UseLoginResult {
+ return useLogin(options);
+}
+```
+
+Same shape as TanStack Query's `UseQueryResult` / `UseMutationResult`. `useAuth` already returned the named `UseAuthResult`.
diff --git a/.changeset/provider-senders.md b/.changeset/provider-senders.md
new file mode 100644
index 0000000..3f00cf5
--- /dev/null
+++ b/.changeset/provider-senders.md
@@ -0,0 +1,20 @@
+---
+"@timonwa/firebase-hooks": minor
+---
+
+`AuthProvider` gains `senders`, so an app that emails links from its own API configures that once instead of at every call site:
+
+```tsx
+ api.sendSignInLink(email, actionCodeSettings),
+ passwordReset: ({ email, actionCodeSettings }) => api.sendPasswordReset(email, actionCodeSettings),
+ emailVerification: ({ email, actionCodeSettings }) => api.sendVerification(email, actionCodeSettings),
+ }}
+>
+```
+
+One key per flow rather than a single sender: the three send different emails, so one sender for all of them could mail a password reset to someone asking to verify an address. The shape follows the same convention as TanStack Query's `defaultOptions`, which namespaces defaults by operation kind so the per-call signature stays identical to the global one.
+
+Each key follows the existing rule — a hook's own `sendLink`/`sendEmail` overrides it, and `null` opts that one flow back to Firebase's client-side send.
diff --git a/.changeset/status-instead-of-loading.md b/.changeset/status-instead-of-loading.md
new file mode 100644
index 0000000..e3007d0
--- /dev/null
+++ b/.changeset/status-instead-of-loading.md
@@ -0,0 +1,20 @@
+---
+"@timonwa/firebase-hooks": minor
+---
+
+**Breaking: every action hook reports `status` with derived booleans, replacing `loading`, `success` and `resetState`.**
+
+```tsx
+const { login, status, isIdle, isPending, isSuccess, isError, error, reset } =
+ useLogin();
+// ^ 'idle' | 'pending' | 'success' | 'error'
+```
+
+| Before | After |
+| ------------------------------------------ | ------------------------------- |
+| `loading` | `isPending` |
+| `success` (six hooks, hand-rolled) | `isSuccess` — now on every hook |
+| `resetState()` (two hooks) | `reset()` — now on every hook |
+| `useEmailLinkSignIn`'s returned `setError` | removed; `reset()` covers it |
+
+The booleans are derived from `status`, so exactly one is ever true. This is TanStack Query's mutation result, field for field, and the new `ActionStatus` type ships from the root entry beside `AsyncStatus`. Hooks that act on mount — `useVerifyEmail` — keep `AsyncStatus` (no `idle`, since the work starts before you can render) and gain the same `isPending`/`isSuccess`/`isError`.
diff --git a/.prettierignore b/.prettierignore
index 8922ae3..1f80343 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,6 +1,20 @@
node_modules
dist
-apps
+
+# Biome's territory — two formatters on one file fight forever.
+packages/**/*.ts
+packages/**/*.tsx
+packages/**/*.json
+/*.ts
+/*.json
+
+# Next build output
+**/.next
+**/.source
+**/next-env.d.ts
# Generated by changesets — reformatting it would churn on every release.
**/CHANGELOG.md
+
+# Machine-owned; pnpm writes it.
+pnpm-lock.yaml
diff --git a/.prettierrc.json b/.prettierrc.json
index d25ee21..0b08905 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -1,5 +1,20 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
- "printWidth": 90,
- "proseWrap": "never"
+ "printWidth": 80,
+ "semi": true,
+ "trailingComma": "all",
+ "arrowParens": "avoid",
+ "bracketSameLine": true,
+ "proseWrap": "never",
+ "plugins": ["prettier-plugin-tailwindcss"],
+ "overrides": [
+ {
+ "files": "apps/docs/**",
+ "options": { "tailwindStylesheet": "./apps/docs/app/global.css" }
+ },
+ {
+ "files": "apps/playground/**",
+ "options": { "tailwindStylesheet": "./apps/playground/app/globals.css" }
+ }
+ ]
}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 03758f3..1a5c10b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -24,7 +24,7 @@ This is a pnpm workspace. The published package lives in `packages/firebase-hook
- **One folder per service** (`src/core/`, `src/auth/`, later `src/firestore/`, `src/storage/`), each with its own `index.ts` entry barrel. **One file per hook**, kebab-cased after it — `src/auth/use-login.ts`. Start the file with a `"use client"` directive and a JSDoc block; the JSDoc is what editors show, so keep it agreeing with the README. Internals a service shares live in that folder's `_shared.ts` and never reach the barrel.
- **Follow the shared contract.** `auth: Auth | null` first argument; actions resolve to `HookResult` and never throw (`useAuthTask` gives you the skeleton); sensitive operations reauthenticate first.
-- **Options go in an exported `UseOptionsProps` interface** extending `HookErrorOptions`, with a TSDoc line on every field it declares itself (and `@defaultValue` where there is one). The docs site generates its options table from that interface, so an undocumented field ships an empty cell. Exported for the generator's sake — keep it out of the barrel, so the published types don't change.
+- **Options go in an exported `UseOptions` interface** extending `HookErrorOptions`, with a TSDoc line on every field it declares itself (and `@defaultValue` where there is one). The docs site generates its options table from that interface, so an undocumented field ships an empty cell. Re-export it from the barrel too, so an app can type a wrapper around the hook without restating the shape.
- **Export it explicitly** from its service's entry barrel (`src/auth/index.ts` for auth), one line per file, alphabetical. The root entry (`src/core/index.ts`) carries only the service-agnostic core — nothing service-specific is ever added to it; a new service gets a new folder + subpath entry.
- **Document it in the same change** — add a page under `apps/docs/content/docs/auth/` and list it in that folder's `meta.json`. Follow the shape of the existing pages: prose intro, example, `## Returns`, then `` for the options. The table generates from the option interface's TSDoc, so document each field there rather than hand-writing a table.
- **Add it to the playground in the same change** — a section component under `apps/playground/components//`, rendered from that service's page, and its name in the right group in `apps/playground/lib/hooks-map.ts` so it appears in the sidebar. See below.
diff --git a/README.md b/README.md
index 9c9aff5..d76a20b 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
---
-Each hook runs one flow end to end. It holds its own loading, error, and success state, and sets up the browser pieces and callbacks the flow needs. Every action returns a result you can branch on: `{ success: true, … }` when it works, `{ success: false, error, code, cause }` when it doesn't. Firebase's own response is on both paths.
+Each hook runs one flow end to end. It holds its own status and error, and sets up the browser pieces and callbacks the flow needs. Every action returns a result you can branch on: `{ success: true, … }` when it works, `{ success: false, error, code, cause }` when it doesn't. Firebase's own response is on both paths.
Zero dependencies — `firebase` and `react` are peers.
@@ -37,7 +37,7 @@ Create your `Auth` instance once with the Firebase SDK, then wrap your app:
import { AuthProvider } from "@timonwa/firebase-hooks/auth";
import { auth } from "@/lib/firebase"; // getAuth(initializeApp(config))
- createSession(idToken)}>
+ createSession(idToken)}>
{children}
;
```
@@ -48,7 +48,7 @@ Hooks below it need nothing passed:
import { useLogin } from "@timonwa/firebase-hooks/auth";
function LoginForm() {
- const { login, loading, error } = useLogin();
+ const { login, isPending, error } = useLogin();
async function onSubmit(email: string, password: string) {
const result = await login(email, password);
@@ -74,7 +74,10 @@ The most-used services ship first; more (Realtime Database, Remote Config, Cloud
Each service is its own import, so an app only carries the services it uses. The root holds what every service shares — `formatFirebaseError`, `getFirebaseErrorCode`, and the `HookResult` types.
```ts
-import { formatFirebaseError, getFirebaseErrorCode } from "@timonwa/firebase-hooks";
+import {
+ formatFirebaseError,
+ getFirebaseErrorCode,
+} from "@timonwa/firebase-hooks";
import {
AuthProvider,
useLogin,
@@ -89,7 +92,7 @@ Every hook has its own page — signature, options, and a worked example — in
One contract, so learning one hook is learning them all:
- **Every hook needs an `Auth` instance** — taken from `AuthProvider`, or passed in as the first argument to override it. No global, no hidden singleton. Passing `null` means "not ready yet", never "use the provider's", so an action called before `auth` exists fails cleanly with `{ success: false, error }`.
-- **Every action resolves to `HookResult`** — `{ success: true, ...data }` or `{ success: false, error, code, cause }`. The hook's `error` state carries the same message for rendering, and `loading` and `success` track the action. See [Error handling](#error-handling).
+- **Every action resolves to `HookResult`** — `{ success: true, ...data }` or `{ success: false, error, code, cause }`. The hook's `status` — `idle`, `pending`, `success`, `error` — tracks the action, with `isPending`/`isSuccess`/`isError` derived from it and `reset()` to go back to idle; `error` carries the message for rendering. See [Error handling](#error-handling).
- **`onIdToken(idToken, user)` runs after a successful sign-in**, with a freshly minted token. Throw inside it to abort the flow; the error surfaces like any other.
- **`currentPassword` triggers reauthentication** in `useUpdatePassword`, `useUpdateEmail`, and `useDeleteAccount`. `useReauthenticate` exposes the same step for custom flows.
- **Provider options are defaults, hook options win.** `onIdToken`, `onBeforeSignOut`, `actionCodeSettings`, `formatErrorMessage`, and the `onError` observer can all be set once on `AuthProvider`; a hook's own option overrides the provider, and an explicit `null` opts that flow out entirely:
@@ -97,11 +100,12 @@ One contract, so learning one hook is learning them all:
```tsx
createSession(idToken)}
+ onIdToken={idToken => createSession(idToken)}
onBeforeSignOut={() => clearSession()}
actionCodeSettings={{ url: `${origin}/auth/action`, handleCodeInApp: true }}
- formatErrorMessage={(e) => formatFirebaseError(e, { messages: AUTH_ERROR_MESSAGES })}
->
+ formatErrorMessage={e =>
+ formatFirebaseError(e, { messages: AUTH_ERROR_MESSAGES })
+ }>
{children}
;
@@ -149,8 +153,7 @@ For logging and analytics, the provider's **`onError` observer** sees every fail
```tsx
track("auth_error", { action, code })}
->
+ onError={(error, { action, code }) => track("auth_error", { action, code })}>
{children}
```
diff --git a/apps/docs/.prettierignore b/apps/docs/.prettierignore
deleted file mode 100644
index 324551b..0000000
--- a/apps/docs/.prettierignore
+++ /dev/null
@@ -1,4 +0,0 @@
-.next
-.source
-node_modules
-next-env.d.ts
diff --git a/apps/docs/.prettierrc.json b/apps/docs/.prettierrc.json
deleted file mode 100644
index 7659439..0000000
--- a/apps/docs/.prettierrc.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/prettierrc",
- "singleQuote": true,
- "semi": true,
- "trailingComma": "all",
- "printWidth": 90,
- "plugins": ["prettier-plugin-tailwindcss"],
- "overrides": [
- {
- "files": "*.mdx",
- "options": {
- "proseWrap": "never"
- }
- }
- ]
-}
diff --git a/apps/docs/README.md b/apps/docs/README.md
index cac2c1f..261aaaa 100644
--- a/apps/docs/README.md
+++ b/apps/docs/README.md
@@ -12,9 +12,9 @@ Open .
## Environment
-| Variable | Required | What it's for |
-| ---------------------- | -------- | ------------------------------------------------------------- |
-| `NEXT_PUBLIC_SITE_URL` | No | Absolute origin for canonicals, the sitemap and OG image URLs |
+| Variable | Required | What it's for |
+| --- | --- | --- |
+| `NEXT_PUBLIC_SITE_URL` | No | Absolute origin for canonicals, the sitemap and OG image URLs |
**Nothing to configure on Vercel.** The origin resolves from `VERCEL_PROJECT_PRODUCTION_URL`, which Vercel sets automatically. Set `NEXT_PUBLIC_SITE_URL` only once a custom domain points at the site.
@@ -22,17 +22,17 @@ Locally the origin falls back to `http://localhost:3000`. If your dev server pic
## Layout
-| Path | What it is |
-| ----------------------- | ---------------------------------------------------------------------------- |
-| `content/docs/` | The MDX pages; `meta.json` per folder controls nav order |
-| `lib/source.ts` | Content source adapter — the nav, sitemap and OG routes all read from it |
-| `lib/site.ts` | Site config: origin, description, author, whether the env is indexable |
-| `lib/seo.ts` | `buildMetadata()` — every page's canonical, OG, Twitter and robots |
-| `lib/schema.ts` | JSON-LD graph, anchored by stable `@id` |
-| `lib/layout.shared.tsx` | Nav title, version pill, and the nav links |
-| `app/og/` | OG image routes — `/og` for the home page, `/og/docs/*` prerendered per page |
-| `app/(home)/` | Landing page and its footer |
-| `app/docs/` | Documentation layout and pages |
+| Path | What it is |
+| --- | --- |
+| `content/docs/` | The MDX pages; `meta.json` per folder controls nav order |
+| `lib/source.ts` | Content source adapter — the nav, sitemap and OG routes all read from it |
+| `lib/site.ts` | Site config: origin, description, author, whether the env is indexable |
+| `lib/seo.ts` | `buildMetadata()` — every page's canonical, OG, Twitter and robots |
+| `lib/schema.ts` | JSON-LD graph, anchored by stable `@id` |
+| `lib/layout.shared.tsx` | Nav title, version pill, and the nav links |
+| `app/og/` | OG image routes — `/og` for the home page, `/og/docs/*` prerendered per page |
+| `app/(home)/` | Landing page and its footer |
+| `app/docs/` | Documentation layout and pages |
## Writing a page
diff --git a/apps/docs/app/(home)/layout.tsx b/apps/docs/app/(home)/layout.tsx
index 0ac81cb..96c68a6 100644
--- a/apps/docs/app/(home)/layout.tsx
+++ b/apps/docs/app/(home)/layout.tsx
@@ -1,8 +1,8 @@
-import { HomeLayout } from 'fumadocs-ui/layouts/home';
-import { SiteFooter } from '@/components/site-footer';
-import { baseOptions } from '@/lib/layout.shared';
+import { HomeLayout } from "fumadocs-ui/layouts/home";
+import { SiteFooter } from "@/components/site-footer";
+import { baseOptions } from "@/lib/layout.shared";
-export default function Layout({ children }: LayoutProps<'/'>) {
+export default function Layout({ children }: LayoutProps<"/">) {
return (
{children}
diff --git a/apps/docs/app/(home)/page.tsx b/apps/docs/app/(home)/page.tsx
index 4488cfe..abae1c7 100644
--- a/apps/docs/app/(home)/page.tsx
+++ b/apps/docs/app/(home)/page.tsx
@@ -10,21 +10,21 @@ import {
SlidersHorizontal,
UserCog,
Workflow,
-} from 'lucide-react';
-import Link from 'next/link';
-import type { ReactNode } from 'react';
-import { CodeSample } from '@/components/code-sample';
-import { CopyButton } from '@/components/copy-button';
-import type { Metadata } from 'next';
-import { buildMetadata } from '@/lib/seo';
-import { npmUrl, packageName, packageVersion } from '@/lib/shared';
+} from "lucide-react";
+import Link from "next/link";
+import type { ReactNode } from "react";
+import { CodeSample } from "@/components/code-sample";
+import { CopyButton } from "@/components/copy-button";
+import type { Metadata } from "next";
+import { buildMetadata } from "@/lib/seo";
+import { npmUrl, packageName, packageVersion } from "@/lib/shared";
export const metadata: Metadata = buildMetadata({
// No `title` key: the home page keeps the root layout's default title rather
// than having the template append the package name a second time.
description:
- 'Typed React hooks for every Firebase Auth flow — email/password, OAuth, magic link, phone and anonymous sign-in, plus password, email, profile and provider linking. Zero dependencies.',
- path: '/',
+ "Typed React hooks for every Firebase Auth flow — email/password, OAuth, magic link, phone and anonymous sign-in, plus password, email, profile and provider linking. Zero dependencies.",
+ path: "/",
});
const INSTALL_COMMAND = `pnpm add ${packageName} firebase`;
@@ -32,80 +32,92 @@ const INSTALL_COMMAND = `pnpm add ${packageName} firebase`;
const FEATURES = [
{
icon: Workflow,
- title: 'Whole flows, not single calls',
- body: 'usePhoneSignIn builds and tears down the reCAPTCHA verifier. useOAuthSignIn finishes a redirect when the page returns. useEmailLinkSignIn asks for the address instead of calling window.prompt.',
+ title: "Whole flows, not single calls",
+ body: "usePhoneSignIn builds and tears down the reCAPTCHA verifier. useOAuthSignIn finishes a redirect when the page returns. useEmailLinkSignIn asks for the address instead of calling window.prompt.",
},
{
icon: CircleAlert,
- title: 'Failures are values',
- body: 'Actions never throw. A failure carries Firebase’s own code and the untouched original error, so you branch on a result instead of wrapping every call in try/catch.',
+ title: "Failures are values",
+ body: "Actions never throw. A failure carries Firebase’s own code and the untouched original error, so you branch on a result instead of wrapping every call in try/catch.",
},
{
icon: KeyRound,
- title: 'Server sessions built in',
- body: 'onIdToken hands you a fresh ID token as part of the sign-in, not after it. Throw inside it and the sign-in aborts, so a user can’t land on a protected page without a server session.',
+ title: "Server sessions built in",
+ body: "onIdToken hands you a fresh ID token as part of the sign-in, not after it. Throw inside it and the sign-in aborts, so a user can’t land on a protected page without a server session.",
},
{
icon: PackageOpen,
- title: 'Nothing withheld',
- body: 'Sign-ins hand back Firebase’s raw UserCredential. Error messages stay exactly as Firebase wrote them unless you opt into formatting.',
+ title: "Nothing withheld",
+ body: "Sign-ins hand back Firebase’s raw UserCredential. Error messages stay exactly as Firebase wrote them unless you opt into formatting.",
},
{
icon: SlidersHorizontal,
- title: 'Configure once, override anywhere',
- body: 'Session callbacks, action-code settings and error wording live on the provider. Any hook can override them, or opt out entirely with null.',
+ title: "Configure once, override anywhere",
+ body: "Session callbacks, action-code settings and error wording live on the provider. Any hook can override them, or opt out entirely with null.",
},
{
icon: Fingerprint,
- title: 'Reauthentication handled',
- body: 'Pass currentPassword to a sensitive operation and the recent-sign-in check happens first. Omit it, and auth/requires-recent-login reaches you to handle your own way.',
+ title: "Reauthentication handled",
+ body: "Pass currentPassword to a sensitive operation and the recent-sign-in check happens first. Omit it, and auth/requires-recent-login reaches you to handle your own way.",
},
];
const GROUPS = [
{
- label: 'Signing in and out',
+ label: "Signing in and out",
icon: LogIn,
hooks: [
- 'useLogin',
- 'useSignup',
- 'useLogout',
- 'useOAuthSignIn',
- 'useEmailLinkSignIn',
- 'usePhoneSignIn',
- 'useAnonymousSignIn',
- 'useCustomTokenSignIn',
+ "useLogin",
+ "useSignup",
+ "useLogout",
+ "useOAuthSignIn",
+ "useEmailLinkSignIn",
+ "usePhoneSignIn",
+ "useAnonymousSignIn",
+ "useCustomTokenSignIn",
],
},
{
- label: 'Passwords',
+ label: "Passwords",
icon: Lock,
- hooks: ['useSendPasswordResetEmail', 'useConfirmPasswordReset', 'useUpdatePassword'],
+ hooks: [
+ "useSendPasswordResetEmail",
+ "useConfirmPasswordReset",
+ "useUpdatePassword",
+ ],
},
{
- label: 'Email',
+ label: "Email",
icon: Mail,
- hooks: ['useSendEmailVerification', 'useVerifyEmail', 'useUpdateEmail'],
+ hooks: ["useSendEmailVerification", "useVerifyEmail", "useUpdateEmail"],
},
{
- label: 'Account and linking',
+ label: "Account and linking",
icon: UserCog,
hooks: [
- 'useUpdateProfile',
- 'useDeleteAccount',
- 'useReauthenticate',
- 'useLinkProvider',
- 'useUnlinkProvider',
+ "useUpdateProfile",
+ "useDeleteAccount",
+ "useReauthenticate",
+ "useLinkProvider",
+ "useUnlinkProvider",
],
},
];
const SERVICES = [
- { name: 'Core', entry: '@timonwa/firebase-hooks', ready: true },
- { name: 'Auth', entry: '@timonwa/firebase-hooks/auth', ready: true },
- { name: 'Firestore', entry: '@timonwa/firebase-hooks/firestore', ready: false },
- { name: 'Storage', entry: '@timonwa/firebase-hooks/storage', ready: false },
- { name: 'Cloud Functions', entry: '@timonwa/firebase-hooks/functions', ready: false },
+ { name: "Core", entry: "@timonwa/firebase-hooks", ready: true },
+ { name: "Auth", entry: "@timonwa/firebase-hooks/auth", ready: true },
+ {
+ name: "Firestore",
+ entry: "@timonwa/firebase-hooks/firestore",
+ ready: false,
+ },
+ { name: "Storage", entry: "@timonwa/firebase-hooks/storage", ready: false },
+ {
+ name: "Cloud Functions",
+ entry: "@timonwa/firebase-hooks/functions",
+ ready: false,
+ },
];
// Kept under ~56 columns so it fits a half-width column without scrolling.
@@ -139,26 +151,30 @@ const result = await login(email, password);
if (result.success) router.push("/dashboard");`;
function toSlug(hook: string) {
- return hook.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+ return hook.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}
function Section({
children,
- className = '',
+ className = "",
}: {
children: ReactNode;
className?: string;
}) {
return (
- {children}
+
+ {children}
+
);
}
function SectionHeading({ title, lead }: { title: string; lead: string }) {
return (
-
- v{packageVersion} ·
- Auth available · Firestore next
+
+ v
+ {packageVersion} · Auth available · Firestore next
- Typed React hooks for Firebase,{' '}
+ Typed React hooks for Firebase,{" "}
one hook per flow
-
- Each hook runs a whole flow end to end and holds its own loading, error and
- success state. Zero dependencies — firebase and{' '}
- react stay peers.
+
+ Each hook runs a whole flow end to end and holds its own loading,
+ error and success state. Zero dependencies — firebase{" "}
+ and react stay peers.
+ className="group inline-flex items-center gap-2 rounded-lg bg-fd-primary px-5 py-2.5 text-sm font-medium text-fd-primary-foreground transition-opacity hover:opacity-90">
Get started
+ className="surface px-5 py-2.5 text-sm font-medium transition-colors hover:bg-fd-accent">
Browse the hooks
-
+
$
{INSTALL_COMMAND}
-
+
@@ -229,7 +246,7 @@ export default function HomePage() {
only the label above and the note below — no second border. */}
-
+
Firebase directly21 lines
@@ -237,18 +254,19 @@ export default function HomePage() {
-
+
With useLogin6 lines
-
- And it does more: if createSession throws, the sign-in aborts
- rather than leaving a signed-in user with no server session.
+
+ And it does more: if createSession throws, the
+ sign-in aborts rather than leaving a signed-in user with no server
+ session.
@@ -263,7 +281,7 @@ export default function HomePage() {
{/* One hairline grid rather than six outlined cards, so the icons stay the
only accent in the section. */}
-
+
{/* The grid is pulled 1px past the container so the last column's and
last row's borders land under the container's own border and get
clipped — otherwise the rounded corners sit on straight cell borders
@@ -272,17 +290,18 @@ export default function HomePage() {
{FEATURES.map(({ icon: Icon, title, body }) => (
+ className="inline-block rounded-md border border-fd-border px-2.5 py-1 font-mono text-xs transition-colors hover:border-fd-primary/40 hover:bg-fd-primary/5">
{/* Colouring the shared prefix carries the accent through
twenty otherwise-grey chips, and shows the naming
pattern at a glance. */}
use
- {hook.slice(3)}
+
+ {hook.slice(3)}
+