@molecule/app-settings-panel-react
v1.0.5
Published
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.
Readme
@molecule/app-settings-panel-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 exportedenablePushOnCurrentDevice/disablePushOnCurrentDevicehelpers 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.
Quick Start
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>
)
}Type
feature
Installation
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/reactAPI
Interfaces
Device
A 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
}PushToggleDevice
Device row slice returned by GET /api/devices.
interface PushToggleDevice {
id: string
isCurrent?: boolean
hasPushSubscription?: boolean
}PushToggleHttp
Minimal 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 }>
}PushToggleToken
Push token slice returned by @molecule/app-push register().
interface PushToggleToken {
value: string
platform: 'web' | 'ios' | 'android'
}SettingsPanelContextValue
Context 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
}Types
PushToggleFailureReason
Why an enable/disable attempt failed (mapped to i18n by the component).
type PushToggleFailureReason =
'permission-denied' | 'server-unconfigured' | 'register-failed' | 'persist-failed'PushToggleResult
Result of an enable/disable attempt.
type PushToggleResult =
{ ok: true } | { ok: false; reason: PushToggleFailureReason; message?: string }Functions
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.ElementAppearanceSection()
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.ElementAuthSection()
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):
- Enable →
{action:'setup'}returns a QR code + secret to scan into an authenticator app → user enters the 6-digit code →{action:'enable', token}. - Disable → user enters a current code →
{action:'disable', token}. The current status is read from/users/me. (This replaces the previous boolean toggle, which PATCHedtwoFactorEnabled— 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 | nullBillingSection(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.ElementDevicesSection(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.ElementdisablePushOnCurrentDevice(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 fromusePush().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 fromusePush().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 | undefineddevices— Rows fromGET /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.ElementNotificationsSection()
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.ElementreadCurrentDevicePushEnabled(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): unknowntoken— The push token returned byregister().
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.ElementTiersUpgradeSection()
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.ElementuseSettingsPanelContext()
Reads the onClose handler exposed by the parent <SettingsContainer>.
Throws if used outside the container so component misuse is loud.
function useSettingsPanelContext(): SettingsPanelContextValueConstants
SettingsPanelContext
React context object for the settings panel; consume via useSettingsPanelContext.
const SettingsPanelContext: Context<SettingsPanelContextValue | null>Injection Notes
Requirements
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
Runtime Dependencies
@molecule/app-auth@molecule/app-react@molecule/app-ui@molecule/app-ui-reactreactreact-routerWiring prereqs: sections need the standard
@molecule/app-reactprovider stack —<I18nProvider>,<HttpProvider>(authenticated client),<AuthProvider>,<ThemeProvider>(AppearanceSection), push + device providers (NotificationsSection/ThisDeviceSection) — plus a bonded ClassMap.<BillingSection>and<LogOutDeleteSection>also call react-router'suseNavigate()and throw outside a<Router>.Section components throw if rendered outside
<SettingsContainer>(they read its context foronClose).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-panelcompanion bond.
Translations
Translation strings are provided by @molecule/app-locales-settings-panel.
