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.
Pick the recipe closest to your app. The session creation and route names are app-specific, but the Revyl contract stays the same.
Expo / Expo Router
Put the handler near your root layout so it catches initial links and links opened while the app is running.
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.
React Native CLI
Use React Native Linking for the deep link and read launch config from your native layer or staging backend.
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.
Native iOS
Revyl injects launch variables into iOS simulator apps as launch arguments:
-REVYL_AUTH_BYPASS_ENABLED true -REVYL_AUTH_BYPASS_TOKEN <token>.
Call handleRevylAuthBypass(_:) from your SwiftUI .onOpenURL handler or your app delegate URL handler.
Native Android
Revyl injects launch variables into Android as string extras on the app launch intent.
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.
Flutter
Flutter apps usually handle the link in Dart and read launch config through a platform channel or staging backend.
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.
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.