From rn-claude
Use when implementing auth -- SecureStore for tokens, session management with React Query, Expo Router auth guards, refresh token pattern, sign in/out flow.
How this skill is triggered — by the user, by Claude, or both
Slash command
/rn-claude:authThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Tokens in SecureStore. Session in React Query. Guards in Expo Router layouts.
Tokens in SecureStore. Session in React Query. Guards in Expo Router layouts.
src/libs/token.ts)import * as SecureStore from 'expo-secure-store'
const ACCESS_KEY = 'access_token'
const REFRESH_KEY = 'refresh_token'
export const tokenStorage = {
getAccess: () => SecureStore.getItemAsync(ACCESS_KEY),
setAccess: (token: string) => SecureStore.setItemAsync(ACCESS_KEY, token),
getRefresh: () => SecureStore.getItemAsync(REFRESH_KEY),
setRefresh: (token: string) => SecureStore.setItemAsync(REFRESH_KEY, token),
clear: () => Promise.all([
SecureStore.deleteItemAsync(ACCESS_KEY),
SecureStore.deleteItemAsync(REFRESH_KEY),
]),
}
src/hooks/use-session.ts)import { useQuery, useQueryClient } from '@tanstack/react-query'
import { tokenStorage } from '@/libs/token'
import { api } from '@/libs/axios'
const SESSION_KEY = ['session']
export function useSession() {
const queryClient = useQueryClient()
const { data: session, isPending } = useQuery({
queryKey: SESSION_KEY,
queryFn: async () => {
const token = await tokenStorage.getAccess()
if (!token) return null
const { data } = await api.get('/auth/me')
return data
},
staleTime: 5 * 60 * 1000, // revalidate session every 5 min
})
const signIn = async (tokens: { access: string; refresh: string }) => {
await Promise.all([
tokenStorage.setAccess(tokens.access),
tokenStorage.setRefresh(tokens.refresh),
])
await queryClient.invalidateQueries({ queryKey: SESSION_KEY })
}
const signOut = async () => {
await tokenStorage.clear()
queryClient.setQueryData(SESSION_KEY, null)
queryClient.clear()
}
return { session, isPending, signIn, signOut }
}
app/(auth)/_layout.tsx -- guest only, redirects logged-in users:
import { Redirect, Stack } from 'expo-router'
import { useSession } from '@/hooks/use-session'
import { ActivityIndicator, View } from 'react-native'
export default function AuthLayout() {
const { session, isPending } = useSession()
if (isPending) {
return (
<View className="flex-1 items-center justify-center">
<ActivityIndicator />
</View>
)
}
if (session) return <Redirect href="/(app)/(tabs)/" />
return <Stack screenOptions={{ headerShown: false }} />
}
app/(app)/_layout.tsx -- protected, redirects guests:
import { Redirect, Stack } from 'expo-router'
import { useSession } from '@/hooks/use-session'
export default function AppLayout() {
const { session, isPending } = useSession()
if (isPending) return null // splash screen shows during check
if (!session) return <Redirect href="/(auth)/login" />
return <Stack screenOptions={{ headerShown: false }} />
}
// src/libs/axios.ts
api.interceptors.request.use(async (config) => {
const token = await tokenStorage.getAccess()
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// Refresh token on 401
api.interceptors.response.use(
(res) => res,
async (error) => {
const original = error.config
if (error.response?.status === 401 && !original._retry) {
original._retry = true
try {
const refresh = await tokenStorage.getRefresh()
const { data } = await axios.post(`${process.env.EXPO_PUBLIC_API_URL}/auth/refresh`, { refresh })
await tokenStorage.setAccess(data.access)
original.headers.Authorization = `Bearer ${data.access}`
return api(original)
} catch {
await tokenStorage.clear()
// Redirect to login -- handled by router guard
}
}
return Promise.reject(error)
},
)
function LoginScreen() {
const { signIn } = useSession()
const { mutate: login, isPending } = useMutation({
mutationFn: (values: LoginFormValues) => api.post('/auth/login', values).then(r => r.data),
onSuccess: async (data) => {
await signIn({ access: data.accessToken, refresh: data.refreshToken })
// Router guard handles redirect automatically
},
})
// ...form using react-hook-form (see forms skill)
}
function ProfileScreen() {
const { signOut } = useSession()
return (
<Pressable onPress={signOut} accessibilityRole="button">
<Text className="text-red-500">Sign out</Text>
</Pressable>
)
}
SecureStore only -- never MMKV, never zustand, never AsyncStorageuseEffect auth checksqueryClient.clear() on sign out -- removes all cached dataCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub shankulkarni/claude-plugin-marketplace --plugin rn-claude