npm package: @fluid-app/portal-sdk
Description: SDK for building custom Fluid portals
Latest version: 0.1.134

# @fluid-app/portal-sdk

SDK for building custom Fluid portals. Provides React hooks, providers, and an API client for integrating with the Fluid Commerce platform.

## Installation

```bash
npm install @fluid-app/portal-sdk
# or
pnpm add @fluid-app/portal-sdk
# or
bun add @fluid-app/portal-sdk
```

### Peer Dependencies

This package requires the following peer dependencies:

```json
{
  "react": ">=18.0.0",
  "@tanstack/react-query": ">=5.0.0"
}
```

## Quick Start

Wrap your application with `FluidProvider`:

```tsx
import { FluidProvider } from "@fluid-app/portal-sdk";

function App() {
  return (
    <FluidProvider
      config={{
        baseUrl: "https://api.fluid.app/api",
        getAuthToken: () => localStorage.getItem("fluid_token"),
        onAuthError: () => {
          // Handle 401 errors (e.g., redirect to login)
          window.location.href = "/login";
        },
      }}
    >
      <YourApp />
    </FluidProvider>
  );
}
```

## Hooks

### useFluidApi

Access the API client for making requests:

```tsx
import { useFluidApi } from "@fluid-app/portal-sdk";
import { useQuery } from "@tanstack/react-query";

function ProductList() {
  const api = useFluidApi();

  const { data: products, isLoading } = useQuery({
    queryKey: ["products"],
    queryFn: () => api.products.list({ active: true }),
  });

  if (isLoading) return <Spinner />;

  return (
    <ul>
      {products?.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}
```

### useCurrentRep

Fetch the currently authenticated rep's profile:

```tsx
import { useCurrentRep } from "@fluid-app/portal-sdk";

function RepHeader() {
  const { data: rep, isLoading } = useCurrentRep();

  if (isLoading) return <Skeleton />;

  return (
    <div>
      <Avatar src={rep?.avatar_url} />
      <span>
        {rep?.first_name} {rep?.last_name}
      </span>
    </div>
  );
}
```

### useFluidProfile

Fetch the portal profile (themes, navigation, screens):

```tsx
import { useFluidProfile } from "@fluid-app/portal-sdk";

function Navigation() {
  const { data: profile, isLoading } = useFluidProfile();

  if (isLoading) return <Spinner />;

  return (
    <nav>
      {profile?.navigation.navigation_items.map((item) => (
        <NavItem key={item.id} item={item} />
      ))}
    </nav>
  );
}
```

### useFluidPermissions

Check user permissions with a convenient `can()` helper:

```tsx
import { useFluidPermissions } from "@fluid-app/portal-sdk";

function TeamSettings() {
  const { can, isSuperAdmin } = useFluidPermissions();

  // Check if user can manage team
  if (!can("team", "manage")) {
    return <AccessDenied />;
  }

  return (
    <div>
      <h1>Team Settings</h1>
      {/* Only show delete button if user has delete permission */}
      {can("team", "delete") && <DeleteTeamButton />}
    </div>
  );
}
```

Available permission actions: `"view"`, `"create"`, `"update"`, `"delete"`, `"settings"`, `"add"`, `"manage"`, `"send"`

### useFluidTheme

Control theme settings:

```tsx
import { useFluidTheme } from "@fluid-app/portal-sdk";
import type { Theme } from "@fluid-app/portal-sdk";

function ThemeSwitcher({ themes }: { themes: Theme[] }) {
  const { currentTheme, setTheme, setThemeMode, mode } = useFluidTheme();

  return (
    <div>
      <select
        value={currentTheme?.name}
        onChange={(e) => {
          const theme = themes.find((t) => t.name === e.target.value);
          if (theme) setTheme(theme);
        }}
      >
        {themes.map((theme) => (
          <option key={theme.name} value={theme.name}>
            {theme.name}
          </option>
        ))}
      </select>

      <button onClick={() => setThemeMode(mode === "dark" ? "light" : "dark")}>
        Toggle {mode === "dark" ? "Light" : "Dark"} Mode
      </button>
    </div>
  );
}
```

## API Client

The SDK provides a typed API client with methods for common operations:

### Products

