@flamingo-stack/openframe-frontend-core 0.0.596-2034.5669.1 → 0.0.596-2034.5671.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/meeting-scheduler/booking-form.d.ts +22 -2
- package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/context-panel.d.ts +1 -9
- package/dist/components/meeting-scheduler/context-panel.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +47 -26
- package/dist/components/meeting-scheduler/index.cjs.map +1 -1
- package/dist/components/meeting-scheduler/index.d.ts +1 -1
- package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.js +46 -25
- package/dist/components/meeting-scheduler/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/meeting-scheduler/booking-form.tsx +57 -5
- package/src/components/meeting-scheduler/context-panel.tsx +3 -13
- package/src/components/meeting-scheduler/index.tsx +1 -2
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
4
|
-
import { Fragment, useEffect, useMemo } from 'react';
|
|
5
|
-
import type { ReactNode, Ref } from 'react';
|
|
4
|
+
import { Fragment, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import type { FormEvent, ReactNode, Ref } from 'react';
|
|
6
6
|
import { useForm, Controller } from 'react-hook-form';
|
|
7
7
|
import {
|
|
8
8
|
makeDeferredBookingSchema,
|
|
@@ -43,6 +43,25 @@ export interface BookingFieldSlot {
|
|
|
43
43
|
|
|
44
44
|
export type BookingFieldRow = BookingFieldSlot[];
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* A HOST-supplied consent row — the block the waitlist form draws for its SMS
|
|
48
|
+
* consent, here for "I agree to the Privacy Policy and to be contacted". It is
|
|
49
|
+
* the host's copy and the host's link, so it is a prop, not HubSpot metadata;
|
|
50
|
+
* HubSpot's own `legalConsent` block (verbatim, declared on the link) renders
|
|
51
|
+
* alongside when the link carries one.
|
|
52
|
+
*
|
|
53
|
+
* A client-side gate, exactly like the waitlist's: Continue is refused until it
|
|
54
|
+
* is ticked, and the tick rides in the payload as `hostConsent` — which the
|
|
55
|
+
* host's book route strips as an undeclared key. It is not a HubSpot consent
|
|
56
|
+
* record; declare `legalConsent` on the link when one is required.
|
|
57
|
+
*/
|
|
58
|
+
export interface BookingFormConsent {
|
|
59
|
+
label: ReactNode;
|
|
60
|
+
description?: ReactNode;
|
|
61
|
+
/** Shown under the row when Continue is pressed unticked. */
|
|
62
|
+
errorMessage?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
/** Static so Tailwind's scanner sees every class — a template built from a
|
|
47
66
|
* runtime span would compile to nothing. */
|
|
48
67
|
const SPAN_CLASS: Record<1 | 2 | 3 | 4, string> = {
|
|
@@ -101,6 +120,8 @@ export interface BookingFormProps {
|
|
|
101
120
|
* appended full width. Both are deliberate — see `slotNode`/`unplacedFields`.
|
|
102
121
|
*/
|
|
103
122
|
fieldRows?: BookingFieldRow[];
|
|
123
|
+
/** Host-supplied consent row, rendered after the fields — see `BookingFormConsent`. */
|
|
124
|
+
consent?: BookingFormConsent;
|
|
104
125
|
isSubmitting: boolean;
|
|
105
126
|
onSubmit: (payload: Record<string, unknown>) => Promise<void>;
|
|
106
127
|
/** From useHumanitySignals — parent owns the instance so it can resetSignals(). */
|
|
@@ -119,6 +140,7 @@ export function BookingForm({
|
|
|
119
140
|
submitLabel,
|
|
120
141
|
footerNote,
|
|
121
142
|
fieldRows,
|
|
143
|
+
consent,
|
|
122
144
|
isSubmitting,
|
|
123
145
|
onSubmit,
|
|
124
146
|
honeypotInputProps,
|
|
@@ -170,6 +192,12 @@ export function BookingForm({
|
|
|
170
192
|
|
|
171
193
|
const priorConsents = initialValues?.legalConsentResponses as typeof consentDefaults | undefined;
|
|
172
194
|
|
|
195
|
+
// The host consent lives OUTSIDE react-hook-form (it is not a wire field), so
|
|
196
|
+
// a remount restores it from the stash the same way the fields come back.
|
|
197
|
+
const [consented, setConsented] = useState(initialValues?.hostConsent === true);
|
|
198
|
+
const [consentError, setConsentError] = useState<string | null>(null);
|
|
199
|
+
const consentMissing = Boolean(consent) && !consented;
|
|
200
|
+
|
|
173
201
|
// The seeded availability is refetched immediately on mount, so the link's
|
|
174
202
|
// consent set can change WHILE this form is open. Reconcile rather than
|
|
175
203
|
// remount: carry each `consented` across by id and default new ids to false.
|
|
@@ -186,17 +214,26 @@ export function BookingForm({
|
|
|
186
214
|
);
|
|
187
215
|
}, [consentDefaults, priorConsents, setValue]);
|
|
188
216
|
|
|
189
|
-
const
|
|
217
|
+
const submitValid = handleSubmit(async data => {
|
|
218
|
+
if (consentMissing) return; // the error is already on screen — see `submit`
|
|
190
219
|
if (deferSlot) {
|
|
191
220
|
// Collect-only. Signals are captured HERE, while this form and its
|
|
192
221
|
// honeypot are still mounted — `getSignals()` reads a detached ref once
|
|
193
222
|
// they unmount, which would silently disable the decoy.
|
|
194
|
-
await onSubmit({ ...data, meetingId });
|
|
223
|
+
await onSubmit({ ...data, meetingId, hostConsent: consented });
|
|
195
224
|
return;
|
|
196
225
|
}
|
|
197
|
-
await onSubmit({ ...data, meetingId, startTimeMs, durationMs, timezone, ...getSignals() });
|
|
226
|
+
await onSubmit({ ...data, meetingId, hostConsent: consented, startTimeMs, durationMs, timezone, ...getSignals() });
|
|
198
227
|
});
|
|
199
228
|
|
|
229
|
+
// Consent is checked BEFORE the resolver runs, not inside the valid branch,
|
|
230
|
+
// so an unticked box and an empty field are reported together rather than
|
|
231
|
+
// one submit apart.
|
|
232
|
+
const submit = (event: FormEvent<HTMLFormElement>) => {
|
|
233
|
+
if (consentMissing) setConsentError(consent?.errorMessage ?? 'Please agree to continue.');
|
|
234
|
+
return submitValid(event);
|
|
235
|
+
};
|
|
236
|
+
|
|
200
237
|
const fieldError = (name: string): string | undefined => {
|
|
201
238
|
const err = (errors.formFields as Record<string, { message?: string }> | undefined)?.[name];
|
|
202
239
|
return err?.message;
|
|
@@ -426,6 +463,21 @@ export function BookingForm({
|
|
|
426
463
|
</>
|
|
427
464
|
)}
|
|
428
465
|
|
|
466
|
+
{consent && (
|
|
467
|
+
<CheckboxBlock
|
|
468
|
+
id="ms-host-consent"
|
|
469
|
+
checked={consented}
|
|
470
|
+
onCheckedChange={v => {
|
|
471
|
+
setConsented(v);
|
|
472
|
+
if (v) setConsentError(null);
|
|
473
|
+
}}
|
|
474
|
+
disabled={isSubmitting}
|
|
475
|
+
error={consentError ?? undefined}
|
|
476
|
+
label={consent.label}
|
|
477
|
+
description={consent.description}
|
|
478
|
+
/>
|
|
479
|
+
)}
|
|
480
|
+
|
|
429
481
|
{legalConsent && (
|
|
430
482
|
// Same out-of-flow message as the fields above, so a missed consent
|
|
431
483
|
// box doesn't shove the submit button down the card. With no error the
|
|
@@ -47,14 +47,6 @@ export interface SchedulerContextPanelProps {
|
|
|
47
47
|
* states the length and a live selector beside a filled-in form invites a
|
|
48
48
|
* change that would throw the form away. Going Back restores them. */
|
|
49
49
|
locked?: boolean;
|
|
50
|
-
/**
|
|
51
|
-
* The link's length is not the visitor's to choose here: the static duration
|
|
52
|
-
* line renders and the chips never do, at every step. `flow="details-first"`
|
|
53
|
-
* sets it — the mock draws "30 min call" beside the host on the calendar step
|
|
54
|
-
* and offers no selector — and unlike `locked` it has NO effect on the
|
|
55
|
-
* timezone picker, which on that step is the only control the visitor has.
|
|
56
|
-
*/
|
|
57
|
-
fixedDuration?: boolean;
|
|
58
50
|
/** Whether the timezone picker renders. Default true: the zone governs every
|
|
59
51
|
* time on screen, the form's summary line included, so it stays reachable
|
|
60
52
|
* right up to the confirmation — where there is nothing left to re-read. */
|
|
@@ -173,7 +165,6 @@ export function SchedulerContextPanel({
|
|
|
173
165
|
onBack,
|
|
174
166
|
backLabel = 'Back',
|
|
175
167
|
locked = false,
|
|
176
|
-
fixedDuration = false,
|
|
177
168
|
showTimezone = true,
|
|
178
169
|
className,
|
|
179
170
|
}: SchedulerContextPanelProps) {
|
|
@@ -232,9 +223,8 @@ export function SchedulerContextPanel({
|
|
|
232
223
|
</div>
|
|
233
224
|
)}
|
|
234
225
|
|
|
235
|
-
{locked ||
|
|
236
|
-
? // Post-selection,
|
|
237
|
-
// length: a static line,
|
|
226
|
+
{locked || durationsMs.length <= 1
|
|
227
|
+
? // Post-selection, or a link that offers one length: a static line,
|
|
238
228
|
// which is what sits beside the host on the header-strip layouts.
|
|
239
229
|
selectedDurationMs != null && (
|
|
240
230
|
<p className="shrink-0 text-ods-text-secondary text-h6">
|
|
@@ -249,7 +239,7 @@ export function SchedulerContextPanel({
|
|
|
249
239
|
|
|
250
240
|
{/* Several lengths on offer: chips, which need a full row of their
|
|
251
241
|
own at every width — so they stay OUT of the identity row. */}
|
|
252
|
-
{!locked &&
|
|
242
|
+
{!locked && durationsMs.length > 1 && (
|
|
253
243
|
<div className="flex flex-col gap-[var(--spacing-system-xxs)]">
|
|
254
244
|
<p className={FIELD_LABEL_CLASS}>Duration</p>
|
|
255
245
|
<div className="flex flex-wrap gap-[var(--spacing-system-xs)]">
|
|
@@ -62,6 +62,7 @@ export {
|
|
|
62
62
|
type BookingFormProps,
|
|
63
63
|
type BookingFieldRow,
|
|
64
64
|
type BookingFieldSlot,
|
|
65
|
+
type BookingFormConsent,
|
|
65
66
|
} from './booking-form';
|
|
66
67
|
import { Confirmation } from './confirmation';
|
|
67
68
|
import { SchedulerContextPanel, ContextPanelSkeleton } from './context-panel';
|
|
@@ -706,8 +707,6 @@ export function HubSpotMeetingScheduler({
|
|
|
706
707
|
// summary line on the form — so the picker stays until there is
|
|
707
708
|
// nothing left to re-read in it.
|
|
708
709
|
showTimezone={step !== 'confirmed'}
|
|
709
|
-
// The mock's calendar step states the length, it does not offer it.
|
|
710
|
-
fixedDuration={detailsFirst}
|
|
711
710
|
className={CONTEXT_PANEL_CLASS}
|
|
712
711
|
/>
|
|
713
712
|
)}
|