← All @molecule/* packages · App templates

@molecule/app-settings-panel-react

Feature · 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

npm · Source on GitHub

How it works

@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

Reference

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.ts JSDoc, 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.

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/react

API

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.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):

  • 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 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.unregisterusePush().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.requestPermissionusePush().requestPermission.
  • deps.registerusePush().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

Constants

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.1
  • react ^18.0.0 || ^19.0.0
  • react-router ^7.0.0 || ^8.0.0

Runtime Dependencies

  • @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.

Translations

Translation strings are provided by @molecule/app-locales-settings-panel.