# @edgespark/client

Browser SDK for EdgeSpark BaaS - authentication + API client in one package.

## Install

```bash
npm i @edgespark/client
```

> **Note**: This SDK is browser-only. It does not support Node.js or SSR environments.

## Quick Start

```ts
import { createEdgeSpark } from "@edgespark/client";
import "@edgespark/client/styles.css";

const client = createEdgeSpark({ baseUrl: "https://api.example.com" });

// Option 1: Use managed login UI (recommended)
await client.auth.renderAuthUI(document.getElementById("auth")!, {
  redirectTo: "/dashboard",
  onLogin: (user) => console.log("Logged in!", user.email),
});

// Option 2: Build custom UI with direct API calls
await client.auth.signIn.email({ email, password });
const session = await client.auth.getSession();

// API requests (auth token auto-injected)
const res = await client.api.fetch("/api/users");
```

---

## Managed Login UI

`renderAuthUI` provides a complete out-of-the-box authentication flow:

| Flow | Description |
|------|-------------|
| **Sign In** | Email/password login, OAuth (Google, etc.), anonymous temporary account |
| **Sign Up** | Email/password registration (handles email verification automatically) |
| **Forgot Password** | Send reset code → Enter code → Set new password |
| **Email Verification** | Send verification email after signup, supports resend |

### Container Requirements

The managed login UI is optimized for these container dimensions:

| Dimension | Value | Notes |
|-----------|-------|-------|
| **Optimal width** | `420px` | Matches `--edgespark-auth-card-width` |
| **Minimum width** | `320px` | Mobile devices |
| **Responsive** | `width: 100%; max-width: 420px` | Recommended |

```html
<div id="auth" style="width: 100%; max-width: 420px; margin: 0 auto;"></div>
```

### Basic Usage

```ts
import { createEdgeSpark } from "@edgespark/client";
import "@edgespark/client/styles.css";  // Required

const client = createEdgeSpark({ baseUrl: "https://api.example.com" });

// Check if already logged in
const session = await client.auth.getSession();
if (session.data?.user) {
  window.location.href = "/dashboard";
} else {
  // Render login UI
  await client.auth.renderAuthUI(document.getElementById("auth")!, {
    redirectTo: "/dashboard",
    onError: (error) => console.error("Auth error:", error),
  });
}
```

### React Example

```tsx
import { useRef, useEffect } from "react";
import { createEdgeSpark } from "@edgespark/client";
import "@edgespark/client/styles.css";

// Create client outside component (singleton)
const client = createEdgeSpark({ baseUrl: "https://api.example.com" });

export function AuthPage() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!ref.current) return;

    // Check login status first
    client.auth.getSession().then((session) => {
      if (session.data?.user) {
        window.location.href = "/dashboard";
        return;
      }
      client.auth.renderAuthUI(ref.current!, {
        redirectTo: "/dashboard",
      });
    });
  }, []);

  return <div ref={ref} />;
}

// Dashboard page - get user info here
export function DashboardPage() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    client.auth.getSession().then((session) => {
      if (!session.data?.user) {
        window.location.href = "/login";
        return;
      }
      setUser(session.data.user);
    });
  }, []);

  if (!user) return <div>Loading...</div>;
  return <div>Welcome, {user.name}!</div>;
}
```

### Custom Labels

Override default labels via the `labels` parameter (partial override supported):

```ts
await client.auth.renderAuthUI(container, {
  labels: {
    // Common labels
    common: {
      continueButton: "Continue",
      backButton: "Back",
      unknownError: "Something went wrong, please try again",
    },
    // Sign in page
    signIn: {
      title: "Welcome Back",
      subtitle: "Sign in to continue",
      emailPlaceholder: "Enter your email",
      passwordPlaceholder: "Enter your password",
      loginButton: "Sign In",
      forgotPassword: "Forgot password?",
      signUpPrompt: "Don't have an account?",
      toggleToSignUp: "Sign up now",
      guestPrompt: "Not ready to sign up?",
      guestLinkText: "Continue with temporary account",
    },
    // Sign up page
    signUp: {
      title: "Create Account",
      subtitle: "Sign up to get started",
      emailPlaceholder: "Enter your email",
      passwordPlaceholder: "Enter your password",
      confirmPasswordPlaceholder: "Confirm your password",
      signUpButton: "Sign Up",
      toggleToSignIn: "Already have an account?",
      toggleToSignInLink: "Sign in now",
    },
    // Verification code page
    verifyCode: {
      title: "Enter Verification Code",
      sentTo: "Code sent to",
      resendCode: "Resend",
      resendCountdown: "Resend in {seconds}s",  // {seconds} is a placeholder
    },
    // Reset password
    resetPassword: {
      title: "Reset Password",
      subtitle: "Enter your email to receive a reset code",
      sendResetButton: "Send Reset Code",
      backToSignIn: "Back to sign in",
    },
    // Set new password
    setNewPassword: {
      title: "Set New Password",
      subtitle: "Code sent to",
      newPasswordLabel: "New password",
      confirmPasswordLabel: "Confirm password",
      setPasswordButton: "Set Password",
    },
    // OAuth buttons
    oauth: {
      googleBtnName: "Continue with Google",
      githubBtnName: "Continue with GitHub",
    },
    // Error messages (override backend error codes)
    errors: {
      INVALID_EMAIL_OR_PASSWORD: "Invalid email or password",
      INVALID_EMAIL: "Invalid email format",
      PASSWORD_MISMATCH: "Passwords do not match",
      PASSWORD_TOO_SHORT: "Password is too short",
      USER_ALREADY_EXISTS: "This email is already registered",
      EMAIL_NOT_VERIFIED: "Email not verified, please check your inbox",
      INVALID_OTP: "Invalid verification code",
      OTP_EXPIRED: "Verification code has expired",
      TOO_MANY_ATTEMPTS: "Too many attempts, please try again later",
    },
    // Success page
    success: {
      title: "Success!",
      continueButton: "Continue",
    },
    // iframe OAuth modal (for preview page limitations)
    iframeModal: {
      titleTemplate: "Sign in with {provider}",  // {provider} is a placeholder
      messageTemplate: "Due to preview limitations, {provider} sign-in must be completed in a new tab.",
      actionButton: "Open in New Tab",
    },
  },
});
```

