Bring Your Own Screen

When using Flows, you can use Descope's Bring Your Own Screen (BYOS) functionality if you want to utilize your own custom screens. This allows you to maintain complete control over the UI while still leveraging the logic of Descope flows.

Warning

Using custom screens (Bring Your Own Screens) means giving up the flexibility of Descope flows — including the ability to update authentication logic and UI without redeploying code. We only recommend BYOS in exceptional cases, such as strict compliance needs or deeply embedded native experiences.

Overview

Bring Your Own Screen works by:

  1. Using the Descope web component
  2. Implementing the onScreenUpdate callback to handle screen transitions
  3. Rendering your custom components based on the current screen name
  4. Managing the required inputs and outputs of each screen

Critical Concepts

When implementing BYOS, the most important aspects to get right are:

  1. Interaction IDs

    • Each next step in your flow has a unique Interaction ID, often tied to a button or a state change
    • These IDs are required to proceed to the next step in the flow
  2. Inputs and Outputs

    • Each screen expects specific inputs from the previous screen
    • Each screen produces specific outputs for the next screen
    • These must match exactly what the flow expects
  3. Screen Names

    • Each screen has a name found and modified from the screen editor
    • Each screen in your flow must have a unique name

To find the Interaction IDs, Inputs, and Outputs for each screen, you can expand the screen details within the flow builder.

Screen Detail Expander

The Interaction IDs can be seen on the screen widget, and the expected inputs and outputs of the screen can be seen in the expanded details on the right.

Screen Detail Expander

Each step has an automatically generated unique Interaction ID. You can choose to modify it from the screen editor itself.

Warning

It is very important to rename your Interaction IDs to something unique and easy to reuse. This will allow you to more easily make changes to your flow without having to redeploy your code every time.

Modify Interaction ID

The screen name can also be seen and modified from the screen editor.

Warning

It is extremely important to rename your screen names so that within a flow they are all unique. If your screen names are not unique, you will run into conflicts, and be unable to properly run your flow.

Modify Screen Name

Implementation Walkthrough

We'll use a simple example to illustrate how this works. Instead of showing the original "Welcome Screen" from our "sign-up-or-in" flow, we will show a custom "Welcome Screen" component:

import { AuthProvider, Descope } from "@descope/react-sdk/flows";
import { useState } from 'react';
import CustomWelcomeScreen from './CustomWelcomeScreen';
 
export default function App() {
  const [state, setState] = useState({ error: {} });
  const [form, setForm] = useState({});
 
  return (
    <Descope
      flowId="sign-up-or-in"
      onScreenUpdate={((screenName, state, next) => {
        // Handle screen updates and state management
        setState(prevState => ({ ...prevState, ...state, next, screenName }));
        // Return true to use custom screen, false to use Descope's screen
        return screenName === 'Welcome Screen';
      })}
      onSuccess={() => {
        console.log('success')
        setState(prevState => ({ ...prevState }))
      }}
    >
      {state?.screenName === 'Welcome Screen' &&
        <CustomWelcomeScreen
          onFormUpdate={setForm}
          onClick={async () => {
            // Call the "next" function with the next step's Interaction ID and required inputs
            await state.next('<interaction-id>', { ...form })
          }}
          errorText={state?.error?.text}
        />}
    </Descope>
  );
}

Key Components

These are the key components to the example above:

  1. Descope Component:

    • flowId: Specifies which flow to use
    • onScreenUpdate: Callback that determines when to use custom screens
    • onSuccess: Handles successful authentication
  2. Custom Screen Components:

    • Can be any custom component
    • Receive props to:
      • Update and use form values
      • Handle user actions
      • Display errors
    • Contain event handlers to define when to proceed to the next step in the flow
  3. State Management:

    • state: Contains the flow state, including the screen name, errors, and the next function
    • form: Manages form data
    • next: Function to proceed to the next screen

Within the Descope component, we check if the current state's "Screen Name" matches the name of a screen we want to replace. If it does, we show our custom screen component instead of the default one. The custom screen component must implement all required inputs and outputs, as shown below.

Custom Screen Implementation

Here's an example of a custom screen component:

function CustomWelcomeScreen({ onFormUpdate, onClick, errorText }) {
  return (
    <div className="custom-screen">
      <input
        type="email"
        onChange={(e) => onFormUpdate({ email: e.target.value })}
      />
      <button onClick={onClick}>Continue</button>
      {errorText && <div className="error">{errorText}</div>}
    </div>
  );
}

The form object maintains state throughout your flow, collecting and passing data between screens and actions. Each screen receives the current form values and can update them using the onFormUpdate function.

In this example, any change in the email input box on the custom screen updates the form object:

<input
  type="email"
  onChange={(e) => onFormUpdate({ email: e.target.value })}
/>

Note

Each screen must update the form with all values listed in the "Outputs" section of that screen in the Descope flow builder. These outputs are required for the next steps in your flow to function correctly.

Screen outputs in flow builder

When the user clicks on the "Continue" button in the custom screen, the onClick handler is triggered.

<button onClick={onClick}>Continue</button>

The onClick handler calls the state.next() function, passing in the interaction-id, in this case "sign-up-or-in", corresponding to that button in the flow. We also pass in the form object, so that the email and any other updated values are available in the next step of the flow.

onClick={async () => {
  if (state.next) {
    // Call the "next" function with the next step's Interaction ID and required form values
    await state.next('sign-up-or-in', form)
  }
}}

Error Handling

When implementing custom screens, you can access error information through the state object passed to your component. The state object contains an error property that provides details about any errors that occur during the flow execution.

The error object contains the following fields:

FieldDescriptionExample
codeA unique error code that identifies the specific error type"E011003"
textA high-level error message that describes the general error category"Failed to sign up or in"
descriptionA more detailed explanation of what went wrong"Request is invalid"
messageThe specific reason for the error"The loginId field is required"

You can handle errors by either showing all errors in your screen, like in the example screen above, or by handling errors with more granularity.

For example, if an OTP sent to the user has expired, within your custom screen you can call a function to handle the resend of the OTP:

// Resend the OTP when the OTP has expired
useEffect(() => {
  if (state?.error?.code === "E061104") {
    handleResend();
  }
}, [state?.error?.code]);
 
// Handle resend click
const handleResend = () => {
  if (canResend) {
    setIsResending(true)
    onResendClick()
  }
};

Within the flow component, we handle the onResendClick() event by proceeding to the step where the resend Interaction Id points

{state?.screenName === verifyScreenName &&
  <OtpVerification
    // ... other flow inputs & event handlers
    onResendClick={async () => {
      if (state.next) {
        await state.next('resend', form)
      }
    }}
    state={state}
  />
}

For a full list of common errors that are included in the error object, see our Common Errors doc.

Note

Input validation must be handled within your custom screen component. The error object passed through the state only contains flow-level errors and does not include input validation errors. You'll need to implement your own validation logic for form inputs.

Best Practices

  • Keep your custom components focused and reusable
  • Handle errors appropriately using the provided error state
  • Maintain consistent styling with your application
  • Use the next function to pass all required data to the next screen
  • Test thoroughly to ensure proper flow progression

The most common issues when implementing BYOS are:

  • Using incorrect interaction IDs
  • Missing required inputs
  • Providing inputs in the wrong format
  • Not handling all required outputs

Always verify these in the Descope Console before implementing your custom screens.

Security Considerations

  • Never expose sensitive data in the UI
  • Validate all user inputs

Sample App

For a complete working example of Bring Your Own Screen, check out the BYOS Sample App.

Was this helpful?

On this page