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

# LifeWind React SSO Client

**Complete frontend SSO authentication for React applications.**

Provider, hooks, and components for seamless OAuth 2.0 authentication with LifeWind.

## 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 Frontend")
   - **Redirect URI**: Your frontend callback URL (e.g. `https://your-app.com/sso/callback`)
   - **Grant Type**: Authorization Code
4. After creation, copy the **Client ID** — you'll need it for your frontend config

> **Tip**: For local development, use `http://localhost:5173/sso/callback` as the redirect URI (adjust port to match your Vite dev server).

## Backend Requirement

This frontend 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 frontend 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
- **useSSO Hook**: Access authentication state and actions from any component
- **Ready-to-use Components**: `<SSOButton>` and `<SSOCallback>` for quick integration
- **TypeScript**: Full type safety and IntelliSense
- **React Router**: Built-in integration with React Router v6/v7
- **Secure**: CSRF state validation, error handling, and localStorage token storage

## Quick Start

### 1. Installation

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

Peer dependencies: `react`, `react-dom`, `react-router-dom`

### 2. Wrap Your App with SSOProvider

```tsx
// main.tsx
import { BrowserRouter } from "react-router-dom"
import { SSOProvider } from "lifewind-react-sso-client"
import App from "./App"

const ssoConfig = {
  baseUrl: "https://lifewind-core.your-domain.com",
  clientId: "your_oauth_client_id",
  redirectUri: "https://your-app.com/sso/callback",
  backendApiUrl: "https://your-backend-api.com",
}

createRoot(document.getElementById("root")!).render(
  <BrowserRouter>
    <SSOProvider config={ssoConfig}>
      <App />
    </SSOProvider>
  </BrowserRouter>
)
```

### 3. Add Routes

```tsx
// App.tsx
import { Routes, Route } from "react-router-dom"
import { SSOCallback } from "lifewind-react-sso-client"
import LoginPage from "./pages/Login"
import DashboardPage from "./pages/Dashboard"

export default function App() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route
        path="/sso/callback"
        element={
          <SSOCallback
            successRedirect="/dashboard"
            errorRedirect="/login"
          />
        }
      />
      <Route path="/dashboard" element={<DashboardPage />} />
    </Routes>
  )
}
```

### 4. Add Login Button

```tsx
// pages/Login.tsx
import { SSOButton } from "lifewind-react-sso-client"

export default function LoginPage() {
  return (
    <div>
      <h1>Welcome</h1>
      <SSOButton
        buttonText="Sign in with LifeWind"
        loadingText="Redirecting..."
        onError={(err) => console.error(err)}
      />
    </div>
  )
}
```

## Components

### SSOButton

Drop-in login button that triggers the OAuth redirect.

```tsx
<SSOButton
  buttonText="Sign In"
  loadingText="Signing in..."
  className="custom-btn-class"
  onSuccess={() => console.log("Redirecting to SSO...")}
  onError={(error) => console.error(error)}
/>
```

**Props:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `buttonText` | `string` | `"Login with SSO"` | Button label |
| `loadingText` | `string` | `"Authenticating..."` | Label during redirect |
| `className` | `string` | Built-in Tailwind classes | CSS classes for the button |
| `onSuccess` | `() => void` | — | Called after redirect starts |
| `onError` | `(error: string) => void` | — | Called on error |
| `children` | `ReactNode \| (isLoading: boolean) => ReactNode` | — | Custom content or render prop |

**Render prop example:**

```tsx
<SSOButton>
  {(isLoading) => (
    <span>{isLoading ? "Please wait..." : "Login with LifeWind"}</span>
  )}
</SSOButton>
```

### SSOCallback

Handles the OAuth callback page — extracts `code` and `state` from the URL, exchanges them for a JWT, and redirects.

```tsx
<SSOCallback
  successRedirect="/dashboard"
  errorRedirect="/login"
  autoRedirect={true}
  successDelay={1500}
  errorDelay={3000}
  onSuccess={(result) => console.log("Authenticated:", result.user)}
  onError={(error) => console.error(error)}
/>
```

**Props:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `successRedirect` | `string` | `"/"` | Redirect path on success |
| `errorRedirect` | `string` | `"/login"` | Redirect path on error |
| `autoRedirect` | `boolean` | `true` | Auto-redirect after processing |
| `successDelay` | `number` | `1500` | Delay (ms) before success redirect |
| `errorDelay` | `number` | `3000` | Delay (ms) before error redirect |
| `onSuccess` | `(result: SSOResult) => void` | — | Called on success |
| `onError` | `(error: string) => void` | — | Called on error |

## useSSO Hook

