npm package: @fdback.io/sdk
Description: Collect user feedback, build roadmaps, and share changelogs - SDK for Fdback.io
Author: Fdback
Latest version: 0.6.0

# @fdback.io/sdk

Official TypeScript SDK for integrating [Fdback](https://fdback.io) user feedback into your application.

## Features

- **Type-safe** - Full TypeScript support with detailed type definitions
- **Isomorphic** - Works in Node.js and browsers
- **Secure** - HMAC-SHA256 signed authentication
- **Framework support** - First-class React and Next.js integrations
- **Lightweight** - ~3.7KB core, ~5KB for framework bindings

## Installation

```bash
npm install @fdback.io/sdk
# or
pnpm add @fdback.io/sdk
# or
yarn add @fdback.io/sdk
```

## Quick Start

### 1. Get your credentials

Get your **Workspace ID** and **Workspace Secret** from your [Fdback dashboard](https://app.fdback.io) under Settings > SDK Credentials.

### 2. Server-side: Sign user data

```typescript
import { FdbackServer } from "@fdback.io/sdk";

const fdback = new FdbackServer({
  workspaceId: process.env.FDBACK_WORKSPACE_ID!,
  workspaceSecret: process.env.FDBACK_WORKSPACE_SECRET!,
});

// Create signed login data for a user
const signedData = await fdback.createSignedLoginData({
  email: "user@example.com",
  name: "John Doe",
  avatar: "https://example.com/avatar.jpg", // optional
});
```

### 3. Client-side: Open feedback board

```typescript
import { FdbackClient } from "@fdback.io/sdk";

const fdback = new FdbackClient({
  workspaceId: "your-workspace-id",
});

// Open with signed data from your server
fdback.open(signedData, { mode: "popup" });
```

> **Note**: `FdbackServer` and `FdbackClient` are aliases for the same `Fdback` class. The naming makes it clearer which environment you're in.

---

## Security Model

The workspace secret is used to sign requests with HMAC-SHA256. **Never expose the secret in client-side code.**

| Environment | Approach |
|-------------|----------|
| Server-side | Use `workspaceSecret` directly |
| Client-side | Fetch signed data from your backend API |

---

## Core SDK

### `Fdback` Class

```typescript
import { Fdback } from "@fdback.io/sdk";

const fdback = new Fdback({
  workspaceId: string;       // Required
  workspaceSecret?: string;  // Server-side only
  baseUrl?: string;          // Default: "https://app.fdback.io"
});
```

#### Methods

| Method | Environment | Description |
|--------|-------------|-------------|
| `createSignedLoginData(user)` | Server | Create signed auth data for a user |
| `getLoginUrl(user)` | Server | Get a signed login URL for redirects |
| `open(signedData, options?)` | Browser | Open feedback board in popup/tab |
| `openDirect(user, options?)` | Browser | Sign and open (requires secret - not recommended) |
| `getBoardUrl()` | Both | Get the public board URL |
| `isServerMode()` | Both | Check if secret is configured |

### Types

```typescript
interface FdbackUser {
  email: string;      // Required
  name?: string;      // Display name
  avatar?: string;    // Avatar URL
}

interface SignedLoginData {
  url: string;
  headers: {
    "x-workspace-id": string;
    "x-timestamp": string;
    "x-signature": string;
  };
  body: string;
  expiresAt: number;  // Expires in 5 minutes
}

interface OpenOptions {
  mode?: "popup" | "tab" | "redirect";  // Default: "popup"
  width?: number;   // Default: 800
  height?: number;  // Default: 700
}
```

---

## React Integration

```bash
npm install @fdback.io/sdk react
```

### Setup Provider

```tsx
// app/providers.tsx or app/layout.tsx
import { FdbackProvider } from "@fdback.io/sdk/react";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <FdbackProvider
      workspaceId="your-workspace-id"
      signEndpoint="/api/fdback/sign"
    >
      {children}
    </FdbackProvider>
  );
}
```

### FeedbackButton Component

The easiest way to add feedback - a ready-to-use button:

```tsx
"use client";
import { FeedbackButton } from "@fdback.io/sdk/react";

export function Header() {
  const user = useCurrentUser(); // Your auth hook

  return (
    <FeedbackButton
      user={{ email: user.email, name: user.name }}
      className="btn btn-primary"
      onOpen={(win) => console.log("Opened!", win)}
      onError={(err) => console.error(err)}
    >
      Give Feedback
    </FeedbackButton>
  );
}
```

### useFdbackLogin Hook

For custom implementations:

```tsx
"use client";
import { useFdbackLogin } from "@fdback.io/sdk/react";

export function CustomFeedback() {
  const { login, isLoading, error } = useFdbackLogin();

  const handleClick = async () => {
    await login(
      { email: "user@example.com", name: "John" },
      { mode: "popup" }
    );
  };

  return (
    <button onClick={handleClick} disabled={isLoading}>
      {isLoading ? "Opening..." : "Feedback"}
    </button>
  );
}
```

### useFdback Hook

Access the Fdback context directly:

```tsx
"use client";
import { useFdback } from "@fdback.io/sdk/react";

export function FeedbackStatus() {
  const { isOpen, getBoardUrl, workspaceId } = useFdback();

  return (
    <div>
      <p>Workspace: {workspaceId}</p>
      <p>Board URL: {getBoardUrl()}</p>
      <p>Popup open: {isOpen ? "Yes" : "No"}</p>
    </div>
  );
}
```

### FdbackWidget Component

Embed the full widget (feedback, roadmap, changelog tabs) directly in your React app:

```tsx
"use client";
import { FdbackWidget } from "@fdback.io/sdk/react";

// Uncontrolled - uses default floating launcher button
export function App() {
  return (
    <FdbackWidget
      workspaceId="your-workspace-id"
      mode="full"
      tabs={["feedback", "roadmap", "changelog"]}
      theme="dark"  // or "light" or "auto"
      primaryColor="#6366f1"
    />
  );
}

// Controlled - use your own trigger button
export function CustomTrigger() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open Feedback</button>
      <FdbackWidget
        workspaceId="your-workspace-id"
        open={open}
        onOpenChange={setOpen}
        showLauncher={false}
      />
    </>
  );
}
```

#### FdbackWidget Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `workspaceId` | `string` | required | Your workspace ID |
| `mode` | `"simple" \| "full"` | `"simple"` | Simple = feedback only, Full = tabs |
| `tabs` | `("feedback" \| "roadmap" \| "changelog")[]` | `["feedback"]` | Tabs to show in full mode |
| `theme` | `"auto" \| "dark" \| "light"` | `"auto"` | Theme mode |
| `primaryColor` | `string` | - | Hex color for accent |
| `launcherIcon` | `string` | - | Custom icon: image URL, inline SVG, or HTML |
| `position` | `"bottom-right" \| "bottom-left" \| "top-right" \| "top-left"` | `"bottom-right"` | Launcher position |
| `open` | `boolean` | - | Controlled open state |
| `onOpenChange` | `(open: boolean) => void` | - | Open state change callback |
| `showLauncher` | `boolean` | `true` | Show the floating launcher button |
| `signEndpoint` | `string` | - | Auth endpoint URL |

### React API Reference

| Export | Type | Description |
|--------|------|-------------|
| `FdbackProvider` | Component | Context provider |
| `FdbackWidget` | Component | Embeddable widget with tabs |
| `FeedbackButton` | Component | Ready-to-use button |
| `useFdback` | Hook | Access context (client, isOpen, etc.) |
| `useFdbackLogin` | Hook | Login + open with loading/error state |
| `useWidget` | Hook | Programmatic widget control |

---

## Next.js Integration

```bash
npm install @fdback.io/sdk server-only
```

The `/next` subpath includes `server-only` protection to prevent accidental client-side imports.

### Option 1: API Route Handler

Create a route that handles signing:

```typescript
// app/api/fdback/sign/route.ts
import { createSignHandler } from "@fdback.io/sdk/next";

export const POST = createSignHandler({
  workspaceId: process.env.FDBACK_WORKSPACE_ID!,
  workspaceSecret: process.env.FDBACK_WORKSPACE_SECRET!,
});
```

With authentication:

```typescript
// app/api/fdback/sign/route.ts
import { createSignHandler } from "@fdback.io/sdk/next";
import { auth } from "@/server/auth";

export const POST = createSignHandler({
  workspaceId: process.env.FDBACK_WORKSPACE_ID!,
  workspaceSecret: process.env.FDBACK_WORKSPACE_SECRET!,
  authorize: async () => {
    const session = await auth();
    return !!session?.user;
  },
  getUser: async () => {
    const session = await auth();
    if (!session?.user?.email) return null;
    return {
      email: session.user.email,
      name: session.user.name ?? undefined,
      avatar: session.user.image ?? undefined,
    };
  },
});
```

### Option 2: Server Action

Create a server action for use with React Server Components:

```typescript
// lib/fdback.ts
"use server";
import { createSignAction } from "@fdback.io/sdk/next";
import { auth } from "@/server/auth";

const _signAction = createSignAction({
  workspaceId: process.env.FDBACK_WORKSPACE_ID!,
  workspaceSecret: process.env.FDBACK_WORKSPACE_SECRET!,
});

// Wrap with auth check
export async function signFdbackLogin() {
  const session = await auth();
  if (!session?.user?.email) {
    return { success: false as const, error: "Unauthorized" };
  }
  return _signAction({
    email: session.user.email,
    name: session.user.name ?? undefined,
    avatar: session.user.image ?? undefined,
  });
}
```

Use in client component:

```tsx
"use client";
import { signFdbackLogin } from "@/lib/fdback";
import { useFdback } from "@fdback.io/sdk/react";

export function FeedbackButton() {
  const { open } = useFdback();

  const handleClick = async () => {
    const result = await signFdbackLogin();
    if (result.success) {
      open(result.data);
    } else {
      console.error(result.error);
    }
  };

  return <button onClick={handleClick}>Feedback</button>;
}
```

### Next.js API Reference

| Export | Description |
|--------|-------------|
| `createSignHandler(options)` | Creates a POST route handler |
| `createSignAction(options)` | Creates a server action factory |

#### SignHandlerOptions

```typescript
interface SignHandlerOptions {
  workspaceId: string;
  workspaceSecret: string;
  baseUrl?: string;
  authorize?: (request: Request) => Promise<boolean> | boolean;
  getUser?: (request: Request) => Promise<FdbackUser | null> | FdbackUser | null;
}
```

#### SignActionOptions

```typescript
interface SignActionOptions {
  workspaceId: string;
  workspaceSecret: string;
  baseUrl?: string;
}
```

---

## CDN Usage

For non-bundled usage, include via CDN:

```html
<script src="https://unpkg.com/@fdback.io/sdk"></script>
<script>
  const fdback = new FdbackSDK.Fdback({
    workspaceId: "your-workspace-id",
  });

  // Use with pre-signed data from your server
  fetch("/api/fdback/sign", { method: "POST" })
    .then((r) => r.json())
    .then((signedData) => {
      fdback.open(signedData, { mode: "popup" });
    });
</script>
```

---

## Complete Next.js Example

### 1. Environment variables

```env
FDBACK_WORKSPACE_ID=your-workspace-id
FDBACK_WORKSPACE_SECRET=your-workspace-secret
```

### 2. API route

```typescript
// app/api/fdback/sign/route.ts
import { createSignHandler } from "@fdback.io/sdk/next";
import { auth } from "@/server/auth";

export const POST = createSignHandler({
  workspaceId: process.env.FDBACK_WORKSPACE_ID!,
  workspaceSecret: process.env.FDBACK_WORKSPACE_SECRET!,
  authorize: async () => !!(await auth())?.user,
  getUser: async () => {
    const session = await auth();
    if (!session?.user?.email) return null;
    return {
      email: session.user.email,
      name: session.user.name ?? undefined,
    };
  },
});
```

### 3. Provider setup

```tsx
// app/layout.tsx
import { FdbackProvider } from "@fdback.io/sdk/react";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <FdbackProvider
          workspaceId={process.env.NEXT_PUBLIC_FDBACK_WORKSPACE_ID!}
          signEndpoint="/api/fdback/sign"
        >
          {children}
        </FdbackProvider>
      </body>
    </html>
  );
}
```

### 4. Feedback button

```tsx
// components/feedback-button.tsx
"use client";
import { FeedbackButton } from "@fdback.io/sdk/react";
import { useSession } from "next-auth/react";

export function AppFeedbackButton() {
  const { data: session } = useSession();

  if (!session?.user?.email) return null;

  return (
    <FeedbackButton
      user={{
        email: session.user.email,
        name: session.user.name ?? undefined,
        avatar: session.user.image ?? undefined,
      }}
      className="fixed bottom-4 right-4 rounded-full bg-primary px-4 py-2 text-white"
    >
      Feedback
    </FeedbackButton>
  );
}
```

---

## Embedded Widget

The SDK includes an embeddable widget that can be integrated directly into your application. The widget provides a floating button and panel for collecting feedback.

### Basic Usage

```html
<script src="https://unpkg.com/@fdback.io/sdk"></script>
<script>
  const widget = FdbackSDK.widget.init({
    workspaceId: "your-workspace-id",
  });
</script>
```

### With User Authentication

For identified users, provide a `signEndpoint` that authenticates users via HMAC-signed data:

```html
<script src="https://unpkg.com/@fdback.io/sdk"></script>
<script>
  const widget = FdbackSDK.widget.init({
    workspaceId: "your-workspace-id",
    signEndpoint: "/api/fdback/sign",
    onAuthenticated: (user) => {
      console.log("User authenticated:", user);
    },
  });
</script>
```

The `signEndpoint` should return signed user data from your server. See [Next.js Integration](#nextjs-integration) for creating sign endpoints.

### Configuration Options

```typescript
interface WidgetConfig {
  workspaceId: string;                    // Required
  mode?: "simple" | "full";               // "simple" = feedback only, "full" = tabs
  position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  tabs?: ("feedback" | "roadmap" | "changelog")[];
  primaryColor?: string;                  // Hex color for theming
  launcherIcon?: string;                  // Custom icon: image URL, inline SVG, or HTML
  theme?: "auto" | "dark" | "light";      // Theme mode (default: "auto")
  width?: number;                         // Widget width in pixels (default: 440, min: 320, max: 600)
  height?: number;                        // Widget height in pixels (default: 680, min: 400, max: 900)
  signEndpoint?: string;                  // URL for HMAC-signed authentication
  onOpen?: () => void;
  onClose?: () => void;
  onFeedbackSubmitted?: (feedback: { id: string }) => void;
  onTabChange?: (tab: string) => void;
  onAuthenticated?: (user: { id: string; email: string; name?: string }) => void;
}
```

### Widget Methods

```typescript
const widget = FdbackSDK.widget.init(config);

widget.open();                            // Open the widget
widget.close();                           // Close the widget
widget.toggle();                          // Toggle open/closed
widget.setTab("roadmap");                 // Switch tab (full mode)
widget.setUser({ email: "...", name: "..." });  // Update user data
widget.destroy();                         // Remove widget from page
widget.isOpen;                            // Current state (boolean)
```

### Authentication Flow

When `signEndpoint` is configured:

1. Widget initializes and calls your `signEndpoint` with cookies
2. Your server reads the user session and returns HMAC-signed data
3. Widget sends signed data to fdback.io for verification
4. User is authenticated and feedback is attributed to them

```
┌─────────────────────────────────────────────────────────────┐
│  YOUR APP (acme.com)                                        │
│  1. widget.init({ signEndpoint: '/api/fdback/sign' })       │
│  2. SDK calls signEndpoint (with cookies for session)       │
│  3. Server returns HMAC-signed user data                    │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  FDBACK WIDGET (fdback.io)                                  │
│  4. Validates signature with workspace secret               │
│  5. Creates/updates user, sets session cookie               │
│  6. User is now authenticated                               │
└─────────────────────────────────────────────────────────────┘
```

---

## Troubleshooting

### "Signed login data has expired"

The signed data expires after 5 minutes. Generate fresh data before opening.

### "workspaceSecret should not be used in browser code"

You're initializing `Fdback` with a secret in client-side code. Remove the secret and use a backend API to sign requests.

### Build error: "This module cannot be imported from a Client Component"

You're importing `@fdback.io/sdk/next` in client code. These helpers are server-only. Create a server action or API route instead.

---

## License

MIT