Use this page to walk people through creating or connecting something that takes several ordered steps of input, such as adding a sending domain, one step at a time. A stepper shows where they are and takes them back to finished steps, Back and Continue move between steps, and a final review step lists every value before the action that creates it. Use a single form instead when the fields do not depend on each other.
Send email from your own domain so messages show your brand and reach the inbox.
import {
Button,
ButtonGroup,
Checkbox,
DescriptionList,
DescriptionListDetails,
DescriptionListTerm,
Form,
FormLayout,
FormLayoutGroup,
Page,
PageBody,
PageHeader,
PageHeaderDescription,
PageHeaderTitle,
PageSection,
Panel,
PanelDescription,
PanelHeader,
PanelSection,
PanelTitle,
Stepper,
StepperStep,
TextField,
} from '@customerio/pluma-components/react';
export type SetupWizardStep = 'domain' | 'tracking' | 'sender' | 'review';
const steps: { value: SetupWizardStep; label: string }[] = [
{ value: 'domain', label: 'Domain' },
{ value: 'tracking', label: 'Tracking' },
{ value: 'sender', label: 'Sender' },
{ value: 'review', label: 'Review' },
];
export interface SetupWizardValues {
domain: string;
trackClicks: boolean;
/** Only used while `trackClicks` is on. */
trackingSubdomain: string;
trackOpens: boolean;
fromName: string;
fromAddress: string;
/** An empty string sends replies to the from address. */
replyToAddress: string;
}
/** An error message per field, for the fields that failed validation. */
export type SetupWizardErrors = Partial<
Record<'domain' | 'trackingSubdomain' | 'fromName' | 'fromAddress' | 'replyToAddress', string>
>;
export interface SetupWizardProps {
/** The step on screen. */
currentStep: SetupWizardStep;
/** The steps the user has finished. People can go back to any of them from the stepper. */
completedSteps: SetupWizardStep[];
/** The current value of every field, across all steps. */
values: SetupWizardValues;
/** Errors to show under the fields of the current step. */
errors: SetupWizardErrors;
/** Called on every keystroke in the sending domain field. */
onDomainInput: (event: Event & { target: HTMLInputElement }) => void;
/** Called when the click tracking checkbox is toggled. */
onTrackClicksChange: (isChecked: boolean) => void;
/** Called on every keystroke in the tracking subdomain field. */
onTrackingSubdomainInput: (event: Event & { target: HTMLInputElement }) => void;
/** Called when the open tracking checkbox is toggled. */
onTrackOpensChange: (isChecked: boolean) => void;
/** Called on every keystroke in the from name field. */
onFromNameInput: (event: Event & { target: HTMLInputElement }) => void;
/** Called on every keystroke in the from address field. */
onFromAddressInput: (event: Event & { target: HTMLInputElement }) => void;
/** Called on every keystroke in the reply-to address field. */
onReplyToAddressInput: (event: Event & { target: HTMLInputElement }) => void;
/** Called with a completed step the user picks in the stepper. */
onStepSelect: (step: SetupWizardStep) => void;
/** Called when the user goes back from any step but the first. */
onBack: () => void;
/**
* Called when a step before the review is submitted. Validate its fields, then show the next step or pass
* `errors`.
*/
onContinue: () => void;
/** Called when the review step is submitted. Return a promise to lock the form until it settles. */
onSubmit: () => void | Promise<void>;
/** Called when the user leaves the flow. Confirm first if they have entered anything. */
onCancel: () => void;
}
export function SetupWizard({
currentStep,
completedSteps,
values,
errors,
onDomainInput,
onTrackClicksChange,
onTrackingSubdomainInput,
onTrackOpensChange,
onFromNameInput,
onFromAddressInput,
onReplyToAddressInput,
onStepSelect,
onBack,
onContinue,
onSubmit,
onCancel,
}: SetupWizardProps) {
return (
<Page isFullScreen>
<PageHeader>
<PageHeaderTitle>Add sending domain</PageHeaderTitle>
<PageHeaderDescription>
Send email from your own domain so messages show your brand and reach the inbox.
</PageHeaderDescription>
</PageHeader>
<PageBody>
<Form onSubmit={currentStep === 'review' ? onSubmit : onContinue}>
<PageSection>
<Stepper layout="fit" size="sm">
{steps.map((step) => {
const isComplete = completedSteps.includes(step.value);
return (
<StepperStep
key={step.value}
label={step.label}
status={isComplete ? 'complete' : 'incomplete'}
isCurrent={step.value === currentStep}
onClick={isComplete && step.value !== currentStep ? () => onStepSelect(step.value) : undefined}
/>
);
})}
</Stepper>
</PageSection>
<PageSection contentWidth="narrow" divider="contained">
{currentStep === 'domain' ? (
<Panel>
<PanelHeader>
<PanelTitle>Choose a sending domain</PanelTitle>
<PanelDescription>
Use a subdomain you send from nowhere else, so its reputation stays separate from your main domain.
</PanelDescription>
</PanelHeader>
<PanelSection>
<FormLayout>
<TextField
label="Sending domain"
description="After setup, you add DNS records for this domain at your DNS provider."
placeholder="mail.example.com"
value={values.domain}
error={errors.domain}
onInput={onDomainInput}
/>
</FormLayout>
</PanelSection>
</Panel>
) : null}
{currentStep === 'tracking' ? (
<Panel>
<PanelHeader>
<PanelTitle>Set up tracking</PanelTitle>
<PanelDescription>
Tracking adds clicks and opens to your message reports. You can turn it off for any message.
</PanelDescription>
</PanelHeader>
<PanelSection>
<FormLayout>
<Checkbox
label="Track link clicks"
description="Links in your messages redirect through a subdomain of your sending domain"
isChecked={values.trackClicks}
onCheckedChange={onTrackClicksChange}
/>
{values.trackClicks ? (
<TextField
label="Tracking subdomain"
description="Use a subdomain of your sending domain, such as links.mail.example.com."
value={values.trackingSubdomain}
error={errors.trackingSubdomain}
onInput={onTrackingSubdomainInput}
/>
) : null}
<Checkbox
label="Track opens"
description="Adds an invisible image to each email that loads when it is opened"
isChecked={values.trackOpens}
onCheckedChange={onTrackOpensChange}
/>
</FormLayout>
</PanelSection>
</Panel>
) : null}
{currentStep === 'sender' ? (
<Panel>
<PanelHeader>
<PanelTitle>Choose a default sender</PanelTitle>
<PanelDescription>
New messages from this domain start with this sender. You can change it on any message.
</PanelDescription>
</PanelHeader>
<PanelSection>
<FormLayout>
<FormLayoutGroup>
<TextField
label="From name"
value={values.fromName}
error={errors.fromName}
onInput={onFromNameInput}
/>
<TextField
label="From address"
type="email"
value={values.fromAddress}
error={errors.fromAddress}
onInput={onFromAddressInput}
/>
</FormLayoutGroup>
<TextField
label="Reply-to address (optional)"
description="Leave empty to send replies to the from address."
type="email"
value={values.replyToAddress}
error={errors.replyToAddress}
onInput={onReplyToAddressInput}
/>
</FormLayout>
</PanelSection>
</Panel>
) : null}
{currentStep === 'review' ? (
<Panel>
<PanelHeader>
<PanelTitle>Review your domain</PanelTitle>
<PanelDescription>
After you add the domain, we show the DNS records to add at your DNS provider to verify it.
</PanelDescription>
</PanelHeader>
<PanelSection>
<DescriptionList layout="horizontal">
<DescriptionListTerm>Sending domain</DescriptionListTerm>
<DescriptionListDetails>{values.domain}</DescriptionListDetails>
<DescriptionListTerm>Click tracking</DescriptionListTerm>
<DescriptionListDetails>
{values.trackClicks ? `On, through ${values.trackingSubdomain}` : 'Off'}
</DescriptionListDetails>
<DescriptionListTerm>Open tracking</DescriptionListTerm>
<DescriptionListDetails>{values.trackOpens ? 'On' : 'Off'}</DescriptionListDetails>
<DescriptionListTerm>From name</DescriptionListTerm>
<DescriptionListDetails>{values.fromName}</DescriptionListDetails>
<DescriptionListTerm>From address</DescriptionListTerm>
<DescriptionListDetails>{values.fromAddress}</DescriptionListDetails>
<DescriptionListTerm>Reply-to address</DescriptionListTerm>
<DescriptionListDetails>
{values.replyToAddress === '' ? 'Same as from address' : values.replyToAddress}
</DescriptionListDetails>
</DescriptionList>
</PanelSection>
</Panel>
) : null}
</PageSection>
<PageSection contentWidth="narrow">
<ButtonGroup groupVariant="spaced" display="flex">
<Button variant="tertiary" mr="auto" onClick={onCancel}>
Cancel
</Button>
{currentStep === 'domain' ? null : (
<Button variant="secondary" onClick={onBack}>
Back
</Button>
)}
<Button variant="primary" type="submit">
{currentStep === 'review' ? 'Add domain' : 'Continue'}
</Button>
</ButtonGroup>
</PageSection>
</Form>
</PageBody>
</Page>
);
}
Copy this recipe into your app with the Pluma CLI. Replace <dest-dir> with the folder the file goes in, and add --name <Name> to rename the component and its file.
pluma-cli scaffold setup-wizard <dest-dir> --framework reactThis recipe uses these components. Each one's docs page lists its props.