React Native / Expo
Add Base IdP login to a React Native or Expo app — public client, PKCE, with expo-web-browser handling the system browser session.
React Native and Expo are public clients. The app opens a system browser
through expo-web-browser, runs PKCE, and receives the authorization code on
a custom URL scheme. The client id ships in the bundle; no secret ever does.
This guide covers the integration end to end: scaffolding, the TypeScript code, the Expo configuration, and what to do with the tokens once you have them.
Scaffold
npx base-idp create \
--stack react-native \
--client-id sq_live_yourapp \
--redirect-uri "myapp://auth/callback"This writes auth/baseIdp.ts, a .env, and a setup readme into
./base-idp-react-native/. Move the file into your project and follow the
configuration steps below.
Install
npm install base-idp expo-web-browser expo-linkingIf you are using bare React Native instead of Expo, swap expo-web-browser
and expo-linking for react-native-inappbrowser-reborn and
react-native-url-polyfill. The Base IdP browser SDK does not care which
one you use — you only need a way to open the URL and receive the redirect.
The TypeScript side
import { createBrowserBaseIdpAuth } from "base-idp/browser";
export const auth = createBrowserBaseIdpAuth({
clientId: process.env.EXPO_PUBLIC_BASE_IDP_CLIENT_ID!,
});EXPO_PUBLIC_* variables are exposed to the bundle, which is the right
place for a client id (not a secret).
EXPO_PUBLIC_BASE_IDP_CLIENT_ID=sq_live_yourappRunning the login
The browser SDK's loginWithRedirect does not actually open the browser on
its own in a mobile context — it returns the URL, and you hand that URL to
expo-web-browser. After the redirect, you pass the returned URL back to
handleCallback.
import * as WebBrowser from "expo-web-browser";
import { auth } from "./baseIdp";
const REDIRECT_URI = "myapp://auth/callback";
export async function signIn() {
const url = await auth.buildLoginUrl({
redirectUri: REDIRECT_URI,
});
const result = await WebBrowser.openAuthSessionAsync(url, REDIRECT_URI);
if (result.type !== "success" || !result.url) {
throw new Error("Sign-in was cancelled");
}
return await auth.handleCallback(result.url);
}openAuthSessionAsync opens the system browser, waits for the redirect to
REDIRECT_URI, and resolves with the redirect URL. From there, the SDK's
handleCallback parses the code, exchanges it for tokens, and returns the
verified session.
Configuring the URL scheme
Expo and React Native both need to know your app handles the custom scheme.
Expo
In app.json (or app.config.ts):
{
"expo": {
"scheme": "myapp",
"ios": {
"bundleIdentifier": "com.example.myapp",
"infoPlist": {
"CFBundleURLTypes": [
{ "CFBundleURLSchemes": ["myapp"] }
]
}
},
"android": {
"package": "com.example.myapp",
"intentFilters": [
{
"action": "VIEW",
"category": ["DEFAULT", "BROWSABLE"],
"data": [{ "scheme": "myapp" }]
}
]
}
}
}After changing app.json, run npx expo prebuild so the native projects
pick up the new scheme.
Bare React Native
For bare projects, do the same Info.plist and AndroidManifest.xml edits
that the Flutter guide shows. The XML is identical.
A sign-in screen
import { useState } from "react";
import { View, Text, Button } from "react-native";
import { signIn } from "@/auth/signIn";
export default function SignInScreen() {
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
async function onPress() {
setBusy(true);
setError(null);
try {
const session = await signIn();
console.log("signed in as", session.principal.email);
// Navigate to your home screen, or send the session to your backend.
} catch (e: any) {
setError(e.message ?? "Sign-in failed");
} finally {
setBusy(false);
}
}
return (
<View>
{error && <Text>{error}</Text>}
<Button title={busy ? "Signing in..." : "Sign in"} onPress={onPress} disabled={busy} />
</View>
);
}Storing tokens
Use expo-secure-store (Expo) or react-native-keychain (bare RN). Never
use AsyncStorage for tokens — it is unencrypted.
import * as SecureStore from "expo-secure-store";
export const tokenStore = {
async save(access: string, refresh: string) {
await SecureStore.setItemAsync("access_token", access);
await SecureStore.setItemAsync("refresh_token", refresh);
},
async getAccess() {
return SecureStore.getItemAsync("access_token");
},
async getRefresh() {
return SecureStore.getItemAsync("refresh_token");
},
async clear() {
await SecureStore.deleteItemAsync("access_token");
await SecureStore.deleteItemAsync("refresh_token");
},
};Sending the session to your backend
Same pattern as Flutter: POST the session to your backend's exchange endpoint and let it issue its own session tokens.
export async function exchangeWithBackend(session: BaseIdpSession) {
const response = await fetch("https://api.example.com/auth/idp-exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
access_token: session.tokens.accessToken,
refresh_token: session.tokens.refreshToken,
principal: session.principal,
}),
});
return response.json();
}On the server side, see Express, Go, or Rust.
Refresh and logout
import { auth } from "./baseIdp";
import { tokenStore } from "./tokenStore";
export async function refresh() {
const refreshToken = await tokenStore.getRefresh();
if (!refreshToken) return;
const tokens = await auth.refreshTokens(refreshToken);
await tokenStore.save(tokens.accessToken, tokens.refreshToken);
}
export async function signOut() {
const refreshToken = await tokenStore.getRefresh();
if (refreshToken) {
await auth.logout(refreshToken);
}
await tokenStore.clear();
}Common problems
"An error occurred during sign-in"
The redirect URI in your code does not match the scheme registered with the
OS. Check app.json, run npx expo prebuild, rebuild the app.
Web preview does not redirect back
Expo's web preview does not handle custom URL schemes. Test sign-in on a real device or simulator, not in Expo for web.
result.type === "cancel" immediately
The browser opened a URL with no scheme it recognizes. Check that the URL
your code passes to openAuthSessionAsync is the authorize URL from
buildLoginUrl, not your redirect URI.