skills/cjharmath/rn-auth/SKILL.md
React Native authentication patterns for Expo apps. Use when implementing login flows, Google/Apple sign-in, token management, session handling, or debugging auth issues in Expo/React Native.
npx skillsauth add aiskillstore/marketplace rn-authInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use expo-auth-session with expo-web-browser for OAuth flows:
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import * as Google from 'expo-auth-session/providers/google';
// Critical: Call this at module level for proper redirect handling
WebBrowser.maybeCompleteAuthSession();
// Inside component
const [request, response, promptAsync] = Google.useAuthRequest({
iosClientId: 'YOUR_IOS_CLIENT_ID.apps.googleusercontent.com',
webClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com', // For backend verification
scopes: ['profile', 'email'],
});
maybeCompleteAuthSession() - Auth redirects fail silently without this at module levelapp.json scheme must match Google Cloud Console redirect URIAuthSession.makeRedirectUri() to handle bothUse expo-secure-store for tokens (not AsyncStorage):
import * as SecureStore from 'expo-secure-store';
const TOKEN_KEY = 'auth_token';
const REFRESH_KEY = 'refresh_token';
export const tokenStorage = {
async save(token: string, refresh?: string) {
await SecureStore.setItemAsync(TOKEN_KEY, token);
if (refresh) {
await SecureStore.setItemAsync(REFRESH_KEY, refresh);
}
},
async get() {
return SecureStore.getItemAsync(TOKEN_KEY);
},
async getRefresh() {
return SecureStore.getItemAsync(REFRESH_KEY);
},
async clear() {
await SecureStore.deleteItemAsync(TOKEN_KEY);
await SecureStore.deleteItemAsync(REFRESH_KEY);
},
};
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
type AuthState = {
token: string | null;
user: User | null;
isLoading: boolean;
signIn: (token: string, user: User) => Promise<void>;
signOut: () => Promise<void>;
};
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [token, setToken] = useState<string | null>(null);
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Restore session on mount
async function restore() {
try {
const savedToken = await tokenStorage.get();
if (savedToken) {
// Validate token with backend before trusting it
const userData = await validateToken(savedToken);
setToken(savedToken);
setUser(userData);
}
} catch {
await tokenStorage.clear();
} finally {
setIsLoading(false);
}
}
restore();
}, []);
const signIn = async (newToken: string, userData: User) => {
await tokenStorage.save(newToken);
setToken(newToken);
setUser(userData);
};
const signOut = async () => {
await tokenStorage.clear();
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider value={{ token, user, isLoading, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be inside AuthProvider');
return ctx;
};
// app/_layout.tsx
import { Slot, useRouter, useSegments } from 'expo-router';
import { useAuth } from '@/contexts/auth';
import { useEffect } from 'react';
export default function RootLayout() {
const { token, isLoading } = useAuth();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
if (isLoading) return;
const inAuthGroup = segments[0] === '(auth)';
if (!token && !inAuthGroup) {
router.replace('/(auth)/login');
} else if (token && inAuthGroup) {
router.replace('/(app)/home');
}
}, [token, isLoading, segments]);
if (isLoading) {
return <LoadingScreen />;
}
return <Slot />;
}
// api/client.ts
import { tokenStorage } from '@/utils/tokenStorage';
const API_BASE = process.env.EXPO_PUBLIC_API_URL;
async function authFetch(path: string, options: RequestInit = {}) {
const token = await tokenStorage.get();
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
if (response.status === 401) {
// Token expired - try refresh or force logout
const refreshed = await attemptTokenRefresh();
if (!refreshed) {
await tokenStorage.clear();
// Trigger auth state update (emit event or use callback)
}
}
return response;
}
# For reference: backend should verify Google tokens like this
from google.oauth2 import id_token
from google.auth.transport import requests
def verify_google_token(token: str, client_id: str) -> dict:
"""Verify Google ID token and return user info."""
idinfo = id_token.verify_oauth2_token(
token,
requests.Request(),
client_id # Use WEB client ID here, not iOS
)
return {
"google_id": idinfo["sub"],
"email": idinfo["email"],
"name": idinfo.get("name"),
}
// Log the redirect URI being used
console.log('Redirect URI:', AuthSession.makeRedirectUri());
Compare this with what's configured in:
app.json scheme field| Error | Likely Cause |
|-------|--------------|
| "redirect_uri_mismatch" | Redirect URI in console doesn't match app |
| Auth popup opens but nothing happens | Missing maybeCompleteAuthSession() |
| Works in Expo Go, fails in build | Using Expo Go redirect URI in standalone config |
| Token validation fails on backend | Using iOS client ID instead of web client ID for verification |
await tokenStorage.clear()development
Apple Human Interface Guidelines for content display components. Use this skill when the user asks about charts component, collection view, image view, web view, color well, image well, activity view, lockup, data visualization, content display, displaying images, rendering web content, color pickers, or presenting collections of items in Apple apps. Also use when the user says how should I display charts, what's the best way to show images, should I use a web view, how do I build a grid of items, what component shows media, or how do I present a share sheet. Cross-references: hig-foundations for color/typography/accessibility, hig-patterns for data visualization patterns, hig-components-layout for structural containers, hig-platforms for platform-specific component behavior.
tools
Automate HelpDesk tasks via Rube MCP (Composio): list tickets, manage views, use canned responses, and configure custom fields. Always search tools first for current schemas.
testing
Expert Haskell engineer specializing in advanced type systems, pure functional design, and high-reliability software. Use PROACTIVELY for type-level programming, concurrency, and architecture guidance.
tools
GraphQL gives clients exactly the data they need - no more, no less. One endpoint, typed schema, introspection. But the flexibility that makes it powerful also makes it dangerous. Without proper controls, clients can craft queries that bring down your server. This skill covers schema design, resolvers, DataLoader for N+1 prevention, federation for microservices, and client integration with Apollo/urql. Key insight: GraphQL is a contract. The schema is the API documentation. Design it carefully.