> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revyl.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Auth Bypass Deep Links

> Use launch variables and deep links to reach authenticated app states in test builds

Auth bypass deep links let Revyl start a test from a signed-in app state without automating every login screen.

The easiest implementation is:

1. Add a test-only deep link handler in your app.
2. Gate that handler with Revyl launch variables.
3. Open one deep link from `revyl device navigate` or a YAML `navigate` step.

<Warning>
  Only enable this in simulator, staging, debug, or explicit test builds. Do not ship a permanent auth bypass in production.
</Warning>

## Fastest Path For Expo Cloud Agents

Use this path when you already run an Expo development client through `revyl dev`.

```bash theme={null}
npx expo start --tunnel --dev-client

revyl dev --no-build --tunnel "<expo-dev-client-link>" \
  --launch-var REVYL_AUTH_BYPASS_ENABLED \
  --launch-var REVYL_AUTH_BYPASS_TOKEN

revyl device navigate \
  --url "myapp://revyl-auth?token=$REVYL_AUTH_BYPASS_TOKEN&role=buyer&redirect=%2Fcheckout"
```

Load the Expo dev client project first, then send the app-specific auth deep link. In managed Expo apps, JavaScript may not automatically see native launch values; use a small native config bridge, verify the token with a staging backend, or use a local demo fallback for sample apps only.

## Copy This Contract

Use one route shape across every framework:

```text theme={null}
myapp://revyl-auth?token=<token>&role=<role>&redirect=<allowlisted-route>
```

Create these launch variables:

```bash theme={null}
revyl global launch-var create REVYL_AUTH_BYPASS_ENABLED=true
revyl global launch-var create REVYL_AUTH_BYPASS_TOKEN=revyl-demo-token
```

Then start a device with the variables attached and open the bypass link:

```bash theme={null}
revyl device start \
  --platform ios \
  --launch-var REVYL_AUTH_BYPASS_ENABLED \
  --launch-var REVYL_AUTH_BYPASS_TOKEN

revyl device navigate \
  --url "myapp://revyl-auth?token=revyl-demo-token&role=buyer&redirect=%2Fcheckout"
```

Use the same URL in YAML tests:

```yaml theme={null}
test:
  metadata:
    name: "Auth bypass to checkout"
    platform: ios

  build:
    name: "Shopping App"

  env_vars:
    - REVYL_AUTH_BYPASS_ENABLED
    - REVYL_AUTH_BYPASS_TOKEN

  variables:
    auth_bypass_token: revyl-demo-token

  blocks:
    - type: manual
      step_type: navigate
      step_description: "myapp://revyl-auth?token={{auth_bypass_token}}&role=buyer&redirect=%2Fcheckout"

    - type: validation
      step_description: "The checkout screen is visible and the user is signed in"
```

## App Checklist

Your app owns four small pieces:

* Read `REVYL_AUTH_BYPASS_ENABLED` and `REVYL_AUTH_BYPASS_TOKEN` at app launch.
* Register a handler for `myapp://revyl-auth`.
* Verify the token, role, and redirect before changing app state.
* Create a test-only session and route only to an allowlisted destination.

Invalid links should fail visibly in test builds. Show a debug banner, account-screen status, toast, or log line that says why the link was rejected.

## Framework Recipes

Pick the recipe closest to your app. The session creation and route names are app-specific, but the Revyl contract stays the same.

