{"id":8276,"plugin_id":"plugin_asdk_app_6a183f5bead08191b494b99bc881e8c0","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T22:52:24.296Z","digest":"0beae4f2107b676a3c143c70b250bf666c444643f2a36553b2c7b9ef23de3dd5","against":null,"payload":{"name":"descope-byos-builder","description":"Use when building React \"Bring Your Own Screen\" (BYOS) custom UI on top of a Descope flow — takes exported flow JSONs, extracts the real interaction IDs and outputs, generates BYOS components that match hosted parity, and avoids the rediscovery-the-hard-way failure modes (silent form rejection, shared screen-name collisions, anonymous-session stickiness, nested-form hydration errors, wrong form keys, dead-end buttons, missing OAuth provider field).","included_files":[{"relative_path":"references/byos-component-patterns.md","size_in_bytes":8963},{"relative_path":"references/gotchas.md","size_in_bytes":17995}],"skill_md_contents":"---\nname: descope-byos-builder\ndescription: Use when building React \"Bring Your Own Screen\" (BYOS) custom UI on top of a Descope flow — takes exported flow JSONs, extracts the real interaction IDs and outputs, generates BYOS components that match hosted parity, and avoids the rediscovery-the-hard-way failure modes (silent form rejection, shared screen-name collisions, anonymous-session stickiness, nested-form hydration errors, wrong form keys, dead-end buttons, missing OAuth provider field).\n---\n\n# Descope BYOS Builder\n\nTranslate **Descope flow JSON exports** into working React BYOS screens that call `state.next(interactionId, form)`. The failures below are recorded from real BYOS sessions — every one cost 15–60 minutes the first time.\n\n## When to Use\n\n- Building custom UI over a Descope flow while keeping Descope's flow engine (no client-side JWT parsing, full flow logic intact)\n- Modifying an existing BYOS implementation after the underlying flow changed in the Descope console\n- Debugging \"flow completes but session stays wrong\" / \"button does nothing\" / \"session is anonymous\" / \"passkey ceremony aborts\" symptoms\n- Auditing whether an existing BYOS matches hosted-screen parity\n- Adding post-auth promotion subflows (e.g. `add-passkeys`) that run after `logged-in`\n\n**Don't use for:** flows that fully work with the hosted `<Descope flowId=... />` widget (BYOS is a tradeoff — you give up flow edits propagating without code changes).\n\n## The Iron Rule\n\n**Ground every BYOS component in the exported flow JSON.** Do not guess interaction IDs, output key names, or screen names. Every failure in the catalog starts with someone making up a value that looked reasonable.\n\n## Inputs Required (Ask the User First)\n\nThis skill cannot fetch flows from the Descope console — the agent has no console access. **Before doing any work, ask the user to provide:**\n\n1. **Flow JSON files** — exported from Descope console for the main flow AND every subflow it invokes (LoadSubflow `arguments.flowId`), including post-auth promotion subflows (e.g. `add-passkeys`). Accept either file paths or pasted JSON.\n2. **Project ID** + **base URL** (if not already configured) — needed for the `AuthProvider` / SDK init.\n3. **Mount point** in the React app — file/component where the BYOS entry point should render.\n4. **Existing BYOS code** (if modifying) — paths to current screen components and screen map.\n\nIf the user only provides the main flow JSON, **stop and ask for subflows** before generating components — missing subflow JSONs is the #1 cause of `[byos] no handler for screen \"...\"` runtime errors.\n\n## Workflow\n\n1. **Collect flow JSONs from the user.** Agent cannot reach the Descope console — user must export and paste/path them in. Need the **main** flow AND every subflow it invokes (LoadSubflow actions with `arguments.flowId`). This includes **post-auth promotion subflows** that run AFTER a `logged-in` action (e.g. `add-passkeys`) — easy to miss because the user is already authenticated by then. If only the main flow was provided, **scan it for `LoadSubflow` actions and ask the user to export each one** before proceeding. Missing subflow JSONs → undiscovered screens → `[byos] no handler for screen …` at runtime.\n\n2. **Parse with `parse-flow.mjs`** (in this directory). Run `node parse-flow.mjs <path/to/flow.json>` — it prints every screen task with `screenName`, `allInputKeys`, `contextKeys`, next-rules (interactionId → taskId), UI node summaries (input `name` attrs, button labels), and subflow invocations.\n\n3. **Build the screen-name map.** One React component per **unique screen name across all flows**. Watch for collisions — multiple tasks often share the name (e.g., \"Welcome Screen\" used for email-entry AND password-entry). When collisions exist, write a **router component** that dispatches based on `state.context.form.*` heuristics (see Gotchas → Screen name collisions).\n\n4. **Write each component.** Every BYOS screen does the same four things:\n   - Read context for display: `state.context.sentTo.maskedEmail`, `state.context.form.email`, etc.\n   - Collect inputs into `form` (via `setForm({ ...form, key: value })`)\n   - Fire `state.next(interactionId, payload)` on button click\n   - Render `state.error?.text` when Descope reports errors\n\n5. **Wire the flow.** The Descope React SDK provides `<Descope flowId=\"...\" onScreenUpdate={handler} />` from `@descope/react-sdk` as the BYOS entry point — `onScreenUpdate` receives `(screenName, state, next)` and you dispatch to the matching screen component. A common pattern is to build a thin `FlowOrByos` wrapper that takes a `byosScreens` map and dispatches internally, but `FlowOrByos` is not an SDK export — you build it. Mount one instance at the entry point. On `onSuccess`, invalidate any auth-state caches (see Gotchas → Session stickiness).\n\n6. **Verify end-to-end.** Walk every user journey the flow supports. Any screen that hits `[byos] no handler for screen \"...\"` in the console is missing from your map.\n\n## Critical Rules\n\n> **Note:** Several rules below (nested `<form>` tag behavior, WebAuthn ceremony ownership, `ctxKey` prefill, `componentsConditions` field name, E.164 silent rejection) are empirically derived from real BYOS sessions and are not explicitly documented in Descope's official docs — but have been validated in production and verified against flow JSON structure.\n\n- **Form keys match the input node's `name` prop**, NOT `allInputKeys` or `inputsMetadata.key`. Task 40's Set Password input has `name=\"newPassword\"` but `allInputKeys: [\"newPassword_noPolicyOverrides\"]`. Submit with `{ newPassword: \"...\" }`.\n- **Send only the current screen's outputs** in `state.next` payloads. Spreading the full accumulated form across subflow boundaries can pollute context and cause silent failures.\n- **Never nest `<form>` tags.** Descope's web component wraps children in a `<form>`. Use `<div>` + `onClick` + `onKeyDown={makeEnterHandler(submit)}`.\n- **OAuth buttons must set `provider`** in the payload (`{ provider: 'google' }`), not only the interaction ID.\n- **Phone numbers need E.164 format.** Non-E.164 gets silently rejected by the SMS connector — the flow completes but no session.\n- **Invalidate auth caches on `onSuccess`.** Session upgrades (anonymous → verified) can reuse the same `sub`, so userId-keyed caches never auto-refresh.\n- **Read `componentsConditions`** from screen JSON to mirror hide/show rules (e.g., hide \"Sign in with code\" when `unauthUser.verifiedPhone` is false).\n- **WebAuthn flows: SDK runs the ceremony.** When an interaction routes to a webauthn action task (`webauthn-update-user-start/finish`, `Sign Up or In / Passkeys`), just fire `state.next(interactionId)`. **Never** call `navigator.credentials.create/get` from BYOS. **Never** import `@simplewebauthn/browser`, `startAuthentication`, `startRegistration`, or any WebAuthn helper library — the Descope SDK already does both ceremony halves. SDK observes the action on `onScreenUpdate` and runs the ceremony itself; cancel/error surfaces as `state.error.text`.\n- **Read input `ctxKey` for prefill.** When an input node has `props.ctxKey=\"someKey\"`, seed `form[name]` from `state.context[someKey]` on first render via `useEffect` keyed on the context value (only set if local field is empty).\n- **Mirror `Device Not Supported` branches.** WebAuthn flows commonly branch on `deviceInfo.webAuthnSupport`. The unsupported branch is a real screen task — give it a BYOS component, otherwise hosted widget renders mid-promotion.\n\n## Heuristics for Shared Screen Names\n\nWhen two tasks share a screen name, you **cannot guess** a disambiguation signal. You must trace each path and build a \"form field accumulation\" table.\n\n**The process (non-negotiable — skipping this produces silent bugs):**\n\n1. For each task that uses the colliding screen name, trace backwards through `next.rules` until you hit the parent flow's entry point.\n2. For each path, list every screen task along the way and its `allInputKeys` — those are the form fields the user writes into on that path.\n3. Find a form field that is populated on exactly one path.\n4. Use `Boolean(state?.context?.form?.<that field>)` as the heuristic.\n\n**Common mistake:** picking a field that's populated on **both** paths because you didn't trace both paths fully.\n\n> **Examples below are from one specific flow set — your task IDs, screen names, and subflow names will differ. They illustrate the method; always run `parse-flow.mjs` on your own flows.**\n>\n> Example: for \"Verify OTP\" shared between `sign-in-sms-otp` and `progressive-profile-sms`, `form.phone` looks attractive — but BOTH flows collect phone in a Phone-input screen before reaching Verify OTP. Pick `form.password` instead: the sign-in-sms-otp path is entered from the parent flow's password screen which writes `form.password`; the progressive-profile-sms path is entered post-magic-link where no password was ever typed.\n\n**Reference heuristics (example from the flow set this skill was born from — not universal):**\n\n| Collision | Path A collects before this screen | Path B collects before this screen | Signal |\n|-----------|------------------------------------|------------------------------------|--------|\n| \"Welcome Screen\" (email-entry vs password-entry) | — | `email` | `state.context.form.email` |\n| \"Verify OTP\" (sign-in-sms-otp vs progressive-profile-sms) | `email`, `password`, `phone` | `email`, `phone` | `state.context.form.password` (NOT `phone` — both collect it!) |\n\nThree-way collision example (Magic Link Sent across main and two subflow variants):\n\n| Path | Collects before this screen | |\n|------|----------------------------|---|\n| Main flow | `email` | no password |\n| Subflow new-user | `email`, `password`, `fullName` | password + fullName |\n| Subflow existing-user | `email`, `password` | password, no fullName |\n\nPredicate: `supportsChooseOther = !hasPassword || hasFullName`.\n\nDocument the chosen heuristic at the top of the router component with the full trace. If the Descope console could rename one screen, ask — unique names beat heuristics forever.\n\n**Real success case:** Two `User Information` tasks (verify-email-magic-link existing-user vs new-user branches) were renamed in console to `User Information - Unverified - Email Only` and `User Information - Unverified - Email and Name`. Heuristic was possible (presence of `fullName` output); rename was cheaper, clearer, and survives flow edits. **Default toward rename.**\n\n## Gotchas Reference\n\nFull failure catalog (problem → symptom → fix): **see `references/gotchas.md`**. Before blaming BYOS code for a silent failure, scan that file — most \"doesn't work\" symptoms match a known gotcha.\n\n## Verification\n\nEnd-to-end test each user journey the flow supports:\n\n- Flow JSON says the main flow starts at `task N` and ends at `task M` with `action=logged-in`?\n- Every screen task in the flow has either a BYOS component OR a conscious \"fallback to hosted\" decision?\n- `onSuccess` fires and the app reaches the expected authenticated state (not stuck anonymous)?\n- `onError` fires and surfaces the Descope error message (not silent)?\n- Post-auth promotion subflows (passkey promotion, etc.) walked end-to-end including the `Device Not Supported` branch?\n- WebAuthn screens fire `state.next(interaction)` only — no `navigator.credentials.*` calls in BYOS code?\n\nConsole should show **no** `[byos] no handler for screen \"...\"` warnings during a full journey walk.\n\n## Parser\n\n`parse-flow.mjs` (this dir) — Node script. Input: flow JSON path(s). Output: a summary table per flow showing screen tasks, their inputs, their exit interactions, their UI node names, and subflow loaders. Run it, paste the output into your screen-map comment header, and you have the source of truth for every constant the BYOS components need.\n\n## Red Flags\n\nIf you find yourself:\n\n- Hardcoding an interaction ID you didn't see in the flow JSON — **stop, parse first**\n- Spreading `{ ...form }` into `state.next` across a subflow boundary — **send only the outputs**\n- Adding the same button to every \"Verify OTP\" variant without checking each flow's rules — **gate by context**\n- Writing a `<form>` tag inside your BYOS component — **use `<div>`**\n- Calling `navigate()` in `onSuccess` without invalidating auth caches — **call invalidate first**\n- Calling `navigator.credentials.create/get` from a BYOS click handler — **the SDK does it; just fire the interaction**\n- Importing `@simplewebauthn/browser` or any WebAuthn helper — **same trap, third-party-lib variant; SDK does it**\n- Skipping subflow export because \"the user is already logged in by then\" — **post-auth promotion subflows render real screens**\n\nAll of these mean: pause, re-read `references/gotchas.md`, verify against the flow JSON.\n\n## References\n\n- `references/byos-component-patterns.md` — positive code patterns: core wiring (`onScreenUpdate` → `ByosState`), screen router, screen skeleton, and complete examples for email, OTP, phone, OAuth, collision router, and ctxKey prefill. Start here when bootstrapping.\n- `references/gotchas.md` — failure catalog: 19 real BYOS failure modes, each with symptom → root cause → fix. Also contains a pre-ship checklist.\n- `parse-flow.mjs` — Node parser: `node parse-flow.mjs <flow.json> [more.json ...]`. Prints screen tasks, real form-key `name` props, interaction IDs, subflow loaders, and collision warnings. Run before writing any component.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}