Getting started
This guide takes a React App from nothing to a mounted widget that reads the Widget account. It
covers what to ask Fluent for, the install, the styles, the Network, mounting <FluentWidget>,
the connect button, reading the account, and debugging. The terms it uses (Fluent ID, External
wallet, Widget account, Widget session) are defined in concepts.md; every option
of config is in configuration.md.
What you need from Fluent
Ask Fluent for these before you write any code:
appId: the App's id,app_followed by 32 lowercase hex characters, as the Fluent Dashboard shows it.privyClientId: the login client Fluent issues for the App, aclient-…value. It is required today and is marked experimental, because it goes away when Fluent's own login replaces it (experimental.md).- Your page origins allow-listed for Direct login: Direct login happens in the App's own page, so every origin the App is served from, a development origin included, has to be allow-listed for the App's login client beforehand.
- For
getAuthToken(), Sponsorship,linkX()and User settings: Auth tokens enabled for the App, and each page origin that calls it registered on the App. Without themgetAuthToken()rejects withFluentAuthErrorandcodeapp_not_auth_enabledororigin_not_allowed(errors.md), a Fluent ID's operations are not sponsored,linkX()cannot complete a link, and a person's User settings are neither loaded nor saved. See guides/backend-auth.md. - For Sponsorship, the App enabled for Sponsorship: without it a Fluent ID's operations are not sponsored (guides/sponsorship.md).
Install
Install the package and its peer dependencies:
pnpm add @fluent.xyz/connect @tanstack/react-query react react-dom viem wagmi
The peers are @tanstack/react-query 5, react and react-dom 18 or later, viem 2 and
wagmi 2. They are peers rather than dependencies so that your App and the widget resolve one
copy of each: the viem types the Widget API takes and returns (an Address, a Hex, an
Abi) are the ones your own code imports.
You do not need a QueryClientProvider. The widget mounts a TanStack Query client of its own.
When your App mounts its own QueryClientProvider around <FluentWidget>, the content your
renderPage or renderHome returns is rendered under your client again, so a query your App
prefetched or hydrated is read from your cache; the widget's own queries stay on its client.
You do not need a Buffer polyfill either. Importing @fluent.xyz/connect defines
globalThis.Buffer when the page has none, and leaves an existing one in place.
Styles
Import the widget's stylesheet once, in your App's entry module:
import "@fluent.xyz/connect/styles.css";
Network
config.network names the Network the widget runs on: "testnet" or "mainnet". It is
required and has no default, and the widget reads no environment variable for it. Rendering
<FluentWidget> with no network throws FluentWidgetConfigError with code
missing_option; a value other than the two throws it with code invalid_option
(errors.md). The error is thrown during the render, so the
nearest error boundary catches it.
The Network is chosen once per deployment of the App, and choosing it is your App's own code.
Pass the literal each deployment runs on: for example one config module per deployment, each
with its own literal, or a build-time constant of your own that your code checks is exactly
"testnet" or "mainnet" before it reaches config. Do not map an unknown value to one of the
two: a production build with a misspelled value would then run on the wrong Network.
import type { FluentWidgetConfig } from "@fluent.xyz/connect";
// config.testnet.ts; the mainnet deployment's module passes network: "mainnet".
export const config: FluentWidgetConfig = {
appId: "app_0123456789abcdef0123456789abcdef",
privyClientId: "client-0123456789abcdef",
network: "testnet",
};
Keep config stable: a module constant, as here, or a memo. The widget resolves config again
on every render that passes a new object, and such a render can throw too.
Mount the widget
<FluentWidget> renders the connect button, the connect dialog and the account drawer, and
hands your App's content the widget's render context. mode picks which of your two render
functions it calls:
mode="home", the default, callsrenderHome.mode="page"callsrenderPage.
Each receives the render context, a FluentWidgetRenderContext. Any component rendered inside
that content can read the same object with useFluentWidget(), so you do not have to pass it
down. useFluentWidget() throws an Error when it is called outside the content of
<FluentWidget>, the button renderConnectButton returns included: that button receives its
own context, described below.
import "@fluent.xyz/connect/styles.css";
import { FluentWidget, useFluentWidget, type FluentWidgetConfig } from "@fluent.xyz/connect";
const config: FluentWidgetConfig = {
appId: "app_0123456789abcdef0123456789abcdef",
privyClientId: "client-0123456789abcdef",
network: "testnet",
};
function Dashboard() {
const { status, widget } = useFluentWidget();
if (status !== "connected") return <p>Connect to continue.</p>;
return <p>Your account: {widget.account.address}</p>;
}
export function Root() {
return <FluentWidget config={config} mode="page" renderPage={() => <Dashboard />} />;
}
The connect button
connectButton decides where the widget's own button goes:
"fixed", the default: a floating button at the top right of the page."inline": the same button without fixed positioning, for your own layout to place.false: no button; open the widget's screens yourself.
The button opens the connect dialog while no Widget account is connected, and toggles the account drawer once one is.
To draw your own button in that place, pass renderConnectButton. It receives a
FluentWidgetConnectButtonRenderContext: connected, pending (true while a Fluent ID's
account is being prepared), addressLabel (once connected, the shortened
address, or "Connected"), userAvatarUrl, userXDisplayName and userGoogleName (the same
values as the render context's, see Account name), onClick (what the default
button does on click),
openConnect, openAccount, and DefaultButton, which renders the widget's own button. When
renderConnectButton returns null or undefined, the widget falls back to the button
connectButton names.
import { FluentWidget, type FluentWidgetConfig } from "@fluent.xyz/connect";
const config: FluentWidgetConfig = {
appId: "app_0123456789abcdef0123456789abcdef",
privyClientId: "client-0123456789abcdef",
network: "testnet",
};
export function Root() {
return (
<FluentWidget
config={config}
renderConnectButton={({ connected, onClick, DefaultButton }) =>
connected ? (
<DefaultButton />
) : (
<button type="button" onClick={onClick}>
Connect
</button>
)
}
renderHome={() => <main>Your App</main>}
/>
);
}
With connectButton={false}, open the screens from your own content with the render context's
openConnect (the connect dialog) and openAccount (the account drawer). openConnect ignores
a click event passed to it, so it can be handed straight to onClick. The example after the
next section shows both.
Reading the account
The render context, and useFluentWidget(), tell your App who is connected:
status, aFluentWidgetStatus:"restoring"(a session from a previous visit may exist and nothing is decided yet),"connecting"(a Fluent ID's login is done and its account is being prepared, after a Direct login or on a restore),"connected"(a Widget account is connected) or"disconnected". Branch onstatus, not onhasConnectedAccountalone:hasConnectedAccountisfalseboth while a stored session is being restored and when nobody is connected, and treating the two alike shows a Connect button to a person who is already connected.hasConnectedAccount: whether a Widget account, a Fluent ID or an External wallet, is connected now.widget.account, aFluentWidgetAccount:address(the Fluent ID's address, or the External wallet's;undefinedwhile there is none),type("smart"for a Fluent ID whose account is ready,"eoa"for a connected External wallet,undefinedotherwise),executionReady(whether the account can send now),executionStatus("ready", or why it cannot send:"disconnected", which is also what a Fluent ID still being prepared, or whose account failed, reads;"unavailable"or"error"only for a connected External wallet whose client is not ready), andcapabilities(atomicBatchanderc20Gas, bothtrueonly for a Fluent ID). While no External wallet is connected,addressfalls back to the address of the stored Widget session, so it can be set whilestatusis not"connected": branch onstatusbefore you use it.session, aFluentWidgetSessionornull: the Widget session, which holds the Fluent ID'saccount(addressandtype) andissuedAt. It isnulluntil a Fluent ID connects, and an External wallet never sets it: read an External wallet fromwidget.account. A session stored on a previous visit is insessionfrom the first render, without any callback, and stays there until a disconnect through the widget or a new Direct login replaces it, even whilestatusis"disconnected"or only an External wallet is connected. The session is not a credential; to prove who the person is to your backend, use an Auth token (guides/backend-auth.md).
import { useFluentWidget } from "@fluent.xyz/connect";
export function AccountBar() {
const { status, widget, openConnect, openAccount } = useFluentWidget();
switch (status) {
case "connected":
return (
<button type="button" onClick={openAccount}>
{widget.account.address ?? "Connected"}
</button>
);
case "disconnected":
return (
<button type="button" onClick={openConnect}>
Connect
</button>
);
default:
// "restoring", "connecting", and any status a later release adds.
return <span>Loading…</span>;
}
}
Keep a default branch in any switch over status or another code or status union: a later
release may add a value.
A Fluent ID can sign without the widget's Review, under Quick sign: see concepts.md.
onSessionChange is called each time the widget writes its Widget session: with the new
FluentWidgetSession when a Fluent ID connects by Direct login, and with null on a disconnect
through the widget. It is not called for a session restored from storage when the widget mounts,
nor when an External wallet connects; read session and widget.account from the render
context for those. One exception: when a restored login has no matching stored session, the
widget writes a new one after it mounts and calls onSessionChange with it.
import {
FluentWidget,
type FluentWidgetConfig,
type FluentWidgetSession,
} from "@fluent.xyz/connect";
const config: FluentWidgetConfig = {
appId: "app_0123456789abcdef0123456789abcdef",
privyClientId: "client-0123456789abcdef",
network: "testnet",
};
function handleSessionChange(session: FluentWidgetSession | null) {
console.info(session ? `Connected: ${session.account.address}` : "Disconnected");
}
export function Root() {
return (
<FluentWidget
config={config}
onSessionChange={handleSessionChange}
renderHome={({ status }) => <p>Status: {status}</p>}
/>
);
}
Account name
The render context, useFluentWidget() and the context renderConnectButton receives carry the
names the person is known by: userXDisplayName, the display name of their linked X account (the
name X shows above the handle, not the handle), and userGoogleName, the name on the Google
account linked to their Direct login. Each is undefined when no such account is linked or it
reports no name, and while an External wallet is connected without a Fluent ID.
avatar.forceDefault hides the Account avatar's picture only and leaves both names in place.
Neither is a fallback for the other: a person with Google linked and no X account has no X
display name, so pick your own order.
import { useFluentWidget } from "@fluent.xyz/connect";
export function Greeting() {
const { userXDisplayName, userGoogleName } = useFluentWidget();
const name = userXDisplayName ?? userGoogleName;
return <span>{name ? `Hi, ${name}` : "Hi"}</span>;
}
Debugging
debugLogging prints the widget's connect, account and signing diagnostics to the browser
console, and in mode="home" also shows the widget's debug panel. It is off by default; turn it
on only while debugging an integration.
import { FluentWidget, type FluentWidgetConfig } from "@fluent.xyz/connect";
const config: FluentWidgetConfig = {
appId: "app_0123456789abcdef0123456789abcdef",
privyClientId: "client-0123456789abcdef",
network: "testnet",
};
export function Root() {
return <FluentWidget config={config} debugLogging renderHome={() => <main>Your App</main>} />;
}
When the widget refuses its config, the error's message names the option and how to fix it,
and its code is listed in errors.md.
Checklist
appIdandprivyClientIdfrom Fluent, and every page origin allow-listed for Direct login.- For
getAuthToken(), Sponsorship,linkX()and User settings: Auth tokens enabled for the App and each calling origin registered. - For Sponsorship: the App enabled for Sponsorship.
@fluent.xyz/connectand all five peers installed.@fluent.xyz/connect/styles.cssimported once.networkpassed as a literal per deployment, never defaulted.configkept stable: a module constant or a memo.useFluentWidget()called only inside the contentrenderPageorrenderHomereturns.- UI branched on
status, with adefaultbranch. debugLoggingoff in production.
Next: send a Batch operation (guides/transactions.md), request a signature (guides/signing.md), or verify the person on your backend (guides/backend-auth.md).