npm package: @lifewind-ltda/react-native-sso-client
Description: React Native SSO client for LifeWind authentication
Author: LifeWind
Latest version: 1.0.1

# 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:

1. **Login** to the LifeWind Core admin panel at `https://lifewind-core.test/admin` (or your production URL)
2. Navigate to **OAuth Clients** → **Create Client**
3. Fill in:
   - **Name**: Your app name (e.g. "Atlas Mobile")
   - **Redirect URI**: Your app's deep link scheme (e.g. `myapp://sso/callback` or `exp://192.168.1.10:8081/--/sso/callback` for Expo dev)
   - **Grant Type**: Authorization Code
4. 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/callback` as the redirect URI. For production builds, use your registered deep link scheme like `myapp://sso/callback`.

## Backend Requirement

This package works together with the **Laravel SSO Client** package on your backend:

```bash
composer require lifewind/laravel-sso-client
```

The 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](https://github.com/lifewind/laravel-sso-client) 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 `Pressable` button with loading state
- **Expo Integration**: Uses `expo-web-browser` for the OAuth flow
- **AsyncStorage**: Persistent token storage across app restarts
- **TypeScript**: Full type safety

## Quick Start

### 1. Installation

```bash
npm install lifewind-react-native-sso-client
```

Install peer dependencies:

```bash
npx expo install expo-auth-session expo-web-browser @react-native-async-storage/async-storage
```

### 2. Wrap Your App with SSOProvider

```tsx
// 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

```tsx
// 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:

```tsx
// 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`.

```tsx
<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:

```tsx
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**: `isReady` starts as `false` and becomes `true` after the initial AsyncStorage load completes. Use this to show a splash/loading screen while persisted auth state is being restored.

## Configuration

### SSOConfig

```typescript
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`:

```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/callback
```

## TypeScript Types

```typescript
interface SSOUser {
  id: string
  email: string
  name: string
  lifewind_uuid?: string
}

interface SSOResult {
  user: SSOUser
  token: string
  expires_in: number
}
```

## Authentication Flow

1. **User taps login** — `<SSOButton>` or `login()` opens an in-app browser via `expo-web-browser`
2. **User authenticates** — LifeWind redirects back to your deep link with a code
3. **Deep link captured** — Your app receives the URL via `Linking.addEventListener`
4. **Token exchange** — `handleCallback(code, state)` sends code to your backend's `/sso/validate`
5. **JWT returned** — Backend validates with LifeWind and returns JWT + user data
6. **State persisted** — Token and user stored in `AsyncStorage` and React context
7. **Ready** — `useSSO()` returns `isAuthenticated: true`, data persists across restarts

## Advanced Usage

### Navigation Guard

```tsx
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

```tsx
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

```tsx
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-browser` in most cases

**Deep link not being received:**
- Verify your `scheme` is set in `app.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 `AsyncStorage` under `auth_token`
- User data is stored under `user_data`
- Check that `@react-native-async-storage/async-storage` is properly installed

**"useSSO must be used within SSOProvider" error:**
- Make sure `<SSOProvider>` wraps your entire app (above `NavigationContainer`)

## 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](LICENSE) for details.