<AccordionGroup>
  <Accordion title="Expo / Expo Router">
    Put the handler near your root layout so it catches initial links and links opened while the app is running.

    ```tsx theme={null}
    import { useEffect } from "react";
    import * as Linking from "expo-linking";
    import { router } from "expo-router";

    const allowedRedirects = new Map([
      ["/account", "/(tabs)/account"],
      ["/cart", "/cart"],
      ["/checkout", "/checkout"],
    ]);
    const allowedRoles = new Set(["buyer", "support"]);

    function launchValue(key: string) {
      return process.env[key];
    }

    function handleRevylAuthBypass(rawURL: string) {
      const url = new URL(rawURL);
      if (url.protocol !== "myapp:" || url.hostname !== "revyl-auth") return;

      const enabled = launchValue("REVYL_AUTH_BYPASS_ENABLED") === "true";
      const expectedToken = launchValue("REVYL_AUTH_BYPASS_TOKEN");
      const token = url.searchParams.get("token");
      const role = url.searchParams.get("role") || "buyer";
      const redirect = url.searchParams.get("redirect") || "/account";
      const route = allowedRedirects.get(redirect);

      if (!enabled) throw new Error("Revyl auth bypass is disabled");
      if (!expectedToken || token !== expectedToken) throw new Error("Bad Revyl auth bypass token");
      if (!allowedRoles.has(role)) throw new Error("Role is not allowlisted");
      if (!route) throw new Error("Redirect is not allowlisted");

      createTestSession({ role });
      router.replace(route);
    }

    export function useRevylAuthBypass() {
      useEffect(() => {
        Linking.getInitialURL().then(url => {
          if (url) handleRevylAuthBypass(url);
        });

        const subscription = Linking.addEventListener("url", event => {
          handleRevylAuthBypass(event.url);
        });

        return () => subscription.remove();
      }, []);
    }
    ```

    In managed Expo apps, JavaScript may not receive native launch values automatically. Use a small native config bridge, verify the token with a staging backend, or follow the Bug Bazaar sample's demo fallback for a local fixture only.

    For Expo Router apps, add a route backstop such as `app/revyl-auth.tsx` that calls the same handler from route params. Expo Router may treat `myapp://revyl-auth?...` as a normal app route while the development client is already running; the route backstop keeps rejected links visible instead of landing on an unmatched-route screen.
  </Accordion>

  <Accordion title="React Native CLI">
    Use React Native `Linking` for the deep link and read launch config from your native layer or staging backend.

    ```tsx theme={null}
    import { useEffect } from "react";
    import { Linking, NativeModules } from "react-native";

    const allowedRedirects = new Set(["/account", "/checkout", "/cart"]);
    const allowedRoles = new Set(["buyer", "support"]);

    async function getLaunchConfig() {
      return NativeModules.LaunchConfig.getRevylAuthBypassConfig();
      // { enabled: boolean, token: string }
    }

    async function handleRevylAuthBypass(rawURL: string) {
      const url = new URL(rawURL);
      if (url.protocol !== "myapp:" || url.hostname !== "revyl-auth") return;

      const config = await getLaunchConfig();
      const token = url.searchParams.get("token");
      const role = url.searchParams.get("role") || "buyer";
      const redirect = url.searchParams.get("redirect") || "/account";

      if (!config.enabled) throw new Error("Revyl auth bypass is disabled");
      if (token !== config.token) throw new Error("Bad Revyl auth bypass token");
      if (!allowedRoles.has(role)) throw new Error("Role is not allowlisted");
      if (!allowedRedirects.has(redirect)) throw new Error("Redirect is not allowlisted");

      await createTestSession({ role });
      navigationRef.navigate(routeNameForRedirect(redirect));
    }

    export function useRevylAuthBypass() {
      useEffect(() => {
        Linking.getInitialURL().then(url => {
          if (url) void handleRevylAuthBypass(url);
        });

        const subscription = Linking.addEventListener("url", event => {
          void handleRevylAuthBypass(event.url);
        });

        return () => subscription.remove();
      }, []);
    }
    ```

    For iOS, expose Revyl launch variables from `ProcessInfo.processInfo.arguments`. For Android, expose them from the launch `Intent` extras.
  </Accordion>

  <Accordion title="Native iOS">
    Revyl injects launch variables into iOS simulator apps as launch arguments:
    `-REVYL_AUTH_BYPASS_ENABLED true -REVYL_AUTH_BYPASS_TOKEN <token>`.

    ```swift theme={null}
    import SwiftUI

    private let allowedRedirects: [String: AppRoute] = [
        "/account": .account,
        "/checkout": .checkout,
        "/cart": .cart
    ]
    private let allowedRoles: Set<String> = ["buyer", "support"]

    func launchValue(_ key: String) -> String? {
        let args = ProcessInfo.processInfo.arguments
        guard let index = args.firstIndex(of: "-\(key)") else { return nil }
        let valueIndex = args.index(after: index)
        return args.indices.contains(valueIndex) ? args[valueIndex] : nil
    }

    func queryValue(_ name: String, in url: URL) -> String? {
        let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
        return items.first(where: { $0.name == name })?.value
    }

    func handleRevylAuthBypass(_ url: URL) throws {
        guard url.scheme == "myapp", url.host == "revyl-auth" else { return }

        guard launchValue("REVYL_AUTH_BYPASS_ENABLED") == "true" else { throw AuthBypassError.disabled }
        guard queryValue("token", in: url) == launchValue("REVYL_AUTH_BYPASS_TOKEN") else { throw AuthBypassError.badToken }

        let role = queryValue("role", in: url) ?? "buyer"
        let redirect = queryValue("redirect", in: url) ?? "/account"
        guard allowedRoles.contains(role) else { throw AuthBypassError.badRole }
        guard let route = allowedRedirects[redirect] else { throw AuthBypassError.badRedirect }

        TestSession.signIn(role: role)
        AppRouter.shared.navigate(to: route)
    }
    ```

    Call `handleRevylAuthBypass(_:)` from your SwiftUI `.onOpenURL` handler or your app delegate URL handler.
  </Accordion>

  <Accordion title="Native Android">
    Revyl injects launch variables into Android as string extras on the app launch intent.

    ```kotlin theme={null}
    private val allowedRedirects = mapOf(
      "/account" to AppRoute.Account,
      "/checkout" to AppRoute.Checkout,
      "/cart" to AppRoute.Cart,
    )
    private val allowedRoles = setOf("buyer", "support")

    data class RevylAuthConfig(val enabled: Boolean, val token: String?)

    fun revylAuthConfig(intent: Intent): RevylAuthConfig {
      return RevylAuthConfig(
        enabled = intent.getStringExtra("REVYL_AUTH_BYPASS_ENABLED") == "true",
        token = intent.getStringExtra("REVYL_AUTH_BYPASS_TOKEN"),
      )
    }

    fun handleRevylAuthBypass(intent: Intent, config: RevylAuthConfig) {
      val uri = intent.data ?: return
      if (uri.scheme != "myapp" || uri.host != "revyl-auth") return

      val role = uri.getQueryParameter("role") ?: "buyer"
      val redirect = uri.getQueryParameter("redirect") ?: "/account"

      check(config.enabled) { "Revyl auth bypass is disabled" }
      check(uri.getQueryParameter("token") == config.token) { "Bad Revyl auth bypass token" }
      check(role in allowedRoles) { "Role is not allowlisted" }

      val route = allowedRedirects[redirect] ?: error("Redirect is not allowlisted")
      TestSession.signIn(role)
      appRouter.navigate(route)
    }
    ```

    Store `revylAuthConfig(launchIntent)` when the app starts, then call `handleRevylAuthBypass(intent, config)` from `onCreate` and `onNewIntent`.
  </Accordion>

  <Accordion title="Flutter">
    Flutter apps usually handle the link in Dart and read launch config through a platform channel or staging backend.

    ```dart theme={null}
    import 'package:app_links/app_links.dart';
    import 'package:flutter/services.dart';

    const launchConfig = MethodChannel('app.launchConfig');
    final allowedRedirects = {
      '/account': AppRoute.account,
      '/checkout': AppRoute.checkout,
      '/cart': AppRoute.cart,
    };
    final allowedRoles = {'buyer', 'support'};

    Future<void> handleRevylAuthBypass(Uri uri) async {
      if (uri.scheme != 'myapp' || uri.host != 'revyl-auth') return;

      final config = await launchConfig.invokeMapMethod<String, String>('revylAuthBypass');
      final enabled = config?['REVYL_AUTH_BYPASS_ENABLED'] == 'true';
      final expectedToken = config?['REVYL_AUTH_BYPASS_TOKEN'];
      final role = uri.queryParameters['role'] ?? 'buyer';
      final redirect = uri.queryParameters['redirect'] ?? '/account';

      if (!enabled) throw StateError('Revyl auth bypass is disabled');
      if (uri.queryParameters['token'] != expectedToken) throw StateError('Bad Revyl auth bypass token');
      if (!allowedRoles.contains(role)) throw StateError('Role is not allowlisted');

      final route = allowedRedirects[redirect];
      if (route == null) throw StateError('Redirect is not allowlisted');

      await TestSession.signIn(role: role);
      appRouter.go(route);
    }

    Future<void> installRevylAuthBypass() async {
      final links = AppLinks();
      final initial = await links.getInitialLink();
      if (initial != null) await handleRevylAuthBypass(initial);
      links.uriLinkStream.listen(handleRevylAuthBypass);
    }
    ```

    Implement the platform channel with the native iOS and Android launch-config snippets above, or verify the token against your staging backend from Dart.
  </Accordion>
</AccordionGroup>

## What To Verify

Before relying on the bypass in a test, confirm each case is visible:

* Valid token, role, and redirect signs in and routes to the target screen.
* Wrong token is rejected.
* Missing or disabled `REVYL_AUTH_BYPASS_ENABLED` is rejected.
* Unknown role is rejected.
* Unknown redirect is rejected.
* The handler is unavailable in production builds.

## Bug Bazaar Sample

This repo includes the runnable Expo sample in `internal-apps/bug-bazaar`:

```bash theme={null}
revyl device navigate \
  --url "bug-bazaar://revyl-auth?token=revyl-demo-token&role=collector&redirect=%2Fcheckout"
```

The Account tab shows whether the auth bypass link is idle, accepted, or rejected.

Bug Bazaar is a managed Expo sample, so it uses a demo fallback token when no native launch-config bridge is present. Customer apps should wire `REVYL_AUTH_BYPASS_TOKEN` into native launch config or verify the token with a staging backend.