### Custom Styles

All styles are scoped under `[data-edgespark-auth]`. Import the CSS and override via CSS variables:

```css
/* In your CSS file */
[data-edgespark-auth] {
  /* Colors */
  --edgespark-auth-color-primary: rgba(0, 0, 0, 0.95);
  --edgespark-auth-color-secondary: rgba(0, 0, 0, 0.65);
  --edgespark-auth-color-tertiary: rgba(0, 0, 0, 0.45);
  --edgespark-auth-color-text: rgba(0, 0, 0, 0.95);
  --edgespark-auth-color-text-inverse: #ffffff;
  --edgespark-auth-color-error: #dc2626;
  --edgespark-auth-color-border: rgba(0, 0, 0, 0.12);
  --edgespark-auth-color-bg: #ffffff;
  --edgespark-auth-color-bg-secondary: rgba(0, 0, 0, 0.05);
  --edgespark-auth-color-bg-link: #0077ff;

  /* Typography */
  --edgespark-auth-font-family: 'Roboto Flex', -apple-system, sans-serif;
  --edgespark-auth-font-size-title: 32px;
  --edgespark-auth-font-size-body: 14px;
  --edgespark-auth-font-size-small: 12px;

  /* Sizing & Border Radius */
  --edgespark-auth-radius-card: 30px;
  --edgespark-auth-radius-input: 12px;
  --edgespark-auth-radius-button: 999px;
  --edgespark-auth-spacing: 16px;
  --edgespark-auth-input-height: 48px;
  --edgespark-auth-button-height: 48px;
  --edgespark-auth-card-width: 420px;
}
```

---

## Custom UI

If the managed UI doesn't meet your needs, call `client.auth` APIs directly to build your own UI.

