npm package: @portalduct/react-sdk-factorcloud
Description: React SDK for integrating with PortalDuct and FactorCloud APIs
Latest version: 1.3.4

# @portalduct/react-sdk-factorcloud

React SDK for connecting FactorCloud debtors to carrier portals and syncing invoice data through an embedded modal workflow.

> **Auto-managed credentials**
>
> The SDK bootstraps its runtime configuration from the `check-customer-status` webhook. Supply your FactorCloud factor ID and the PortalDuct webhook bearer token and the SDK will fetch the latest FactorCloud and PortalDuct API tokens, API base URLs, and PortalDuct customer UUID for you.

## Installation

```bash
npm install @portalduct/react-sdk-factorcloud
```

**Required peer dependencies** (install if you don't have them):

```bash
npm install react react-dom @tanstack/react-query react-hook-form @hookform/resolvers lucide-react
```

## Quick Start

### 1. Drop in the prebuilt Sync banner (no manual CSS import needed)

The bundle ships with its own stylesheet, so you can simply import the banner component and start collecting connections. Only two required inputs are needed up front: the PortalDuct webhook bearer token (to call the Supabase webhooks) and your FactorCloud factor ID. Everything else is populated automatically after the first customer status check.

```tsx
import { SyncBanner, type SDKConfig } from '@portalduct/react-sdk-factorcloud';

const baseConfig = {
  portalduct: {
    webhook_bearer_token: 'fc_webhook_token',
    // Optional overrides if you are not using the default Supabase functions
    sync_webhook_url: 'https://example.supabase.co/functions/v1/webhook-sync-connection',
    verify_webhook_url: 'https://example.supabase.co/functions/v1/webhook-verify-connection',
    environment: 'sandbox', // Optional: force the environment sent to Supabase
  },
  factorcloud: {
    factorId: 'your_factor_id',
    client_id: 'optional_client_id', // Optional: pre-select a client
  },
} satisfies Partial<SDKConfig>;

function MyApp() {
  return (
    <SyncBanner
      config={baseConfig as SDKConfig} // Remaining fields are hydrated by the SDK
      onSyncComplete={() => console.log('Sync complete')}
      onSyncError={(error) => console.error('Sync failed', error)}
    />
  );
}
```

> **TypeScript tip:** When you only provide the minimal fields above, cast the object to `SDKConfig` at the callsite (as shown). The SDK fills in the missing properties after the status check resolves.

### 2. Prefer manual control? Wrap your app with `QueryClientProvider`

If you need custom layout control, render `SyncWorkflow` directly. The banner example above auto-creates a React Query client, but for full control create your own instance and wrap your app:

```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SyncWorkflow, type SDKConfig } from '@portalduct/react-sdk-factorcloud';

const baseConfig = {
  portalduct: { webhook_bearer_token: 'fc_webhook_token' },
  factorcloud: { factorId: 'your_factor_id' },
} satisfies Partial<SDKConfig>;

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Your app content */}
      <SyncWorkflow
        config={baseConfig as SDKConfig}
        trigger={<button>Sync Invoices</button>}
      />
    </QueryClientProvider>
  );
}
```

That's it! Clicking the "Sync Invoices" button opens a modal workflow that handles:
- Client selection (if no `client_id` provided)
- Debtor selection
- Portal connection setup
- Credential management (tokens are refreshed automatically)
- Invoice synchronization

## Configuration

### Minimal configuration inputs

Only two fields are required at initialization time:

| Field | Required? | Description |
|-------|-----------|-------------|
| `portalduct.webhook_bearer_token` | ✅ | Authenticates requests to the Supabase webhooks (`check-customer-status`, `webhook-sync-connection`, `webhook-verify-connection`). |
| `factorcloud.factorId` | ✅ | Identifies which FactorCloud account should be synced. |

Optional overrides you can provide:

| Field | When to set it |
|-------|----------------|
| `portalduct.sync_webhook_url` / `portalduct.verify_webhook_url` | Override the default Supabase function URLs. |
| `portalduct.environment` | Force a specific environment string to be sent to the webhook payload. The SDK auto-detects `sandbox` vs `production`, so most integrations can omit this. |
| `factorcloud.client_id` | Skip the client-selection step during syncing. |

Everything else—PortalDuct API bearer token, FactorCloud API bearer token, API base URLs, and the PortalDuct customer UUID—is hydrated automatically from the customer status response.

### Runtime configuration shape

Internally the SDK works with a fully populated configuration object:

```ts
import type { SDKConfig } from '@portalduct/react-sdk-factorcloud';

// After the first status check completes the SDK produces an object like this:
const resolvedConfig: SDKConfig = {
  portalduct: {
    bearer_token: 'pd_token',
    env_url: 'https://sandbox.portalduct.com',
    customer_id: '896ec608-373f-4fde-82e5-00b1f427dba5',
    webhook_bearer_token: 'fc_webhook_token',
    environment: 'sandbox',
    sync_webhook_url: 'https://example.supabase.co/functions/v1/webhook-sync-connection',
    verify_webhook_url: 'https://example.supabase.co/functions/v1/webhook-verify-connection',
  },
  factorcloud: {
    bearer_token: 'fc_token',
    factorId: 'your_factor_id',
    env_url: 'https://api.int.factorcloud.com',
    client_id: 'optional_client_id',
  },
};
```

Use the `useCustomerStatus` hook to observe and reuse the merged configuration:

```tsx
import { useCustomerStatus, type SDKConfig } from '@portalduct/react-sdk-factorcloud';

const baseConfig = {
  portalduct: { webhook_bearer_token: 'fc_webhook_token' },
  factorcloud: { factorId: 'your_factor_id' },
} satisfies Partial<SDKConfig>;

function DebugConfig() {
  const { resolvedConfig, loading } = useCustomerStatus(baseConfig as SDKConfig);

  if (loading || !resolvedConfig) return <p>Loading…</p>;
  return <pre>{JSON.stringify(resolvedConfig, null, 2)}</pre>;
}
```

### Standalone Modal Components

Import individual modals for custom workflows:

```tsx
import { useState } from 'react';
import {
  ConfigModal,
  ConnectDebtorModal,
  RequestPortalModal,
  UpdateCredentialsModal,
  SyncProgressModal,
  ClientSelectionModal,
  type SDKConfig,
} from '@portalduct/react-sdk-factorcloud';

function CustomWorkflow() {
  const [open, setOpen] = useState(false);
  const [config, setConfig] = useState<Partial<SDKConfig>>({
    portalduct: { webhook_bearer_token: 'fc_webhook_token' },
    factorcloud: { factorId: 'your_factor_id' },
  });

  return (
    <ConfigModal
      open={open}
      onOpenChange={setOpen}
      onSave={(next) => setConfig(next)}
      currentConfig={config as SDKConfig}
    />
  );
}
```

### Direct API Access

Need to orchestrate data yourself? Pull the resolved config from `useCustomerStatus` first, then hand it to the lower-level API clients:

```tsx
import { useCustomerStatus, type SDKConfig } from '@portalduct/react-sdk-factorcloud';
import {
  PortalDuctAPI,
  FactorCloudAPI,
  ConnectionSyncAPI,
} from '@portalduct/react-sdk-factorcloud';

const baseConfig = {
  portalduct: { webhook_bearer_token: 'fc_webhook_token' },
  factorcloud: { factorId: 'your_factor_id' },
} satisfies Partial<SDKConfig>;

function CustomDataFlow() {
  const { resolvedConfig } = useCustomerStatus(baseConfig as SDKConfig);

  if (!resolvedConfig) {
    return <p>Loading credentials…</p>;
  }

  const portalductApi = new PortalDuctAPI(
    resolvedConfig.portalduct.env_url,
    resolvedConfig.portalduct.bearer_token,
  );

  const factorcloudApi = new FactorCloudAPI(
    resolvedConfig.factorcloud.env_url,
    resolvedConfig.factorcloud.factorId,
    resolvedConfig.factorcloud.bearer_token,
  );

  const connectionSync = new ConnectionSyncAPI({
    webhookToken: resolvedConfig.portalduct.webhook_bearer_token,
    customerId: resolvedConfig.portalduct.customer_id,
    environment: resolvedConfig.portalduct.environment,
    webhookUrl: resolvedConfig.portalduct.sync_webhook_url,
    verifyWebhookUrl: resolvedConfig.portalduct.verify_webhook_url,
    portalductBaseUrl: resolvedConfig.portalduct.env_url,
  });

  // ...custom logic using the clients above
}
```

### Data Orchestration Service

For complex data synchronization workflows you can instantiate the orchestration service once the config has been resolved:

```tsx
import { useEffect } from 'react';
import { useCustomerStatus, DataOrchestrationService, type SDKConfig } from '@portalduct/react-sdk-factorcloud';

const baseConfig = {
  portalduct: { webhook_bearer_token: 'fc_webhook_token' },
  factorcloud: { factorId: 'your_factor_id' },
} satisfies Partial<SDKConfig>;

function SyncAllDebtors() {
  const { resolvedConfig } = useCustomerStatus(baseConfig as SDKConfig);

  useEffect(() => {
    if (!resolvedConfig) return;

    const orchestration = new DataOrchestrationService(resolvedConfig);
    orchestration.getFactorCloudClients().then(console.log);
  }, [resolvedConfig]);

  return null;
}
```

## Security

⚠️ **Never expose API tokens in client-side code in production!**

Even though the SDK auto-fetches credentials, the webhook you call still needs to run on a trusted backend (the provided Supabase functions or your own infrastructure). Store the webhook bearer token securely (environment variables, secret manager, etc.) and never hard-code it in publicly distributed builds.