Access all authentication state and actions from any component:

```tsx
import { useSSO } from "lifewind-react-sso-client"

function ProfilePage() {
  const {
    // State
    isAuthenticated, // boolean
    isLoading,       // boolean
    user,            // SSOUser | null
    token,           // string | null
    error,           // string | null

    // Actions
    login,           // () => void — redirect to OAuth
    logout,          // () => void — clear auth state
    handleCallback,  // (code, state) => Promise<SSOResult>
    refreshToken,    // () => Promise<void>
    getCurrentUser,  // () => Promise<SSOUser>
    clearError,      // () => void
  } = useSSO()

  if (!isAuthenticated) {
    return <button onClick={login}>Sign In</button>
  }

  return (
    <div>
      <h1>Welcome, {user?.name}!</h1>
      <p>{user?.email}</p>
      <button onClick={logout}>Sign Out</button>
    </div>
  )
}
```

## 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   // Frontend callback URL (e.g. "https://your-app.com/sso/callback")
  backendApiUrl: string // Your Laravel backend URL (e.g. "https://api.your-app.com")
  scopes?: string[]     // OAuth scopes (default: ["openid", "profile", "email"])
}
```

### Environment Variables

Use environment variables for different environments:

```tsx
const ssoConfig = {
  baseUrl: import.meta.env.VITE_LIFEWIND_SSO_URL,
  clientId: import.meta.env.VITE_LIFEWIND_CLIENT_ID,
  redirectUri: `${window.location.origin}/sso/callback`,
  backendApiUrl: import.meta.env.VITE_API_URL,
}
```

```env
# .env.development
VITE_LIFEWIND_SSO_URL=https://lifewind-core.test
VITE_LIFEWIND_CLIENT_ID=your_dev_client_id
VITE_API_URL=https://your-backend.test

# .env.production
VITE_LIFEWIND_SSO_URL=https://sso.lifewind.com
VITE_LIFEWIND_CLIENT_ID=your_prod_client_id
VITE_API_URL=https://api.your-domain.com
```

## 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 clicks login** — `<SSOButton>` or `login()` redirects to LifeWind OAuth
2. **User authenticates** — LifeWind redirects back to your `/sso/callback` with a code
3. **Callback processing** — `<SSOCallback>` captures `code` and `state` from URL
4. **Token exchange** — Frontend sends code to your backend's `/sso/validate` endpoint
5. **JWT returned** — Backend validates with LifeWind and returns JWT + user data
6. **State updated** — Token and user stored in `localStorage` and React context
7. **Ready** — User is authenticated, `useSSO()` returns `isAuthenticated: true`

## Advanced Usage

### Route Protection

```tsx
import { Navigate, Outlet } from "react-router-dom"
import { useSSO } from "lifewind-react-sso-client"

function AuthGuard() {
  const { isAuthenticated, isLoading } = useSSO()

  if (isLoading) return <div>Loading...</div>
  if (!isAuthenticated) return <Navigate to="/login" replace />

  return <Outlet />
}

// In your routes:
<Route element={<AuthGuard />}>
  <Route path="/dashboard" element={<DashboardPage />} />
  <Route path="/settings" element={<SettingsPage />} />
</Route>
```

### API Requests with Token

```tsx
import { useSSO } from "lifewind-react-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 }
}
```

### Token Refresh on App Load

```tsx
import { useEffect } from "react"
import { useSSO } from "lifewind-react-sso-client"

function App() {
  const { token, user, getCurrentUser } = useSSO()

  useEffect(() => {
    if (token && !user) {
      getCurrentUser().catch(() => {
        console.log("Stored token expired")
      })
    }
  }, [])

  return <Routes>{/* ... */}</Routes>
}
```

## Troubleshooting

**CORS errors:**
- Ensure your Laravel backend has `sso/*` paths in `config/cors.php`
- Check that `backendApiUrl` points to the correct backend URL

**"Invalid state parameter" error:**
- The state is stored in `sessionStorage` — make sure the callback URL matches the same origin
- Clear browser storage and try again

**Authentication loops:**
- Verify `redirectUri` in config matches the OAuth client's registered redirect URI exactly
- Check that `<SSOCallback>` route path matches `redirectUri`

**Token not persisting:**
- Token is stored in `localStorage` under `auth_token`
- User data is stored under `user_data`
- Check browser DevTools > Application > Local Storage

## Bundle Size

- **ESM**: ~15 KB (gzip: ~3.7 KB)
- **UMD**: ~17 KB (gzip: ~4.0 KB)
- Tree-shakable exports

## License

MIT License. See [LICENSE](LICENSE) for details.