@glideidentity/glide-fe-sdk-react-native
v3.0.0
Published
Glide Identity SDK for React Native — carrier-grade SIM-based phone number verification and authentication
Readme
@glideidentity/glide-fe-sdk-react-native
React Native SDK for Glide Identity phone authentication. Verify phone numbers through carrier networks with hardware-backed security.
This is the client-side SDK. It calls into your backend's prepare and process endpoints, which integrate with Glide's Magic Auth API v2 via one of the Glide backend SDKs (Node.js, Java, or Go). See Backend Setup below for the partner-side proxy pattern.
Authentication strategies:
The SDK supports two carrier-driven strategies. The carrier (not the OS) determines which strategy a given prepare call returns — the SDK reads authentication_strategy: 'ts43' | 'link' from the prepare response and dispatches accordingly. Branch your code on the strategy, not on Platform.OS.
| Strategy | Mechanism | Partner setup |
|---|---|---|
| ts43 | Native Digital Credentials API intent — PLMN auto-detected from the SIM | compileSdkVersion 35, Android 9+ / API 28 at runtime |
| link | Universal Link / App Link callback + Secure Enclave–signed device binding | Deep-link plumbing — see Link strategy setup |
The carrier may also be ineligible for both strategies (e.g. unsupported carrier, or an Android device that can't satisfy the Digital Credentials API requirements). In those cases prepare() rejects with a typed PhoneAuthError — wrap the call in try/catch and fall back to your alternative flow (SMS, OTP, etc.).
Prerequisites
| Requirement | Android | iOS |
|-------------|---------|-----|
| Runtime | React Native 0.70+ | React Native 0.70+ |
| IDE | Android Studio, compileSdk 35+ | Xcode 15+, iOS 16+ SDK |
| Device | Physical device with SIM card | Physical device (Secure Enclave) |
| Tools | adb on PATH | CocoaPods, Apple Developer account |
| Backend | Node.js 18+ (or Go/Java) | Node.js 18+ (or Go/Java) |
| Credentials | Glide client_id + client_secret | Glide client_id + client_secret |
iOS additional (when authentication_strategy === 'link'): Associated Domains entitlement for Universal Links — see Link strategy setup below.
Note: Emulators/simulators do not support carrier authentication. A physical device with an active SIM is required.
Installation
npm install @glideidentity/glide-fe-sdk-react-native
# iOS only
cd ios && pod installAndroid: The SDK installs on minSdkVersion 23+ (Android 6.0 Marshmallow). It needs compileSdkVersion 35 (required by the androidx.credentials library). The TS43 strategy uses the Android Digital Credentials API (Android 9+ / API 28, Google Play Services 24.0+, registered carrier credential provider). In your android/build.gradle:
buildscript {
ext {
compileSdkVersion = 35 // Required by Glide SDK
targetSdkVersion = 35
// minSdkVersion can stay at your current value (24+)
}
}Optional — for haptic feedback on Android, add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.VIBRATE" />Quick Start — Direct Mode
Use usePhoneAuth() for React components. The hook manages state and deep link handling automatically.
import { usePhoneAuth, USE_CASE } from '@glideidentity/glide-fe-sdk-react-native';
function AuthScreen() {
const { authenticate, isLoading, error, result } = usePhoneAuth({
endpoints: {
prepare: 'https://your-backend.com/api/phone-auth/prepare',
process: 'https://your-backend.com/api/phone-auth/process',
},
});
const handleVerify = async () => {
try {
const result = await authenticate({
use_case: USE_CASE.VERIFY_PHONE_NUMBER,
phone_number: '+14155551234',
});
console.log('Verified:', result.verified);
console.log('SIM swap risk:', result.sim_swap?.risk_level);
} catch (err) {
console.log('Error:', err.code, err.message);
}
};
return <Button title="Verify Phone" onPress={handleVerify} disabled={isLoading} />;
}Class-Based API
usePhoneAuth wraps PhoneAuthClient for React. Use the class directly for non-React contexts or when you need full control:
import { PhoneAuthClient, USE_CASE } from '@glideidentity/glide-fe-sdk-react-native';
const glide = new PhoneAuthClient({
endpoints: {
prepare: 'https://your-backend.com/api/phone-auth/prepare',
process: 'https://your-backend.com/api/phone-auth/process',
},
debug: true,
});
// High-level — one call handles prepare → invoke → process
const result = await glide.authenticate({
use_case: USE_CASE.GET_PHONE_NUMBER,
});
console.log(result.phone_number);Granular API
For more control, use the three-step flow:
const { prepare, invokeSecurePrompt, getPhoneNumber, verifyPhoneNumber } = usePhoneAuth({
endpoints: {
prepare: 'https://your-backend.com/api/phone-auth/prepare',
process: 'https://your-backend.com/api/phone-auth/process',
},
});
// Step 1: Prepare
const prepared = await prepare({
use_case: USE_CASE.GET_PHONE_NUMBER,
});
// Step 2: Invoke carrier auth (must be from user gesture on iOS)
const invokeResult = await invokeSecurePrompt(prepared);
const credential = await invokeResult.credential;
// Step 3: Process
const result = await getPhoneNumber(credential, prepared.session);
console.log(result.phone_number);Configuration
The full PhoneAuthConfig shape (all fields except endpoints are optional):
import type { PhoneAuthConfig, HttpClient, Logger } from '@glideidentity/glide-fe-sdk-react-native';
const config: PhoneAuthConfig = {
endpoints: {
// Required — your backend's prepare proxy.
prepare: 'https://your-backend.com/api/phone-auth/prepare',
// Required — your backend's process proxy. Routes to Glide's
// /get-phone-number or /verify-phone-number based on use_case.
process: 'https://your-backend.com/api/phone-auth/process',
// Optional — your backend's report-invocation proxy for ASR (Auth
// Success Rate) telemetry. Fired once per invokeSecurePrompt() in
// a fire-and-forget POST { session_id }. Omit if you don't want
// ASR metrics; the SDK silently skips the call.
reportInvocation: 'https://your-backend.com/api/phone-auth/report-invocation',
},
// Optional — replace the default fetch-based HTTP client. Implement
// the HttpClient interface to route through Axios/ky/etc. or to add
// common headers / interceptors. Omit to use the built-in fetch.
// httpClient: new MyAxiosClient(),
// Optional — plug in your logging framework (Sentry, Datadog, etc.).
// Defaults to console.{debug,info,warn,error}. The SDK always emits
// info/warn/error; debug is gated by the `debug` flag below.
// logger: pino(),
// Optional — enable verbose debug-level logging. Default: false.
debug: false,
// Optional — per-request timeout in ms. Default: 30_000 (30s).
timeout: 30_000,
// Optional — maximum wait time in ms for the Link strategy to
// receive its Universal Link / App Link callback after the carrier
// hand-off. Default: 300_000 (5 min) — matches the backend session
// TTL. Increase only if you have a non-default backend session TTL.
sessionTimeout: 300_000,
};The minimal config in practice:
const config: PhoneAuthConfig = {
endpoints: {
prepare: 'https://your-backend.com/api/phone-auth/prepare',
process: 'https://your-backend.com/api/phone-auth/process',
},
};Backend Setup
Your backend holds Glide credentials and proxies requests. The process endpoint must route based on use_case — there is no single /process endpoint on the Glide API.
const express = require('express');
const app = express();
app.use(express.json());
const { GLIDE_CLIENT_ID, GLIDE_CLIENT_SECRET } = process.env;
const GLIDE_API = 'https://api.glideidentity.app';
let tokenCache = { token: '', expiresAt: 0 };
async function getToken() {
if (tokenCache.token && Date.now() < tokenCache.expiresAt) return tokenCache.token;
const res = await fetch(`${GLIDE_API}/oauth2-cc/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=client_credentials&client_id=${GLIDE_CLIENT_ID}&client_secret=${GLIDE_CLIENT_SECRET}`,
});
const data = await res.json();
tokenCache = { token: data.access_token, expiresAt: Date.now() + (data.expires_in - 60) * 1000 };
return tokenCache.token;
}
async function proxyToGlide(endpoint, body) {
const token = await getToken();
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
// Forward client_info.user_agent so Glide returns the correct strategy per platform
if (body?.client_info?.user_agent) headers['User-Agent'] = body.client_info.user_agent;
const res = await fetch(`${GLIDE_API}${endpoint}`, {
method: 'POST', headers, body: JSON.stringify(body),
});
return { status: res.status, data: await res.json() };
}
app.post('/api/phone-auth/prepare', async (req, res) => {
try {
const { status, data } = await proxyToGlide('/magic-auth/v2/auth/prepare', req.body);
res.status(status).json(data);
} catch (err) {
res.status(500).json({ error: 'PROXY_ERROR', message: err.message });
}
});
app.post('/api/phone-auth/process', async (req, res) => {
try {
// use_case determines the Glide endpoint but is NOT forwarded to the API
const { use_case, ...glideBody } = req.body;
const endpoint = use_case === 'GetPhoneNumber'
? '/magic-auth/v2/auth/get-phone-number'
: '/magic-auth/v2/auth/verify-phone-number';
const { status, data } = await proxyToGlide(endpoint, glideBody);
res.status(status).json(data);
} catch (err) {
res.status(500).json({ error: 'PROXY_ERROR', message: err.message });
}
});
// Optional — Auth Success Rate (ASR) telemetry. The SDK fires-and-forgets
// POST { session_id } once per invokeSecurePrompt() so the carrier can
// distinguish "user saw the prompt" from "user never opened the app". Omit
// the route entirely if you set `endpoints.reportInvocation: undefined` in
// the SDK config — the SDK skips the call without erroring.
app.post('/api/phone-auth/report-invocation', async (req, res) => {
try {
const { status, data } = await proxyToGlide('/magic-auth/v2/auth/report-invocation', req.body);
res.status(status).json(data);
} catch (err) {
// Fire-and-forget on the SDK side — keep this route quiet on error.
res.status(202).json({});
}
});
app.listen(3001);Key backend behaviors:
- Route on
use_case: The SDK sendsuse_casein the process body. Use it to pick/get-phone-numberor/verify-phone-number, then strip it before forwarding. - Forward User-Agent: The SDK sends
client_info.user_agent. Forward it as the HTTPUser-Agentheader — Glide uses this for strategy selection. - Pass-through everything else:
session,credential,signature,public_key— forward as-is. - Report-invocation is optional: Skip the route +
endpoints.reportInvocationconfig entry if you don't need ASR metrics. With it wired, the SDK posts{ session_id }once perinvokeSecurePrompt()(fire-and-forget — your backend's response doesn't affect the user's flow).
Link strategy setup
When prepare returns authentication_strategy: 'link', the carrier hands off to a Universal Link / App Link that needs to return control to your app. Two pieces are required:
- Per-OS platform setup — described below. Currently iOS only (Android setup will be documented when the SDK ships native plumbing for it).
- Deep-link forwarding in your app — described in Forward deep links to the SDK. Same code on iOS and Android.
Forward deep links to the SDK
usePhoneAuth() already registers the listener for you on both iOS and Android — skip this section if you're using the hook.
If you're using PhoneAuthClient directly, register the Linking listener and gate it with isGlideDeepLink(url) so the SDK only sees URLs that are actually a carrier callback (any other deep link your app receives keeps flowing to your own router):
import { Linking } from 'react-native';
import { PhoneAuthClient, isGlideDeepLink } from '@glideidentity/glide-fe-sdk-react-native';
const glide = new PhoneAuthClient(config);
const sub = Linking.addEventListener('url', ({ url }) => {
if (isGlideDeepLink(url)) {
glide.handleDeepLink(url);
return;
}
// Not ours — your app's own deep-link router takes it from here.
});
// Cold-start: handle a URL the OS used to launch the app, if any.
Linking.getInitialURL().then((url) => {
if (url && isGlideDeepLink(url)) glide.handleDeepLink(url);
});
// Remember to call sub.remove() on unmount.isGlideDeepLink(url) recognises a carrier callback by the standard OAuth shape (both code and state query parameters present). It does NOT match on your redirect domain or path — those are configured at carrier-onboarding time and are partner-specific.
If you prefer a single short-circuit gate, glide.handleDeepLink(url) itself returns a boolean: true if the SDK consumed the URL, false otherwise — equivalent shape to the example above.
iOS — Universal Links
1. Host an AASA file on your redirect domain
During onboarding, Glide configures the carrier to redirect to your redirect domain (e.g., auth.yourcompany.com) after authentication. iOS needs to know your app handles URLs on that domain — done via an Apple App Site Association (AASA) file.
Create a file at https://<your-redirect-domain>/.well-known/apple-app-site-association:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM_ID.com.yourcompany.yourapp",
"paths": [
"/glideid/callback",
"/glideid/callback/*"
]
}
]
}
}- Replace
TEAM_IDwith your Apple Developer Team ID. - Replace
com.yourcompany.yourappwith your app's bundle identifier. - The path
/glideid/callbackis a placeholder — pick any path you control on your redirect domain. Whatever you choose must match the redirect URI you provide to Glide during onboarding (Glide configures the carrier with that exact URL). - Include both the exact path and the
/*wildcard variant — carriers may append session-specific path segments, and exact-only matching causes intermittent fall-through to Safari. - File must be served over HTTPS with
Content-Type: application/jsonand no redirects.
2. Add the Associated Domains capability in Xcode
Project → Target → Signing & Capabilities → + Capability → Associated Domains, then add:
applinks:<your-redirect-domain>
applinks:<your-redirect-domain>?mode=developerThe ?mode=developer variant bypasses Apple's CDN cache during development.
iOS testing checklist
- [ ] AASA file is accessible:
curl https://<your-redirect-domain>/.well-known/apple-app-site-association - [ ] App is installed on a physical device (Universal Links don't fire on the Simulator)
- [ ] App has been launched at least once after install (iOS caches the AASA on first launch)
- [ ] If links don't work: delete the app, reinstall, launch once before testing
| Issue | Fix |
|---|---|
| Universal Link opens Safari instead of app | Delete app, reinstall, launch once. Verify AASA is reachable. |
| AASA not found | Ensure it's at /.well-known/apple-app-site-association (no file extension), HTTPS, no redirects. |
| Works on one device but not another | Each device caches AASA independently. Reinstall the app. |
Android Setup
The TS43 Digital Credentials API is accessed via native modules included in the SDK. Most configuration is automatic.
PLMN auto-detection: The SDK reads the carrier's MCC/MNC from the SIM card via TelephonyManager — no permissions required. This is included in the prepare request so Glide can identify the carrier and return the correct TS43 configuration.
Optional — haptic feedback on successful authentication:
<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.VIBRATE" />Error Handling
import { isAuthError, ERROR_CODES, getUserMessage } from '@glideidentity/glide-fe-sdk-react-native';
try {
await authenticate(request);
} catch (err) {
if (isAuthError(err)) {
switch (err.code) {
case ERROR_CODES.USER_CANCELLED:
// User closed the system prompt
break;
case ERROR_CODES.TIMEOUT:
// Request or Link return timed out
break;
case ERROR_CODES.PLATFORM_NOT_SUPPORTED:
// TS43 not available (non-Android) or SE not available (non-iOS)
break;
}
console.log(getUserMessage(err)); // User-friendly message
}
}Response Types
Both getPhoneNumber() and verifyPhoneNumber() include carrier fraud signals and (when available) parsed phone-number components:
interface GetPhoneNumberResponse {
phone_number: string; // E.164, e.g. "+14155551234"
aud?: string; // Carrier audience value
sim_swap?: SimSwapInfo; // SIM card change detection
device_swap?: DeviceSwapInfo; // IMEI change detection
phone_number_details?: PhoneNumberDetails; // Parsed components (server-populated)
}
interface VerifyPhoneNumberResponse {
phone_number: string;
verified: boolean; // true if the phone matched
aud?: string;
sim_swap?: SimSwapInfo;
device_swap?: DeviceSwapInfo;
phone_number_details?: PhoneNumberDetails;
}
interface PhoneNumberDetails {
country_code: number; // ITU E.164 country calling code (1, 44, 972, …)
national_number: string; // National significant number, no trunk prefix
region_code: string; // ISO 3166-1 alpha-2 ("US", "GB", "IL")
}
interface SimSwapInfo {
checked: boolean;
risk_level?: 'RISK_LEVEL_HIGH' | 'RISK_LEVEL_MEDIUM' | 'RISK_LEVEL_LOW' | 'RISK_LEVEL_UNKNOWN';
age_band?: string; // e.g., "0-4 hours"
checked_at?: string; // RFC 3339
reason?: 'timeout' | 'carrier_not_supported' | 'disabled' | 'error';
}
// DeviceSwapInfo has the same shape as SimSwapInfo.API Reference
| Export | Kind | Description |
|--------|------|-------------|
| PhoneAuthClient | Class | Main client — direct programmatic use (non-React) |
| usePhoneAuth | Hook | React wrapper: PhoneAuthClient + deep-link listener + state |
| UsePhoneAuthReturn | Type | Return type of usePhoneAuth (authenticate, prepare, invokeSecurePrompt, getPhoneNumber, verifyPhoneNumber, isLoading, error, result) |
| generateKeyPair | Function | Secure Enclave P-256 key generation (iOS) |
| signData | Function | Secure Enclave ECDSA signing (iOS) |
| deleteKey | Function | Clean up SE key after use (iOS) |
| requestDigitalCredential | Function | TS43 Digital Credentials API (Android) |
| getCarrierInfo | Function | SIM carrier info (MCC/MNC) |
| parseDeepLink | Function | Extract code/state from callback URL |
| isGlideDeepLink | Function | Check if URL is a Glide callback |
| DeepLinkResult | Type | parseDeepLink's discriminated return type |
| USE_CASE | Const | GET_PHONE_NUMBER, VERIFY_PHONE_NUMBER |
| AUTHENTICATION_STRATEGY | Const | Strategy identifiers (ts43, link) |
| ERROR_CODES | Const | All client-side error codes |
| ErrorCode | Type | Union of ERROR_CODES values |
| createAuthError | Function | Construct an AuthError (mainly for testing / re-wrapping) |
| isAuthError | Function | Type guard for AuthError |
| isRetryableError | Function | Returns true for transient errors safe to retry |
| getUserMessage | Function | User-friendly error message for an AuthError |
| PhoneAuthConfig, PrepareRequest, PrepareResponse, InvokeResult, SessionInfo, GetPhoneNumberResponse, VerifyPhoneNumberResponse, SimSwapInfo, DeviceSwapInfo, AuthError, HttpClient, Logger, … | Types | Full PhoneAuthConfig shape, request/response models, error type, and pluggable interfaces — see source for complete list. |
