← All @molecule/* packages · App templates
@molecule/app-settings-panel-reactFeature · settings-panel · App (browser) · v1.0.1 · Apache-2.0
Composable settings panel — a SettingsContainer + family of section sub-components (Account, Auth, Notifications, Billing, Devices, ThisDevice, LogOutDelete). Apps compose via JSX children, picking which sections to include and what order.
npm install @molecule/app-settings-panel-react@molecule/app-settings-panel-react is a ready-made settings-panel feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.
import {
AccountSection,
AppearanceSection,
AuthSection,
BillingSection,
DevicesSection,
LogOutDeleteSection,
NotificationsSection,
SettingsContainer,
ThisDeviceSection,
} from '@molecule/app-settings-panel-react'
function SettingsPanel({ onClose }: { onClose: () => void }) {
return (
<SettingsContainer onClose={onClose}>
<AccountSection />
<AppearanceSection />
<AuthSection />
<NotificationsSection />
<BillingSection />
<DevicesSection />
<ThisDeviceSection />
<LogOutDeleteSection />
</SettingsContainer>
)
}Works with: @molecule/app-auth, @molecule/app-react, @molecule/app-ui, @molecule/app-ui-react
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
@molecule/app-settings-panel-react — composable, batteries-included
settings panel.
<SettingsContainer onClose={…}> owns the layout and publishes onClose
via context; each section component is independent and loads its own data
through hooks:
<AccountSection> — name/email edit (PATCH /api/users/:id).<AppearanceSection> — dark-mode toggle (useTheme()).<AuthSection> — password change + TOTP two-factor
(POST /api/users/:id/verify-two-factor).<NotificationsSection> — web-push toggle (see the exported
enablePushOnCurrentDevice / disablePushOnCurrentDevice helpers and
their documented device-row contract).<BillingSection> — read-only plan display (GET /api/billing/status),
or <TiersUpgradeSection> — full Stripe upgrade/cancel flow
(/api/billing/tiers|checkout|cancel). Use one or the other, not both.<DevicesSection> / <ThisDeviceSection> — device list + current
device (GET/DELETE /api/devices).<LogOutDeleteSection> — sign out + delete account
(DELETE /api/users/:id).
Apps compose via JSX children — pick sections, order them, and interleave
custom sections.import {
AccountSection,
AppearanceSection,
AuthSection,
BillingSection,
DevicesSection,
LogOutDeleteSection,
NotificationsSection,
SettingsContainer,
ThisDeviceSection,
} from '@molecule/app-settings-panel-react'
function SettingsPanel({ onClose }: { onClose: () => void }) {
return (
<SettingsContainer onClose={onClose}>
<AccountSection />
<AppearanceSection />
<AuthSection />
<NotificationsSection />
<BillingSection />
<DevicesSection />
<ThisDeviceSection />
<LogOutDeleteSection />
</SettingsContainer>
)
}
feature
npm install @molecule/app-settings-panel-react @molecule/app-auth @molecule/app-react @molecule/app-ui @molecule/app-ui-react react react-router
npm install -D @types/react
DeviceA user's registered device (subset rendered in the settings list).
interface Device {
id: string
name: string
platform: string
lastSeen?: string
/** `true` for the device making the current request (the API marks it). */
isCurrent?: boolean
}
PushToggleDeviceDevice row slice returned by GET /api/devices.
interface PushToggleDevice {
id: string
isCurrent?: boolean
hasPushSubscription?: boolean
}
PushToggleHttpMinimal structural slice of @molecule/app-http's HttpClient used here.
interface PushToggleHttp {
get<T = unknown>(url: string): Promise<{ data: T }>
patch<T = unknown>(url: string, data?: unknown): Promise<{ data: T }>
}
PushToggleTokenPush token slice returned by @molecule/app-push register().
interface PushToggleToken {
value: string
platform: 'web' | 'ios' | 'android'
}
SettingsPanelContextValueContext published by <SettingsContainer> to its children.
The container owns the dismiss-the-panel handler; sub-components that need to close after an action (logout, delete account, email error) read it from context rather than threading the prop down.
interface SettingsPanelContextValue {
onClose: () => void
}
PushToggleFailureReasonWhy an enable/disable attempt failed (mapped to i18n by the component).
type PushToggleFailureReason =
'permission-denied' | 'server-unconfigured' | 'register-failed' | 'persist-failed'
PushToggleResultResult of an enable/disable attempt.
type PushToggleResult =
{ ok: true } | { ok: false; reason: PushToggleFailureReason; message?: string }
AccountSection()Account section — edits the user's display name + email. Fetches
/users/me on mount to refresh the current user record (and write it
back to the auth cache so subsequent reloads see the latest data). On
blur of either field, PATCHes /api/users/:id with the changed field;
reverts + shows an inline error if the request fails.
The user resource's update handler accepts name, username, and
email; this surfaces name + email (the universally-present
profile fields). Apps that use a public username handle can extend
this with a username field the same way.
function AccountSection(): JSX.Element
AppearanceSection()Appearance section — dark-mode toggle wired to the theme bond.
Apps that expose other appearance knobs (font size, density, etc.)
can either add more sub-components to this package or render their
own section in line with <AppearanceSection> via children.
function AppearanceSection(): JSX.Element
AuthSection()Authentication section — change password (modal) + two-factor (TOTP) setup.
Two-factor uses the real enrollment flow against the user resource's
POST /users/:id/verify-two-factor endpoint (@molecule/api-two-factor):
{action:'setup'} returns a QR code + secret to scan into an
authenticator app → user enters the 6-digit code → {action:'enable', token}.{action:'disable', token}.
The current status is read from /users/me. (This replaces the previous
boolean toggle, which PATCHed twoFactorEnabled — a field the update
handler deliberately ignores — so it never actually enrolled 2FA.)Auto-hides for OAuth-only users (user.oauthServer truthy) since
password / 2FA wouldn't apply. If the app's API has no two-factor
provider bonded, setup fails gracefully with an inline message.
function AuthSection(): JSX.Element | null
BillingSection(props?)Billing section — current plan + Upgrade button.
Fetches /api/billing/status on mount and uses the returned plan
name as a more accurate label than the parent-supplied default.
The plan prop remains a fallback (e.g. for offline rendering or
apps that don't expose /billing/status).
The Upgrade button navigates to upgradeTo (default /settings).
Pass upgradeTo="/billing" or upgradeTo="/pricing" if your app
routes elsewhere. Apps with a multi-tier checkout flow should use
<TiersUpgradeSection> instead.
function BillingSection({
plan = 'Free',
upgradeTo = '/settings',
}?: {
plan?: string
upgradeTo?: string
}): JSX.Element
DevicesSection(props?)Devices section — lists the user's registered devices and lets them revoke (sign out) any device other than the one they're currently using.
Revoking deletes the device row (DELETE /api/devices/:id). The API's
authorization layer enforces server-side revocation: it rejects that
device's session the next time it makes a request (within the
device-exists cache TTL), so the removed device is actually signed out —
not just hidden from the list. The current device is labelled
"This device" and is not revocable here (use Sign out for the current
session). Recreates molecule v1's device-revocation behaviour.
Apps that want a different trailing element per row can pass a
renderRowIcon callback (which then replaces the built-in revoke
control); pass () => null to suppress it entirely.
function DevicesSection({
renderRowIcon,
}?: {
renderRowIcon?: (device: Device) => ReactNode
}): JSX.Element
disablePushOnCurrentDevice(deps)Disables push: unsubscribes the browser (best-effort — a dev build without a service worker has nothing to unsubscribe) and ALWAYS clears the server state so no further pushes target this device.
function disablePushOnCurrentDevice(deps: {
http: PushToggleHttp
unregister: () => Promise<void>
}): Promise<PushToggleResult>
deps — The http client + push action from usePush().deps.http — Authenticated http client (useHttpClient()).deps.unregister — usePush().unregister.Returns: { ok: true } or a typed failure (server state not cleared).
enablePushOnCurrentDevice(deps)Runs the full enable chain: browser permission → runtime VAPID public key
(GET /api/devices/push/public-key) → register({ vapidPublicKey }) →
persist the subscription on the current device row.
Every failure is returned as a typed reason (never thrown) so the UI can show an honest, specific message instead of hanging or silently reverting.
function enablePushOnCurrentDevice(deps: {
http: PushToggleHttp
requestPermission: () => Promise<string>
register: (options?: { vapidPublicKey?: string }) => Promise<PushToggleToken>
}): Promise<PushToggleResult>
deps — The http client + push actions from usePush().deps.http — Authenticated http client (useHttpClient()).deps.requestPermission — usePush().requestPermission.deps.register — usePush().register.Returns: { ok: true } or a typed failure.
findCurrentDevice(devices)Picks the caller's own device row: the api flags the session device
isCurrent; fall back to the first row for sessions predating the flag.
function findCurrentDevice(devices: PushToggleDevice[] | undefined): PushToggleDevice | undefined
devices — Rows from GET /api/devices.Returns: The current device row, or undefined when the user has none.
LogOutDeleteSection()Bottom actions section — Log out + Delete account.
Owns the delete-account modal internally (the trigger lives in the
same section so the modal lives here too). Reads onClose from
the <SettingsContainer> context to dismiss the panel after either
action and navigates to /login.
function LogOutDeleteSection(): JSX.Element
NotificationsSection()Push-notification toggle section — the full receive-side enable chain:
browser permission → runtime VAPID public key
(GET /api/devices/push/public-key) → pushManager.subscribe with
applicationServerKey → PATCH the subscription onto the current device
row (/api/devices/:id { pushSubscription, hasPushSubscription }), which
is where the api-side push fan-outs look for it.
Initial state reflects SERVER truth (the current device row's
hasPushSubscription), and every failure surfaces as an honest inline
message — a dev build without a service worker fails fast instead of
hanging the switch.
function NotificationsSection(): JSX.Element
readCurrentDevicePushEnabled(http)Reads whether push is currently enabled for THIS device (server truth:
the current device row's hasPushSubscription).
function readCurrentDevicePushEnabled(http: PushToggleHttp): Promise<boolean>
http — Authenticated http client (useHttpClient()).Returns: true when the current device has a stored subscription.
SettingsContainer(props)Outer settings-panel layout: padded vertical stack that hosts the
section sub-components. Publishes onClose to descendants via
context so <LogOutDeleteSection> etc. can dismiss the panel
after an action without explicit prop threading.
function SettingsContainer({
onClose,
children,
}: {
onClose: () => void
children: ReactNode
}): ReactElement<unknown, string | JSXElementConstructor<any>>
subscriptionFromToken(token)Converts an @molecule/app-push token into the device resource's
pushSubscription shape (web PushSubscription JSON, FCM registration for
Android, APNs registration for iOS).
function subscriptionFromToken(token: PushToggleToken): unknown
token — The push token returned by register().Returns: The pushSubscription value to PATCH onto the device row.
ThisDeviceSection()"This device" detail strip — OS, browser, online/offline.
Reads from @molecule/app-device (useDevice hook) + browser
navigator.onLine. No side effects, no app-specific state.
function ThisDeviceSection(): JSX.Element
TiersUpgradeSection()Billing section with full multi-tier upgrade flow.
Replaces the simpler <BillingSection> for apps backed by
@molecule/api-payments-stripe + @molecule/api-resource-payment
— loads the user's current plan from /api/billing/status,
loads available tiers from /api/billing/tiers, and renders an
Upgrade modal with a Subscribe button per tier price. Subscribe
POSTs to /api/billing/checkout and redirects to the Stripe
checkout URL; paid users see a Cancel button that POSTs to
/api/billing/cancel.
function TiersUpgradeSection(): JSX.Element
useSettingsPanelContext()Reads the onClose handler exposed by the parent <SettingsContainer>.
Throws if used outside the container so component misuse is loud.
function useSettingsPanelContext(): SettingsPanelContextValue
SettingsPanelContextReact context object for the settings panel; consume via useSettingsPanelContext.
const SettingsPanelContext: Context<SettingsPanelContextValue | null>
Peer dependencies:
@molecule/app-auth ^1.0.1@molecule/app-react ^1.0.1@molecule/app-ui ^1.0.1@molecule/app-ui-react ^1.0.1react ^18.0.0 || ^19.0.0react-router ^7.0.0 || ^8.0.0@molecule/app-auth
@molecule/app-react
@molecule/app-ui
@molecule/app-ui-react
react
react-router
Wiring prereqs: sections need the standard @molecule/app-react
provider stack — <I18nProvider>, <HttpProvider> (authenticated
client), <AuthProvider>, <ThemeProvider> (AppearanceSection), push +
device providers (NotificationsSection/ThisDeviceSection) — plus a
bonded ClassMap. <BillingSection> and <LogOutDeleteSection> also
call react-router's useNavigate() and throw outside a <Router>.
Section components throw if rendered outside <SettingsContainer>
(they read its context for onClose).
Server contract: the molecule API surface from
@molecule/api-resource-user, @molecule/api-resource-device,
@molecule/api-two-factor, and the billing endpoints
(/api/billing/*) wired by the payments stack. Missing read endpoints
degrade gracefully (sections render empty); the mutating actions do not.
Translations: @molecule/app-locales-settings-panel companion bond.
Translation strings are provided by @molecule/app-locales-settings-panel.