OnboardingPortal
A full-screen, multi-step onboarding wizard that guides new accounts through business setup, phone number configuration, and hardware assignment.
Basic Usage
import { DialstackComponentsProvider } from '@dialstack/sdk-react';
import { OnboardingPortal } from '@dialstack/sdk-react/onboarding';
// `dialstack` comes from loadDialstackAndInitialize(); see Setup
function OnboardingPage({ dialstack }) {
return (
<DialstackComponentsProvider dialstack={dialstack}>
<OnboardingPortal />
</DialstackComponentsProvider>
);
}
Each component has its own import path, so your app only pulls in what it uses. OnboardingPortal comes from @dialstack/sdk-react/onboarding, while the provider and hooks come from the @dialstack/sdk-react root.
The portal automatically saves onboarding progress to the server. Users can leave and return at any time — the wizard resumes where they left off.
How It Works
The onboarding portal presents three main steps, each with its own substeps:
| Step | Description | Substeps |
|---|---|---|
| Account | Business profile and team setup | Business Details, Team Members |
| Numbers | Phone number ordering and porting | Number options, ordering/porting flow, primary number, caller ID, directory listing |
| Hardware | Device assignment for team members | Device Assignment |
The Team Members substep lists the account's users. It also lists portal administrators who have no phone service, which is where the account owner starts. Each of them has a Give phone access button that adds them as a user with the next free extension. Until that happens, they don't count toward the one team member the substep requires. Administrators are marked Admin, and removing one only takes away their phone access: they stay an administrator and go back to the list without phone service.
The portal has three view modes that users progress through:
- Splash screen — a welcome screen shown on first visit
- Overview — a dashboard showing progress across all steps with status indicators
- Wizard — the step-by-step form workflow
When all steps are complete, a celebration screen is shown. After that, the overview screen serves as the landing page where users can review or revise any step.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
locale | Locale | English | Custom locale for UI strings |
formatting | FormattingOptions | - | Date/phone formatting options |
icons | ComponentIcons | - | Custom SVG icons |
collectionOptions | OnboardingCollectionOptions | - | Control which steps to show |
theme | 'light' | 'dark' | 'light' | Color theme |
appearance | { variables?: { colorPrimary?: string; colorPrimaryHover?: string } } | - | Brand color overrides |
canAcceptAgreement | boolean | true | Whether this session may accept the subscription agreement (see below) |
logoHtml | string | - | HTML string for sidebar logo |
platformName | string | - | Platform name shown in the UI |
onBack | () => void | - | Back button callback (shows button when provided) |
backLabel | string | - | Label for the back button |
fullTermsOfServiceUrl | string | - | Link to full terms of service |
recipientTermsOfServiceUrl | string | - | Link to recipient terms of service |
privacyPolicyUrl | string | - | Link to privacy policy |
documentationUrl | string | - | Link shown on the overview screen |
onScheduleCall | () => void | - | Callback for "Schedule a Call" on the overview screen |
onHelpSupport | () => void | - | Callback for "Help & Support" in the sidebar |
onStepChange | (event: { step: AccountOnboardingStep }) => void | - | Called when the active step changes |
className | string | - | Additional CSS class for the root element |
style | React.CSSProperties | - | Inline styles for the root element |
The Subscription Agreement Gate
Before an account can place calls it must accept the DialStack Service Subscription Agreement. The portal enforces this as a blocking screen: until the agreement is accepted, it replaces the whole portal.
Accepting is a capability on the session, not something the API infers about the signed-in person. A session may accept only if you enabled the agreement_acceptance component when you minted it — see Account Sessions. account_onboarding alone lets the session read the agreement and render the screen, but not sign it.
Set canAcceptAgreement to the same condition you used at mint:
<OnboardingPortal canAcceptAgreement={userMayAcceptForThisAccount} />
When it is false, the screen still renders the full agreement — so the reader learns what is pending and that service is blocked — but the affirmation and the Accept button are replaced by a note asking the account owner to accept.
The prop defaults to true, so an existing integration keeps working unchanged. It is a user-experience control, not a security boundary: if the session lacks the component, the submission is rejected regardless. Leaving it true for a session that cannot sign simply strands the user on a button that cannot succeed.
Controlling Steps
Use collectionOptions to show or hide specific steps. The final completion screen is always shown regardless of these options.
Show only specific steps
<OnboardingPortal
collectionOptions={{
steps: {
include: ['account', 'numbers'],
},
}}
/>
Exclude specific steps
<OnboardingPortal
collectionOptions={{
steps: {
exclude: ['hardware'],
},
}}
/>
OnboardingCollectionOptions
| Option | Type | Description |
|---|---|---|
steps.include | AccountOnboardingStep[] | Show only these steps |
steps.exclude | AccountOnboardingStep[] | Hide these steps |
When both include and exclude are set, include is applied first, then exclude removes from that result.
AccountOnboardingStep
The available step values are:
| Value | Description |
|---|---|
'account' | Business details and team members |
'numbers' | Phone number ordering, porting, and configuration |
'hardware' | Device assignment |
'final_complete' | Completion screen (always shown) |
White-Labeling
Customize the portal's branding to match your platform:
<OnboardingPortal
platformName="Acme Telecom"
logoHtml={`
<div style="display:flex;align-items:center;gap:8px">
<img src="/logo.svg" width="28" height="24" alt="" />
<span style="font-size:20px;font-weight:700;color:#f0f0ff">
Acme Telecom
</span>
</div>
`}
appearance={{
variables: {
colorPrimary: '#00A67E',
colorPrimaryHover: '#008F6D',
},
}}
/>
The appearance.variables colors are applied to the sidebar, buttons, and accent elements throughout the wizard. When not provided, the portal uses the colors passed to loadDialstackAndInitialize().
Dark Theme
<OnboardingPortal theme="dark" />
The theme can also be set globally via the appearance.theme option of loadDialstackAndInitialize(). The component-level theme prop takes precedence.
Navigation Callbacks
onBack
Provide an onBack callback to show a back button in the sidebar. Use this to let users navigate back to your application:
<OnboardingPortal onBack={() => router.push('/dashboard')} backLabel="Back to Dashboard" />
onStepChange
Track which step the user is on:
<OnboardingPortal
onStepChange={(event) => {
analytics.track('onboarding_step', { step: event.step });
}}
/>
Help and Support
The overview screen and sidebar can link to help resources:
<OnboardingPortal
documentationUrl="https://docs.example.com/onboarding"
onScheduleCall={() => {
window.open('https://calendly.com/example/onboarding', '_blank');
}}
onHelpSupport={() => {
window.Intercom('showNewMessage', 'I need help with onboarding');
}}
/>
documentationUrl— shows a "View Documentation" link on the overview screenonScheduleCall— shows a "Schedule a Call" button on the overview screenonHelpSupport— shows a "Help & Support" link in the sidebar
Legal Links
Display terms and privacy links on the completion screen:
<OnboardingPortal
fullTermsOfServiceUrl="https://example.com/terms"
recipientTermsOfServiceUrl="https://example.com/recipient-terms"
privacyPolicyUrl="https://example.com/privacy"
/>
Complete Example
import { useEffect, useState } from 'react';
import { loadDialstackAndInitialize, type DialStackInstance } from '@dialstack/sdk-js';
import { DialstackComponentsProvider } from '@dialstack/sdk-react';
import { OnboardingPortal } from '@dialstack/sdk-react/onboarding';
const dialstackPromise = loadDialstackAndInitialize({
publishableKey: 'pk_live_YOUR_KEY',
fetchClientSecret: async () => {
const res = await fetch('/api/dialstack/session', { method: 'POST' });
const { client_secret } = await res.json();
return client_secret;
},
appearance: {
theme: 'light',
},
});
function OnboardingApp() {
const [dialstack, setDialstack] = useState<DialStackInstance | null>(null);
useEffect(() => {
dialstackPromise.then(setDialstack);
}, []);
if (!dialstack) return <div>Loading...</div>;
return (
<DialstackComponentsProvider dialstack={dialstack}>
<OnboardingPortal
platformName="Acme Telecom"
logoHtml={`
<div style="display:flex;align-items:center;gap:8px">
<img src="/logo.svg" width="28" height="24" alt="" />
<span style="font-size:20px;font-weight:700;color:#f0f0ff">
Acme Telecom
</span>
</div>
`}
appearance={{
variables: {
colorPrimary: '#6772E5',
colorPrimaryHover: '#5469D4',
},
}}
collectionOptions={{
steps: { exclude: ['hardware'] },
}}
onBack={() => (window.location.href = '/dashboard')}
backLabel="Back to Dashboard"
documentationUrl="https://docs.example.com/setup"
onScheduleCall={() => window.open('https://calendly.com/example')}
onHelpSupport={() => window.Intercom?.('show')}
onStepChange={(event) => {
console.log('Step changed:', event.step);
}}
fullTermsOfServiceUrl="https://example.com/terms"
privacyPolicyUrl="https://example.com/privacy"
/>
</DialstackComponentsProvider>
);
}