`client.auth` is based on [better-auth](https://www.better-auth.com/) client and supports all better-auth methods.

### Sign In

```ts
// Email/password sign in
const result = await client.auth.signIn.email({
  email: "user@example.com",
  password: "password",
});

if (result.error) {
  console.error("Sign in failed:", result.error.message);
} else {
  console.log("Sign in successful:", result.data);
}

// OAuth sign in (redirects to third-party page)
await client.auth.signIn.social({
  provider: "google",
  callbackURL: "/dashboard",  // Redirect after success
});

// Anonymous temporary account (requires backend support)
await client.auth.signIn.anonymous();
```

### Sign Up

```ts
const result = await client.auth.signUp.email({
  name: "John Doe",
  email: "john@example.com",
  password: "password",
  callbackURL: "/verify-email",  // Link destination in verification email
});

if (result.error) {
  console.error("Sign up failed:", result.error.message);
}
```

### Get User Info

```ts
// Get current logged-in user's session
const session = await client.auth.getSession();

if (session.data) {
  console.log("User info:", session.data.user);
  console.log("Session:", session.data.session);
} else {
  console.log("Not logged in");
}
```

### Sign Out

```ts
await client.auth.signOut();
```

### Forgot Password / Reset Password

```ts
// 1. Send reset code to email
await client.auth.forgetPassword.emailOtp({
  email: "user@example.com",
});

// 2. Set new password with verification code
const result = await client.auth.emailOtp.resetPassword({
  email: "user@example.com",
  otp: "123456",
  password: "newpassword",
});

if (result.error) {
  console.error("Reset failed:", result.error.message);
}
```

### Listen to Auth State Changes

```ts
// Listen to auth state changes (login/logout/token refresh)
const unsubscribe = client.auth.onAuthStateChange(() => {
  // Get latest session when state changes
  client.auth.getSession().then((session) => {
    if (session?.data?.user) {
      console.log("Logged in", session.data.user);
    } else {
      console.log("Logged out");
    }
  });
});

// Unsubscribe
unsubscribe();
```

---

## API Module

`client.api.fetch` is `fetch` with auto-injected auth token:

```ts
// GET
const res = await client.api.fetch("/api/users");
const users = await res.json();

// POST
const res = await client.api.fetch("/api/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Hello" }),
});
```

> **Security Note**: Token is only injected for same-origin requests (same domain as `baseUrl`). Cross-origin requests will not include the token automatically.

---

## Lifecycle & Best Practices

### Check Login Status Before Rendering

Always check if user is already logged in before rendering the login UI:

```ts
const session = await client.auth.getSession();
if (session.data?.user) {
  // Already logged in, redirect to dashboard
  window.location.href = "/dashboard";
  return;
}

// Not logged in, render login UI
await client.auth.renderAuthUI(container, {
  redirectTo: "/dashboard",
});
```

### Get User Info on Target Page

After redirect, use `getSession()` on the target page - this is the **recommended** way to get user info:

```ts
// /dashboard page
const session = await client.auth.getSession();
if (!session.data?.user) {
  window.location.href = "/login";
  return;
}
console.log("Welcome", session.data.user.name);
```

### onLogin vs getSession vs onAuthStateChange

| API | Use Case | Notes |
|-----|----------|-------|
| `getSession()` | **Recommended**: Get current user info | Works everywhere, async |
| `onLogin` | Quick sync ops (analytics, logging) | Not called for OAuth; fires before redirect |
| `onAuthStateChange` | Advanced: real-time auth monitoring | Need manual unsubscribe; fires on any token change |

### React Best Practice

```tsx
// ✅ Create client outside component (singleton)
const client = createEdgeSpark({ baseUrl: "..." });

function AuthPage() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!ref.current) return;

    // Check if already logged in
    client.auth.getSession().then((session) => {
      if (session.data?.user) {
        window.location.href = "/dashboard";
        return;
      }
      // Render login UI
      client.auth.renderAuthUI(ref.current!, {
      redirectTo: "/dashboard",
      });
    });

    // No need to destroy - renderAuthUI auto-cleans previous UI
  }, []);

  return <div ref={ref} />;
}
```

### Cleanup

```ts
// Destroy client (cleanup UI, event listeners, etc.)
client.destroy();

// Also clear locally stored token
client.destroy({ clearToken: true });
```

> **Note**: Only call `destroy()` when the entire app unmounts, not on component unmount.

---

## API Reference

### `createEdgeSpark(options)`

Creates an EdgeSpark client. **Initialization is synchronous.**

| Option | Type | Description |
|--------|------|-------------|
| `baseUrl` | `string` | Backend URL (required) |
| `fetchCredentials` | `RequestCredentials` | Fetch credentials mode, default `"include"` |

### `client.auth`

Authentication client based on better-auth. Main methods:

| Method | Description |
|--------|-------------|
| `signIn.email({ email, password })` | Email/password sign in |
| `signIn.social({ provider, callbackURL })` | OAuth sign in |
| `signIn.anonymous()` | Anonymous sign in |
| `signUp.email({ name, email, password })` | Email sign up |
| `signOut()` | Sign out |
| `getSession()` | Get current session/user info |
| `forgetPassword.emailOtp({ email })` | Send password reset code |
| `emailOtp.resetPassword({ email, otp, password })` | Reset password with code |
| `renderAuthUI(container, options)` | Render managed login UI |
| `onAuthStateChange(listener)` | Listen to token changes |

### `client.api`

| Method | Description |
|--------|-------------|
| `fetch(path, options?)` | Fetch with auth token |

### `LoginUIOptions`

Options for `renderAuthUI`:

| Option | Type | Description |
|--------|------|-------------|
| `redirectTo` | `string` | Redirect URL after login (simple mode) |
| `redirects` | `RedirectOptions` | Fine-grained redirect config (overrides `redirectTo`) |
| `labels` | `Partial<ExtendedLabels>` | Custom labels |
| `onLogin` | `(user) => void` | Optional: for quick sync ops like analytics (not called for OAuth) |
| `onError` | `(error) => void` | Error callback |

### `RedirectOptions`

```ts
interface RedirectOptions {
  oauth?: {
    success?: string;   // OAuth login success
    newUser?: string;   // OAuth first-time signup
    error?: string;     // OAuth error
  };
  emailPassword?: string;     // Email/password login success
  emailVerification?: string; // Email verification link destination
  anonymous?: string;         // Anonymous login success
}
```

---

## Build

```bash
npm run build      # Build
npm run dev        # Run example (examples/dev)
```