```tsx
const api = useFluidApi();

// List products with optional filters
const products = await api.products.list({
  active: true,
  page: 1,
  per_page: 20,
});

// Get a single product
const product = await api.products.get("product-id");

// Search products
const results = await api.products.search("query");
```

### Orders

```tsx
const api = useFluidApi();

// List orders
const orders = await api.orders.list({
  status: "pending",
  date_from: "2024-01-01",
});

// Get a single order
const order = await api.orders.get("order-id");

// Create an order
const newOrder = await api.orders.create({
  customer_id: "customer-id",
  line_items: [{ product_id: "product-id", quantity: 2 }],
});
```

### Reps

```tsx
const api = useFluidApi();

// Get current rep profile
const rep = await api.reps.current();

// Update profile
const updated = await api.reps.updateProfile({
  first_name: "John",
  last_name: "Doe",
});
```

### Analytics

```tsx
const api = useFluidApi();

// Get dashboard data
const dashboard = await api.analytics.dashboard();

// Get sales data
const sales = await api.analytics.sales({
  date_from: "2024-01-01",
  date_to: "2024-12-31",
  group_by: "month",
});
```

### Custom Requests

For endpoints not covered by the typed methods:

```tsx
const api = useFluidApi();

// Generic GET request
const data = await api.get<MyType>("/custom/endpoint", { param: "value" });

// Generic POST request
const result = await api.post<MyType>("/custom/endpoint", { body: "data" });
```

## Providers

### FluidProvider

The main provider that sets up the SDK. It wraps:

- `QueryClientProvider` (TanStack Query)
- `FluidThemeProvider` (theme management)

```tsx
import { FluidProvider } from "@fluid-app/portal-sdk";
import { QueryClient } from "@tanstack/react-query";

// Optional: provide your own QueryClient
const queryClient = new QueryClient({
  defaultOptions: {
    queries: { staleTime: 5 * 60 * 1000 },
  },
});

function App() {
  return (
    <FluidProvider
      config={{
        baseUrl: "https://api.fluid.app/api",
        getAuthToken: () => getToken(),
      }}
      queryClient={queryClient}
      initialTheme={defaultTheme}
    >
      <YourApp />
    </FluidProvider>
  );
}
```

### FluidThemeProvider

Can be used standalone if you need theme management without the full SDK:

```tsx
import { FluidThemeProvider, useFluidTheme } from "@fluid-app/portal-sdk";

function App() {
  return (
    <FluidThemeProvider initialTheme={myTheme}>
      <ThemedContent />
    </FluidThemeProvider>
  );
}
```

Theme CSS variables are applied to the document root (or a custom container) with the `--fluid-` prefix.

## Types

All types are exported for use in your application:

```tsx
import type {
  // Core types
  Profile,
  Theme,
  ThemeConfig,
  Navigation,
  NavigationItem,
  ScreenDefinition,

  // Permissions
  UserPermissions,
  Permissions,
  PermissionAction,
  ResourcePermissions,

  // Rep
  Rep,
  UpdateRepData,

  // API types
  Product,
  ProductListParams,
  Order,
  OrderListParams,
  CreateOrderData,
  DashboardData,
  SalesData,
  SalesParams,

  // Client types
  FluidSDKConfig,
  FluidClient,
  RequestOptions,
} from "@fluid-app/portal-sdk";
```

## Query Keys

Query keys are exported for cache invalidation and prefetching:

```tsx
import {
  PROFILE_QUERY_KEY,
  PERMISSIONS_QUERY_KEY,
  CURRENT_REP_QUERY_KEY,
} from "@fluid-app/portal-sdk";
import { useQueryClient } from "@tanstack/react-query";

function RefreshButton() {
  const queryClient = useQueryClient();

  const handleRefresh = () => {
    queryClient.invalidateQueries({ queryKey: PROFILE_QUERY_KEY });
  };

  return <button onClick={handleRefresh}>Refresh Profile</button>;
}
```

## Error Handling

The SDK provides an `ApiError` class for structured error handling:

```tsx
import { ApiError, isApiError } from "@fluid-app/portal-sdk";

try {
  await api.orders.create(orderData);
} catch (error) {
  if (isApiError(error)) {
    console.log("Status:", error.status);
    console.log("Message:", error.message);
    console.log("Data:", error.data); // Server error details
  }
}
```

## License

Private - Fluid Commerce