Quickstart: React Native
This guide walks through a React Native integration using expo-auth-session for the OAuth flow. React Native apps are public clients — they do not use a client_secret and rely entirely on PKCE for security.
Prerequisites: Expo SDK 51+, an OAuth client registered as Mobile app at developers.platform.loop.health (see the main quickstart).
Install dependencies
npx expo install expo-auth-session expo-crypto expo-web-browserRegister your redirect URI
In the developer portal, add your app’s redirect URI. For Expo development:
exp://127.0.0.1:8081/--/auth/callbackFor production builds, use a custom scheme registered in app.json:
{
"expo": {
"scheme": "myloopapp"
}
}This gives you: myloopapp://auth/callback
Full example — ConnectWithLoop.tsx
import * as AuthSession from "expo-auth-session";
import * as Crypto from "expo-crypto";
import * as WebBrowser from "expo-web-browser";
import { useEffect, useState } from "react";
import { Button, Text, View } from "react-native";
WebBrowser.maybeCompleteAuthSession();
const DISCOVERY = {
authorizationEndpoint: "https://identity.platform.loop.health/v1/oauth/authorize",
tokenEndpoint: "https://identity.platform.loop.health/v1/oauth/token",
revocationEndpoint: "https://identity.platform.loop.health/v1/oauth/revoke",
};
const CLIENT_ID = "client_01HXY...";
const SCOPES = ["openid", "profile", "read:biomarkers"];
export function ConnectWithLoop() {
const redirectUri = AuthSession.makeRedirectUri({ path: "auth/callback" });
const [tokens, setTokens] = useState<AuthSession.TokenResponse | null>(null);
const [biomarkers, setBiomarkers] = useState(null);
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: CLIENT_ID,
scopes: SCOPES,
redirectUri,
usePKCE: true,
responseType: AuthSession.ResponseType.Code,
},
DISCOVERY,
);
useEffect(() => {
if (response?.type !== "success") return;
const { code } = response.params;
AuthSession.exchangeCodeAsync(
{
clientId: CLIENT_ID,
code,
redirectUri,
extraParams: { code_verifier: request?.codeVerifier ?? "" },
},
DISCOVERY,
).then((tokenResponse) => {
setTokens(tokenResponse);
});
}, [response]);
async function fetchBiomarkers() {
if (!tokens) return;
const res = await fetch(
"https://clinical.platform.loop.health/v1/biomarkers/me",
{ headers: { Authorization: `Bearer ${tokens.accessToken}` } },
);
if (res.status === 401) {
// Access token expired — refresh it
const refreshed = await tokens.refreshAsync(
{ clientId: CLIENT_ID },
DISCOVERY,
);
setTokens(refreshed);
// Retry with new token
const retry = await fetch(
"https://clinical.platform.loop.health/v1/biomarkers/me",
{ headers: { Authorization: `Bearer ${refreshed.accessToken}` } },
);
setBiomarkers(await retry.json());
return;
}
setBiomarkers(await res.json());
}
return (
<View style={{ padding: 24, gap: 16 }}>
{!tokens ? (
<Button
title="Connect with Loop"
disabled={!request}
onPress={() => promptAsync()}
/>
) : (
<>
<Text>Connected! Scopes: {tokens.scope}</Text>
<Button title="Fetch Biomarkers" onPress={fetchBiomarkers} />
{biomarkers && (
<Text>{JSON.stringify(biomarkers, null, 2)}</Text>
)}
</>
)}
</View>
);
}Secure token storage
Never store tokens in AsyncStorage unencrypted. Use expo-secure-store:
import * as SecureStore from "expo-secure-store";
async function saveTokens(tokens: AuthSession.TokenResponse) {
await SecureStore.setItemAsync(
"loop_tokens",
JSON.stringify({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresIn: tokens.expiresIn,
issuedAt: tokens.issuedAt,
}),
);
}
async function loadTokens(): Promise<AuthSession.TokenResponse | null> {
const raw = await SecureStore.getItemAsync("loop_tokens");
if (!raw) return null;
return new AuthSession.TokenResponse(JSON.parse(raw));
}Deep link configuration
For standalone builds, configure the custom URL scheme in app.json:
{
"expo": {
"scheme": "myloopapp",
"ios": {
"bundleIdentifier": "com.example.myloopapp"
},
"android": {
"package": "com.example.myloopapp",
"intentFilters": [
{
"action": "VIEW",
"data": [{ "scheme": "myloopapp", "host": "auth", "pathPrefix": "/callback" }],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}Register myloopapp://auth/callback as a redirect URI in the Loop developer portal.
Key differences from web apps
| Aspect | Web (confidential) | React Native (public) |
|---|---|---|
| Client secret | Sent in token exchange | Never used |
| PKCE | Required | Required |
| Token storage | Server-side session / encrypted DB | expo-secure-store |
| Redirect URI | https:// URL | Custom scheme (myloopapp://) |
| Refresh | Server-side POST /token | TokenResponse.refreshAsync() |
Next steps
- Authorization flow — understand the full state machine and error paths.
- Scopes — choose the right scopes for your integration.
- Security — PKCE details, redirect URI rules, what to never log.