Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion example/src/Onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,48 @@ import {
zendeskArticles,
} from '@remoteoss/remote-flows';
import React, { useState } from 'react';
import { Card } from '@remoteoss/remote-flows/internals';
import { ReviewOnboardingStep } from './ReviewOnboardingStep';
import { OnboardingAlertStatuses } from './OnboardingAlertStatuses';
import { RemoteFlows } from './RemoteFlows';
import { AlertError } from './AlertError';
import { sanitizeHtml } from '@remoteoss/remote-flows/internals';
import './css/main.css';

const BenefitsAboutSection = ({
description,
url,
}: {
description?: string;
url?: string;
}) => {
if (!description) {
return null;
}

return (
<Card className='space-y-4 p-6 mb-4'>
<h2 className='text-xl font-semibold text-gray-900'>About</h2>
<div
className='prose prose-sm max-w-none text-xs text-gray-700 leading-relaxed space-y-4'
dangerouslySetInnerHTML={{ __html: sanitizeHtml(description) }}
/>
{url && (
<p className='text-xs text-gray-700 leading-relaxed space-y-4'>
Want more details on benefits?{' '}
<a
href={url}
className='inline-block text-blue-600 hover:text-blue-700 hover:underline text-xs mt-2'
target='_blank'
rel='noopener noreferrer'
>
Check our guide
</a>
</p>
)}
</Card>
);
};
export const InviteSection = ({
title,
description,
Expand Down Expand Up @@ -188,9 +224,17 @@ const MultiStepForm = ({ components, onboardingBag }: MultiStepFormProps) => {
</>
);

case 'benefits':
case 'benefits': {
// Example: Access schema-level presentation metadata
const benefitsPresentation = onboardingBag.meta.presentation;

return (
<div className='benefits-container'>
<BenefitsAboutSection
description={benefitsPresentation?.description as string}
url={benefitsPresentation?.url as string}
/>

<BenefitsStep
onSubmit={(payload: BenefitsFormPayload) =>
console.log('payload', payload)
Expand All @@ -204,6 +248,7 @@ const MultiStepForm = ({ components, onboardingBag }: MultiStepFormProps) => {
}) => setErrors({ apiError: error.message, fieldErrors })}
onSuccess={(data: SuccessResponse) => console.log('data', data)}
/>

<AlertError errors={errors} />
<div className='buttons-container'>
<BackButton
Expand All @@ -221,6 +266,7 @@ const MultiStepForm = ({ components, onboardingBag }: MultiStepFormProps) => {
</div>
</div>
);
}
case 'review':
return (
<ReviewOnboardingStep
Expand Down
3 changes: 3 additions & 0 deletions src/common/createHeadlessForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export const createHeadlessForm = (
return {
meta: {
'x-jsf-fieldsets': jsfSchema['x-jsf-fieldsets'] as JSFFieldset,
'x-jsf-presentation': jsfSchema['x-jsf-presentation'] as
| Record<string, unknown>
| undefined,
},
...baseCreateHeadlessForm(jsfSchema, {
initialValues,
Expand Down
17 changes: 17 additions & 0 deletions src/flows/ContractorOnboarding/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,22 @@ export const useContractorOnboarding = ({
review: null,
};

const stepPresentation: Record<
StepKeys,
Record<string, unknown> | null | undefined
> = {
select_country: selectCountryForm?.meta?.['x-jsf-presentation'],
basic_information: basicInformationForm?.meta?.['x-jsf-presentation'],
pricing_plan:
selectContractorSubscriptionForm?.meta?.['x-jsf-presentation'],
eligibility_questionnaire:
eligibilityQuestionnaireForm?.meta?.['x-jsf-presentation'],
contract_details:
contractorOnboardingDetailsForm?.meta?.['x-jsf-presentation'],
contract_preview: signatureSchemaForm?.meta?.['x-jsf-presentation'],
review: null,
};

const {
country,
basic_information: employmentBasicInformation = {},
Expand Down Expand Up @@ -1276,6 +1292,7 @@ export const useContractorOnboarding = ({
meta: {
fields: fieldsMetaRef.current,
fieldsets: stepFieldsWithFlatFieldsets[stepState.currentStep.name],
presentation: stepPresentation[stepState.currentStep.name],
},

/**
Expand Down
14 changes: 14 additions & 0 deletions src/flows/Onboarding/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,19 @@ export const useOnboarding = ({
review: null,
};

const stepPresentation: Record<
StepKeys,
Record<string, unknown> | null | undefined
> = {
select_country: selectCountryForm?.meta?.['x-jsf-presentation'],
basic_information: basicInformationForm?.meta?.['x-jsf-presentation'],
engagement_agreement_details:
engagementAgreementDetailsSchema?.meta?.['x-jsf-presentation'],
contract_details: contractDetailsForm?.meta?.['x-jsf-presentation'],
benefits: benefitOffersSchema?.meta?.['x-jsf-presentation'],
review: null,
};

const {
country,
basic_information: employmentBasicInformation = {},
Expand Down Expand Up @@ -1086,6 +1099,7 @@ export const useOnboarding = ({
meta: {
fields: fieldsMetaRef.current,
fieldsets: stepFieldsWithFlatFieldsets[stepState.currentStep.name],
presentation: stepPresentation[stepState.currentStep.name],
},

/**
Expand Down
116 changes: 116 additions & 0 deletions src/flows/Onboarding/tests/OnboardingFlow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2566,4 +2566,120 @@ describe('OnboardingFlow', () => {
).toBeInTheDocument();
});
});

it('should include description, fine_print, and benefits_service_fee in benefits presentation', async () => {
let capturedPresentation: Record<string, unknown> | null | undefined = null;

mockRender.mockImplementation(
({ onboardingBag, components }: OnboardingRenderProps) => {
const currentStepIndex = onboardingBag.stepState.currentStep.index;
const steps: Record<number, string> = {
[0]: 'Basic Information',
[1]: 'Contract Details',
[2]: 'Benefits',
[3]: 'Review',
};

if (onboardingBag.stepState.currentStep.name === 'benefits') {
capturedPresentation = onboardingBag.meta.presentation;
}

if (onboardingBag.isLoading) {
return <div data-testid='spinner'>Loading...</div>;
}

return (
<>
<h1>Step: {steps[currentStepIndex]}</h1>
<MultiStepFormWithoutCountry
onboardingBag={onboardingBag}
components={components}
/>
</>
);
},
);

render(
<OnboardingFlow
employmentId={generateUniqueEmploymentId()}
skipSteps={['select_country']}
{...defaultProps}
/>,
{ wrapper: TestProviders },
);

await waitForElementToBeRemoved(() => screen.getByTestId('spinner'));

let nextButton = screen.getByText(/Next Step/i);
nextButton.click();
await screen.findByText(/Step: Contract Details/i);

nextButton = screen.getByText(/Next Step/i);
nextButton.click();
await screen.findByText(/Step: Benefits/i);

expect(capturedPresentation).toEqual({
benefits_service_fee: {
amount: 15.0,
currency: 'USD',
},
description:
'We offer our employees supplemental benefits - Meal and Health Insurance (In partnership with Advance Care/Tranquilidade and Coverflex)',
fine_print:
'New: Health Insurance is now optional for new hires in Portugal.\r\nPlease note that all local payroll deductions for required coverages are included in the TCE.\r\nAny pricing changes will be communicated in advance of updated billing.',
url: 'https://remote.com/benefits-guide/portugal',
});
});

it('should have null or undefined presentation for basic_information step', async () => {
let capturedBasicInfoPresentation:
| Record<string, unknown>
| null
| undefined = undefined;

mockRender.mockImplementation(
({ onboardingBag, components }: OnboardingRenderProps) => {
const currentStepIndex = onboardingBag.stepState.currentStep.index;
const steps: Record<number, string> = {
[0]: 'Basic Information',
[1]: 'Contract Details',
[2]: 'Benefits',
[3]: 'Review',
};

if (onboardingBag.stepState.currentStep.name === 'basic_information') {
capturedBasicInfoPresentation = onboardingBag.meta.presentation;
}

if (onboardingBag.isLoading) {
return <div data-testid='spinner'>Loading...</div>;
}

return (
<>
<h1>Step: {steps[currentStepIndex]}</h1>
<MultiStepFormWithoutCountry
onboardingBag={onboardingBag}
components={components}
/>
</>
);
},
);

render(
<OnboardingFlow
employmentId={generateUniqueEmploymentId()}
skipSteps={['select_country']}
{...defaultProps}
/>,
{ wrapper: TestProviders },
);

await waitForElementToBeRemoved(() => screen.getByTestId('spinner'));
await screen.findByText(/Step: Basic Information/i);

expect(capturedBasicInfoPresentation).toBeUndefined();
});
});
1 change: 1 addition & 0 deletions src/flows/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,6 @@ type FormResult = FormResultNext | FormResultLegacy;
export type JSONSchemaFormResultWithFieldsets = FormResult & {
meta: {
'x-jsf-fieldsets': JSFFieldset;
'x-jsf-presentation'?: Record<string, unknown>;
};
};
2 changes: 1 addition & 1 deletion src/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

// Internal utilities
export { cn } from './lib/utils';
export { cn, sanitizeHtml } from './lib/utils';

// UI Components for internal use
export { Button, buttonVariants } from './components/ui/button';
Expand Down
Loading