# @dodomain/react (https://dodomain.io/docs/react-sdk)

The React bindings: a useDoDomainConnect hook (and a drop-in button) around the @dodomain/connect modal widget.

```bash
npm install @dodomain/react
```

React bindings for the [embeddable widget](https://dodomain.io/docs/widget). The widget's own API is imperative (`showDoDomain()` opens a modal iframe and returns a close handle), so this package's shape is a **hook that owns that handle's lifecycle**, not a pseudo-declarative modal component. It ships dual ESM + CJS builds with self-contained type declarations, requires React 18+ (peer dependency), and its only runtime dependency is `@dodomain/connect` itself; the widget is not bundled twice, and both packages stay free of any other runtime dependency.

```tsx
import { useDoDomainConnect, MOUNT_BLOCKED } from "@dodomain/react";

function ConnectDomainCard({ token }: { token: string }) {
  // token comes from your server: sessions.create() via @dodomain/node
  const { open, isOpen } = useDoDomainConnect({
    token,
    onVerified: ({ domain }) => refetchDomains(),
    onClose: ({ state }) => {
      if (state !== "verified") keepThePromptVisible();
    },
    onError: (err) => {
      // The iframe never mounted (host CSP, network, content blocker):
      // send the user to the same session full-page instead.
      if (err.code === MOUNT_BLOCKED) location.assign(err.hostedUrl);
    },
  });

  return (
    <button onClick={open} disabled={isOpen}>
      Connect your domain
    </button>
  );
}
```

The session token is the only credential involved: the widget never sees your `dd_sk_...` key, exactly as with plain `@dodomain/connect`.

## What the hook adds [#what-the-hook-adds]

Beyond forwarding the call to `showDoDomain()`, the hook carries three guarantees a hand-rolled click handler tends to get wrong:

- **Latest-props callbacks.** The widget receives stable wrapper callbacks that read the _current_ render's handlers at fire time, so a re-render while the modal is open never strands a stale closure. Inline arrow handlers are fine; no `useCallback` needed.
- **One modal at a time.** `open()` while the modal is already open is a no-op, so a double-click cannot stack two backdrops.
- **Unmount cleanup.** If the owning component unmounts while the modal is open, the modal is torn down through the widget's own close path (`onClose` still fires with the last-known state) instead of orphaning a full-screen iframe over the page. StrictMode's dev-only double-mount cycle is handled.

## `useDoDomainConnect(options)` [#usedodomainconnectoptions]

`options` is exactly the widget's `ShowDoDomainOptions`: the same fields, semantics and defaults documented on the [widget page](https://dodomain.io/docs/widget):

| Option          | Type                                    | Notes                                                                      |
| --------------- | --------------------------------------- | -------------------------------------------------------------------------- |
| `token`         | string, **required**                    | Session token (`dd_sess_...`) your server minted.                          |
| `baseUrl`       | string                                  | DoDomain origin. Defaults to `https://app.dodomain.io`.                    |
| `theme`         | `"light" \| "dark"`                     | Pass the theme your page is rendering so the sheet matches it.             |
| `onVerified`    | `(detail: { domain?: string }) => void` | Fires when the domain verifies.                                            |
| `onClose`       | `(detail: DoDomainCloseDetail) => void` | Fires on every dismissal, with the session's last-known state.             |
| `onError`       | `(detail: DoDomainWidgetError) => void` | Fires when the flow fails to load or reports a session error.              |
| `loadTimeoutMs` | number                                  | Load-handshake timeout before the embed counts as failed. Default `15000`. |

Returns:

| Field    | Type         | Notes                                                                                                                  |
| -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `open`   | `() => void` | Opens the modal with the current render's options. No-op while open. Browser-only; call it from an event handler.      |
| `close`  | `() => void` | Dismisses programmatically: fires `onClose` with the last-known state, exactly like the widget's own `handle.close()`. |
| `isOpen` | boolean      | Whether the modal is currently mounted.                                                                                |

The `onVerified` callback remains a UI cue: the signed `connection.verified` [webhook](https://dodomain.io/docs/webhooks) is the source of truth, as everywhere else.

## `<DoDomainConnectButton />` [#dodomainconnectbutton-]

When all you need is a trigger, the drop-in, a plain unstyled `<button type="button">` wired to the hook:

```tsx
import { DoDomainConnectButton } from "@dodomain/react";

<DoDomainConnectButton token={token} onVerified={() => refetchDomains()} className="btn">
  Connect your domain
</DoDomainConnectButton>;
```

It takes every hook option plus `children` (label, defaults to "Connect your domain"), `className` and `disabled`, and disables itself while the modal is open. For anything beyond a button that opens the flow, use the hook directly.

## Re-exported vocabulary [#re-exported-vocabulary]

`MOUNT_BLOCKED` and the widget's types (`ShowDoDomainOptions`, `DoDomainWidgetError`, `DoDomainCloseDetail`, `DoDomainSessionState`, `DoDomainHandle`) are re-exported, so a React app needs only this one import. Their semantics (origin-checked messages, the `MOUNT_BLOCKED` hosted-URL fallback, the close-state table, the `frame-src https://app.dodomain.io` CSP requirement) are documented on the [widget page](https://dodomain.io/docs/widget) and apply unchanged.
