Setup wizard

Page recipe

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.


Preview

Add sending domain

Send email from your own domain so messages show your brand and reach the inbox.

  1. Sender
  2. Review

Choose a default sender

New messages from this domain start with this sender. You can change it on any message.

Source

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>
	);
}

Scaffold

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 react

Components

This recipe uses these components. Each one's docs page lists its props.

On this page