@lifewind-ltda/react-native-sso-client
v1.0.0
Published
React Native SSO client for LifeWind authentication
Downloads
9
Maintainers
Readme
LifeWind React Native SSO Client
SSO authentication for React Native / Expo applications.
Provider, hooks, and components for OAuth 2.0 authentication with LifeWind in mobile apps.
Prerequisites: Register Your App in LifeWind Core
Before installing this package, you need an OAuth client registered in LifeWind Core:
- Login to the LifeWind Core admin panel at
https://lifewind-core.test/admin(or your production URL) - Navigate to OAuth Clients → Create Client
- Fill in:
- Name: Your app name (e.g. "Atlas Mobile")
- Redirect URI: Your app's deep link scheme (e.g.
myapp://sso/callbackorexp://192.168.1.10:8081/--/sso/callbackfor Expo dev) - Grant Type: Authorization Code
- After creation, copy the Client ID — you'll need it for your app config
Tip: For Expo development, use
exp://YOUR_LOCAL_IP:8081/--/sso/callbackas the redirect URI. For production builds, use your registered deep link scheme likemyapp://sso/callback.
Backend Requirement
This package works together with the Laravel SSO Client package on your backend:
composer require lifewind/laravel-sso-clientThe backend provides the JWT token exchange endpoints (/sso/validate, /sso/user, /sso/refresh) that this package communicates with. See the lifewind-laravel-sso-client README for backend setup.
Features
- React Context Provider:
<SSOProvider>manages all auth state with AsyncStorage persistence - useSSO Hook: Access authentication state and actions from any component
- SSOButton Component: Native
Pressablebutton with loading state - Expo Integration: Uses
expo-web-browserfor the OAuth flow - AsyncStorage: Persistent token storage across app restarts
- TypeScript: Full type safety
Quick Start
1. Installation
npm install lifewind-react-native-sso-clientInstall peer dependencies:
npx expo install expo-auth-session expo-web-browser @react-native-async-storage/async-storage2. Wrap Your App with SSOProvider
// App.tsx
import { SSOProvider } from "lifewind-react-native-sso-client"
import { NavigationContainer } from "@react-navigation/native"
import AppNavigator from "./navigation/AppNavigator"
const ssoConfig = {
baseUrl: "https://lifewind-core.your-domain.com",
clientId: "your_oauth_client_id",
redirectUri: "myapp://sso/callback",
backendApiUrl: "https://your-backend-api.com",
}
export default function App() {
return (
<SSOProvider config={ssoConfig}>
<NavigationContainer>
<AppNavigator />
</NavigationContainer>
</SSOProvider>
)
}3. Add Login Button
// screens/LoginScreen.tsx
import { View, Text, StyleSheet } from "react-native"
import { SSOButton } from "lifewind-react-native-sso-client"
export default function LoginScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome</Text>
<SSOButton
title="Sign in with LifeWind"
onError={(err) => console.error(err)}
/>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", padding: 24 },
title: { fontSize: 28, fontWeight: "bold", textAlign: "center", marginBottom: 32 },
})4. Handle the OAuth Callback
After the user authenticates in the browser, your app receives a deep link with code and state parameters. Handle this with Expo's linking or React Navigation:
// App.tsx or navigation setup
import { useEffect } from "react"
import * as Linking from "expo-linking"
import { useSSO } from "lifewind-react-native-sso-client"
function AuthHandler() {
const { handleCallback } = useSSO()
useEffect(() => {
const subscription = Linking.addEventListener("url", async ({ url }) => {
const parsed = Linking.parse(url)
const code = parsed.queryParams?.code as string
const state = parsed.queryParams?.state as string
if (code && state) {
try {
const result = await handleCallback(code, state)
console.log("Authenticated:", result.user)
// Navigate to home screen
} catch (err) {
console.error("Auth failed:", err)
}
}
})
return () => subscription.remove()
}, [handleCallback])
return null
}Components
SSOButton
Native login button using React Native Pressable with ActivityIndicator.
<SSOButton
title="Sign In"
loadingTitle="Signing in..."
style={{ backgroundColor: "#2563eb", borderRadius: 12 }}
textStyle={{ fontSize: 18 }}
onError={(error) => Alert.alert("Error", error)}
/>Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | string | "Login with LifeWind" | Button label |
| loadingTitle | string | "Authenticating..." | Label during auth |
| style | ViewStyle | Dark button style | Custom button styles |
| textStyle | TextStyle | White text, 16px | Custom text styles |
| onError | (error: string) => void | — | Called on error |
useSSO Hook
Access all authentication state and actions:
import { useSSO } from "lifewind-react-native-sso-client"
function ProfileScreen() {
const {
// State
isReady, // boolean — true after initial AsyncStorage load
isAuthenticated, // boolean
isLoading, // boolean
user, // SSOUser | null
token, // string | null
error, // string | null
// Actions
login, // () => Promise<void> — opens browser for OAuth
logout, // () => Promise<void> — clears state + AsyncStorage
handleCallback, // (code, state) => Promise<SSOResult>
refreshToken, // () => Promise<void>
getCurrentUser, // () => Promise<SSOUser>
clearError, // () => void
} = useSSO()
if (!isReady) return <ActivityIndicator />
if (!isAuthenticated) {
return <SSOButton onError={(e) => Alert.alert("Error", e)} />
}
return (
<View>
<Text>Welcome, {user?.name}!</Text>
<Text>{user?.email}</Text>
<Button title="Sign Out" onPress={logout} />
</View>
)
}Note:
isReadystarts asfalseand becomestrueafter the initial AsyncStorage load completes. Use this to show a splash/loading screen while persisted auth state is being restored.
Configuration
SSOConfig
interface SSOConfig {
baseUrl: string // LifeWind Core URL (e.g. "https://lifewind-core.test")
clientId: string // OAuth client ID from admin panel
redirectUri: string // Deep link callback (e.g. "myapp://sso/callback")
backendApiUrl: string // Your Laravel backend URL (e.g. "https://api.your-app.com")
scopes?: string[] // OAuth scopes (default: ["openid", "profile", "email"])
}Deep Link Setup (Expo)
Add your deep link scheme in app.json:
{
"expo": {
"scheme": "myapp"
}
}This allows your app to handle URLs like myapp://sso/callback?code=...&state=....
For development with Expo Go, the redirect URI is:
exp://YOUR_LOCAL_IP:8081/--/sso/callbackTypeScript Types
interface SSOUser {
id: string
email: string
name: string
lifewind_uuid?: string
}
interface SSOResult {
user: SSOUser
token: string
expires_in: number
}Authentication Flow
- User taps login —
<SSOButton>orlogin()opens an in-app browser viaexpo-web-browser - User authenticates — LifeWind redirects back to your deep link with a code
- Deep link captured — Your app receives the URL via
Linking.addEventListener - Token exchange —
handleCallback(code, state)sends code to your backend's/sso/validate - JWT returned — Backend validates with LifeWind and returns JWT + user data
- State persisted — Token and user stored in
AsyncStorageand React context - Ready —
useSSO()returnsisAuthenticated: true, data persists across restarts
Advanced Usage
Navigation Guard
import { useSSO } from "lifewind-react-native-sso-client"
function AppNavigator() {
const { isReady, isAuthenticated } = useSSO()
if (!isReady) {
return <SplashScreen />
}
return (
<Stack.Navigator>
{isAuthenticated ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</>
) : (
<Stack.Screen name="Login" component={LoginScreen} />
)}
</Stack.Navigator>
)
}API Requests with Token
import { useSSO } from "lifewind-react-native-sso-client"
function useApi() {
const { token } = useSSO()
const fetchWithAuth = async (url: string, options: RequestInit = {}) => {
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
...options.headers,
},
})
if (!response.ok) throw new Error(`API Error: ${response.statusText}`)
return response.json()
}
return { fetchWithAuth }
}Refresh Token on App Foreground
import { useEffect } from "react"
import { AppState } from "react-native"
import { useSSO } from "lifewind-react-native-sso-client"
function TokenRefresher() {
const { token, refreshToken } = useSSO()
useEffect(() => {
const subscription = AppState.addEventListener("change", (state) => {
if (state === "active" && token) {
refreshToken().catch(() => {
console.log("Token refresh failed")
})
}
})
return () => subscription.remove()
}, [token, refreshToken])
return null
}Troubleshooting
"Invalid state parameter" error:
- State is stored in
AsyncStorage— ensure the callback is handled in the same app session - If testing with Expo Go, make sure the redirect URI matches
exp://YOUR_IP:PORT/--/sso/callback
Browser doesn't close after auth:
- Call
WebBrowser.dismissBrowser()after handling the callback - This is handled automatically by
expo-web-browserin most cases
Deep link not being received:
- Verify your
schemeis set inapp.json - For Expo Go, use the
exp://scheme - For standalone builds, use your custom scheme (e.g.
myapp://)
Token not persisting across restarts:
- Token is stored in
AsyncStorageunderauth_token - User data is stored under
user_data - Check that
@react-native-async-storage/async-storageis properly installed
"useSSO must be used within SSOProvider" error:
- Make sure
<SSOProvider>wraps your entire app (aboveNavigationContainer)
Peer Dependencies
| Package | Version |
|---------|---------|
| react | ^18.0.0 \|\| ^19.0.0 |
| react-native | * |
| expo-auth-session | >=5.0.0 |
| expo-web-browser | >=13.0.0 |
| @react-native-async-storage/async-storage | >=1.19.0 |
License
MIT License. See LICENSE for details.
