@portalduct/react-sdk-factorcloud
v1.3.4
Published
React SDK for integrating with PortalDuct and FactorCloud APIs
Maintainers
Readme
@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-statuswebhook. 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
npm install @portalduct/react-sdk-factorcloudRequired peer dependencies (install if you don't have them):
npm install react react-dom @tanstack/react-query react-hook-form @hookform/resolvers lucide-reactQuick 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.
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
SDKConfigat 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:
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_idprovided) - 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:
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:
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:
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:
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:
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.
