@evinvest/kitstart 0.17.0 → 0.19.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/README.md +163 -20
- package/dist/core/accept.d.ts +14 -0
- package/dist/core/accept.js +21 -3
- package/dist/core/analytics.d.ts +11 -2
- package/dist/core/analytics.js +22 -0
- package/dist/core/channels.d.ts +12 -4
- package/dist/core/channels.js +11 -1
- package/dist/core/lead-capture-text.d.ts +6 -2
- package/dist/core/lead.d.ts +33 -10
- package/dist/core/lead.js +21 -8
- package/dist/core/messenger-message.d.ts +31 -0
- package/dist/core/messenger-message.js +33 -0
- package/dist/core/messenger-text.d.ts +104 -0
- package/dist/core/messenger-text.js +150 -0
- package/dist/core/messenger.d.ts +92 -0
- package/dist/core/messenger.js +55 -0
- package/dist/core/place/index.d.ts +1 -1
- package/dist/core/place/live.d.ts +5 -1
- package/dist/core/place/live.js +22 -1
- package/dist/core/place/types.d.ts +17 -1
- package/dist/core/pricing/index.d.ts +2 -1
- package/dist/core/pricing/index.js +2 -1
- package/dist/core/pricing/label.d.ts +7 -0
- package/dist/core/pricing/label.js +8 -0
- package/dist/core/pricing/validate.d.ts +1 -3
- package/dist/core/pricing/validate.js +0 -4
- package/dist/core/site.d.ts +8 -1
- package/dist/core/site.js +22 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +7 -4
- package/dist/next/quote-route.js +4 -7
- package/dist/react/AnalyticsBoundary.js +3 -3
- package/dist/react/FormSelect.d.ts +5 -3
- package/dist/react/FormSelect.js +70 -24
- package/dist/react/FormSelectKit.d.ts +24 -0
- package/dist/react/FormSelectKit.js +28 -0
- package/dist/react/LeadCapture.d.ts +24 -1
- package/dist/react/LeadCapture.js +97 -12
- package/dist/react/LeadCaptureChannels.d.ts +7 -2
- package/dist/react/LeadCaptureChannels.js +2 -2
- package/dist/react/LeadCaptureEstimate.d.ts +6 -0
- package/dist/react/LeadCaptureEstimate.js +1 -1
- package/dist/react/LeadCaptureFields.d.ts +17 -0
- package/dist/react/LeadCaptureFields.js +42 -33
- package/dist/react/LeadCaptureMessenger.d.ts +41 -0
- package/dist/react/LeadCaptureMessenger.js +133 -0
- package/dist/react/LeadCapturePrice.d.ts +9 -3
- package/dist/react/LeadCapturePrice.js +25 -5
- package/dist/react/LeadCaptureSubmit.d.ts +3 -0
- package/dist/react/LeadCaptureSubmit.js +2 -2
- package/dist/react/LeadCaptureTiles.js +9 -4
- package/dist/react/analytics-context.d.ts +9 -3
- package/dist/react/analytics-context.js +7 -3
- package/dist/react/form-select-value.d.ts +3 -0
- package/dist/react/form-select-value.js +6 -6
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +1 -0
- package/dist/react/lazy-part.d.ts +33 -0
- package/dist/react/lazy-part.js +66 -0
- package/dist/react/messenger/action.d.ts +144 -0
- package/dist/react/messenger/action.js +301 -0
- package/dist/react/messenger/chip.d.ts +8 -0
- package/dist/react/messenger/chip.js +22 -0
- package/dist/react/messenger/fallback.d.ts +8 -0
- package/dist/react/messenger/fallback.js +11 -0
- package/dist/react/messenger/saga.d.ts +10 -0
- package/dist/react/messenger/saga.js +33 -0
- package/dist/react/messenger/segment.d.ts +8 -0
- package/dist/react/messenger/segment.js +15 -0
- package/dist/react/messenger/select.d.ts +10 -0
- package/dist/react/messenger/select.js +26 -0
- package/dist/react/messenger/sheet.d.ts +10 -0
- package/dist/react/messenger/sheet.js +57 -0
- package/dist/react/messenger/split.d.ts +9 -0
- package/dist/react/messenger/split.js +17 -0
- package/dist/react/messenger/swap.d.ts +10 -0
- package/dist/react/messenger/swap.js +18 -0
- package/dist/react/messenger/thanks.d.ts +10 -0
- package/dist/react/messenger/thanks.js +18 -0
- package/dist/react/messenger/tiles.d.ts +8 -0
- package/dist/react/messenger/tiles.js +20 -0
- package/dist/react/messenger/types.d.ts +98 -0
- package/dist/react/messenger/types.js +1 -0
- package/dist/react/messenger/urgency.d.ts +10 -0
- package/dist/react/messenger/urgency.js +29 -0
- package/dist/react/use-lead-error.d.ts +3 -3
- package/dist/react/use-lead-events.d.ts +21 -1
- package/dist/react/use-lead-events.js +9 -3
- package/dist/react/use-lead-submit.d.ts +20 -6
- package/dist/react/use-lead-submit.js +23 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/lead-store-sqlite.js +11 -5
- package/dist/server/lead-webhook.d.ts +30 -5
- package/dist/server/lead-webhook.js +13 -7
- package/dist/server/notify.js +11 -5
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/landing-contract.js +6 -1
- package/dist/testing/lead-rules.d.ts +8 -0
- package/dist/testing/lead-rules.js +30 -1
- package/package.json +6 -3
- package/template/flake.nix +1 -1
- package/template/package.json +5 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { PRICING_CURRENCY, PRICING_FORMAT, PRICING_LIMITS, type AddOption, type DiscountOption, type MultiplyOption, type NeedPricing, type Price, type PriceLine, type PricingInput, type PricingInputKind, type PricingInputs, type PricingLabels, type PricingModel, } from "./model.js";
|
|
2
2
|
export { CENTS_BUCKETS, centsBucket, mulBp, priceOf, roundTo } from "./price.js";
|
|
3
|
-
export { labelOf
|
|
3
|
+
export { labelOf } from "./label.js";
|
|
4
|
+
export { parsePricingModel, PricingModelError, pricingProblems, pricingProblemsFor } from "./validate.js";
|
|
4
5
|
export { answeredUnknown, ESTIMATE_FIELD_PREFIX, ESTIMATE_UNKNOWN, estimateField, flowOf, LEAD_FLOWS, readEstimateInputs, type LeadFlow, type LeadFlows } from "./flow.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { PRICING_CURRENCY, PRICING_FORMAT, PRICING_LIMITS, } from "./model.js";
|
|
2
2
|
export { CENTS_BUCKETS, centsBucket, mulBp, priceOf, roundTo } from "./price.js";
|
|
3
|
-
export { labelOf
|
|
3
|
+
export { labelOf } from "./label.js";
|
|
4
|
+
export { parsePricingModel, PricingModelError, pricingProblems, pricingProblemsFor } from "./validate.js";
|
|
4
5
|
export { answeredUnknown, ESTIMATE_FIELD_PREFIX, ESTIMATE_UNKNOWN, estimateField, flowOf, LEAD_FLOWS, readEstimateInputs } from "./flow.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PricingLabels } from "./model.js";
|
|
2
|
+
/**
|
|
3
|
+
* The label for `locale`, or the first one the model has. Apart from the
|
|
4
|
+
* validator: the form names its answers with it in the browser, where the
|
|
5
|
+
* validator's weight has no use.
|
|
6
|
+
*/
|
|
7
|
+
export declare function labelOf(labels: PricingLabels, locale: string): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The label for `locale`, or the first one the model has. Apart from the
|
|
3
|
+
* validator: the form names its answers with it in the browser, where the
|
|
4
|
+
* validator's weight has no use.
|
|
5
|
+
*/
|
|
6
|
+
export function labelOf(labels, locale) {
|
|
7
|
+
return (Object.hasOwn(labels, locale) ? labels[locale] : undefined) ?? Object.values(labels)[0] ?? "";
|
|
8
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type PricingModel } from "./model.js";
|
|
2
2
|
/**
|
|
3
3
|
* A model that does not validate, with every reason found. A model is taken
|
|
4
4
|
* whole or not at all: one bad field refuses it, so a page never prices from
|
|
@@ -23,5 +23,3 @@ export declare function parsePricingModel(value: unknown): PricingModel;
|
|
|
23
23
|
* must not show an option with no words.
|
|
24
24
|
*/
|
|
25
25
|
export declare function pricingProblemsFor(value: unknown, locales: readonly string[]): string[];
|
|
26
|
-
/** The label for `locale`, or the first one the model has. */
|
|
27
|
-
export declare function labelOf(labels: PricingLabels, locale: string): string;
|
|
@@ -238,7 +238,3 @@ export function pricingProblemsFor(value, locales) {
|
|
|
238
238
|
}
|
|
239
239
|
return out;
|
|
240
240
|
}
|
|
241
|
-
/** The label for `locale`, or the first one the model has. */
|
|
242
|
-
export function labelOf(labels, locale) {
|
|
243
|
-
return (Object.hasOwn(labels, locale) ? labels[locale] : undefined) ?? Object.values(labels)[0] ?? "";
|
|
244
|
-
}
|
package/dist/core/site.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LocaleRegistry } from "@evinvest/i18n";
|
|
2
2
|
import type { BookingRules } from "./booking/model.js";
|
|
3
3
|
import type { LeadSchema } from "./lead.js";
|
|
4
|
+
import { type MessengerFacts } from "./messenger.js";
|
|
4
5
|
import type { PublicationPolicy } from "./place/publication.js";
|
|
5
6
|
import type { Place } from "./place/types.js";
|
|
6
7
|
import type { PricingModel } from "./pricing/model.js";
|
|
@@ -126,8 +127,14 @@ export declare function assertLaunchable(site: {
|
|
|
126
127
|
}, todos: readonly OwnerTodo[]): void;
|
|
127
128
|
/** The baked place for a slug, if the site has it. */
|
|
128
129
|
export declare function bakedPlace<L extends string, P extends string>(site: Site<L, P>, slug: string): Place<L> | undefined;
|
|
129
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* The numbers a place answers on: its own, or the brand's until it has one.
|
|
132
|
+
* The panel's messenger switches (`place.messengers`) do not reach here — they
|
|
133
|
+
* are the lead form's (`messengerFacts`); the header, the call bar and the
|
|
134
|
+
* control's channels keep the number.
|
|
135
|
+
*/
|
|
130
136
|
export declare function contactOf(site: Pick<SiteConfig<string, string>, "brand">, place: Place<string>): {
|
|
131
137
|
phone: string | null;
|
|
132
138
|
whatsapp: string | null;
|
|
133
139
|
};
|
|
140
|
+
export declare function messengerFacts(_site: Pick<SiteConfig<string, string>, "brand">, place: Place<string>): MessengerFacts;
|
package/dist/core/site.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bookingConfigProblems, calComHostsProblems } from "./booking/validate.js";
|
|
2
|
+
import { TELEGRAM_BOT } from "./messenger.js";
|
|
2
3
|
import { pricingProblemsFor } from "./pricing/validate.js";
|
|
3
4
|
export function defineSite(config) {
|
|
4
5
|
const pageKeys = Object.keys(config.pages).filter((k) => Object.hasOwn(config.pages, k));
|
|
@@ -101,10 +102,30 @@ export function assertLaunchable(site, todos) {
|
|
|
101
102
|
export function bakedPlace(site, slug) {
|
|
102
103
|
return site.places.find(p => p.slug === slug);
|
|
103
104
|
}
|
|
104
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* The numbers a place answers on: its own, or the brand's until it has one.
|
|
107
|
+
* The panel's messenger switches (`place.messengers`) do not reach here — they
|
|
108
|
+
* are the lead form's (`messengerFacts`); the header, the call bar and the
|
|
109
|
+
* control's channels keep the number.
|
|
110
|
+
*/
|
|
105
111
|
export function contactOf(site, place) {
|
|
106
112
|
return {
|
|
107
113
|
phone: place.channels.phone ?? site.brand.phone,
|
|
108
114
|
whatsapp: place.channels.whatsapp ?? site.brand.phone,
|
|
109
115
|
};
|
|
110
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* What a `LeadCapture` `messenger` variant may offer (`messengers`): the
|
|
119
|
+
* place's own WhatsApp number — never the brand's phone, the chat is answered
|
|
120
|
+
* by hand at the place — and its bot, each unless the panel switched it off.
|
|
121
|
+
* A baked bot whose name is not one (`TELEGRAM_BOT`) is none, as a live one is.
|
|
122
|
+
* Not a fallback for `contactOf`: the control's WhatsApp link keeps the brand's.
|
|
123
|
+
*/
|
|
124
|
+
const validBot = (bot) => (bot && TELEGRAM_BOT.test(bot) ? bot : null);
|
|
125
|
+
export function messengerFacts(_site, place) {
|
|
126
|
+
const on = place.messengers ?? {};
|
|
127
|
+
return {
|
|
128
|
+
whatsapp: on.whatsapp === false ? null : place.channels.whatsapp,
|
|
129
|
+
telegram: on.telegram === false ? null : validBot(place.channels.telegram),
|
|
130
|
+
};
|
|
131
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,20 +3,23 @@
|
|
|
3
3
|
* the proxy (edge), a client island and a server module can all import it.
|
|
4
4
|
* Runtime-bound halves live on their own subpaths.
|
|
5
5
|
*/
|
|
6
|
-
export { assertLaunchable, bakedPlace, cardFact, contactOf, defineSite, ogLocaleOf, openLaunchBlockers, perLocale, type BrandFacts, type LegacyRedirect, type OwnerTodo, type PageSuffix, type Site, type SiteConfig, type Topology, } from "./core/site.js";
|
|
7
|
-
export { createPlaceView, DEFAULT_TIME_ZONE, freshRating, isOpenAt, nextOpening, parseInstant, isPublished, mergeLive, parsePlaceLive, placeHref, placeOrigin, placeUrl, publicationGaps, RATING_MAX_AGE_DAYS, servedLocalities, SERVICE_AREA_GATE, siteOrigin, STOREFRONT_GATE, storefrontOf, type DayOfWeek, type Geo, type LinkBase, type LinkMode, type Opening, type OpeningHours, type OriginFacts, type Place, type PlaceLive, type PlaceView, type PostalAddress, type Presence, type PublicationField, type PublicationPolicy, type Rating, type ServiceArea, } from "./core/place/index.js";
|
|
6
|
+
export { assertLaunchable, bakedPlace, cardFact, contactOf, defineSite, messengerFacts, ogLocaleOf, openLaunchBlockers, perLocale, type BrandFacts, type LegacyRedirect, type OwnerTodo, type PageSuffix, type Site, type SiteConfig, type Topology, } from "./core/site.js";
|
|
7
|
+
export { createPlaceView, DEFAULT_TIME_ZONE, freshRating, isOpenAt, nextOpening, parseInstant, isPublished, mergeLive, parsePlaceLive, placeHref, placeOrigin, placeUrl, publicationGaps, RATING_MAX_AGE_DAYS, servedLocalities, SERVICE_AREA_GATE, siteOrigin, STOREFRONT_GATE, storefrontOf, type DayOfWeek, type Geo, type LinkBase, type MessengerSwitches, type LinkMode, type Opening, type OpeningHours, type OriginFacts, type Place, type PlaceLive, type PlaceView, type PostalAddress, type Presence, type PublicationField, type PublicationPolicy, type Rating, type ServiceArea, } from "./core/place/index.js";
|
|
8
8
|
export { createRouting, GONE, GONE_HEADER, gonePath, goneHeader, HOST_MARK, LANG_COOKIE, LANG_COOKIE_MAX_AGE, NON_PAGE_ROUTES, parseGoneHeader, parsePlaceParam, PASS_PATHS, placeParam, pointSuffixes, THANKS, type Decision, type RequestFacts, type Routing, } from "./core/routing.js";
|
|
9
|
-
export { CARD_FIELD, CARD_ID, CHANNEL_FIELD, channelOf, CONSENT_FIELD, LEAD_CHANNELS, LEAD_ERROR_PARAM, LEAD_CARD_PARAM, leadErrorFor, leadErrorOf, LEAD_SCHEMA_VERSION, MAX_CONSENT, MAX_FIELD, readCandidate, rejectionOf, suspectOf, SUBMISSION_FIELD, SUBMISSION_ID, validateCallbackLead, validateCandidate, validateLead, type Lead, type LeadCandidate, type LeadChannel, type LeadError, type PageLeadError, type LeadConsent, type LeadExtra, type LeadPrice, type LeadRejection, type LeadSchema, type LeadStore, type LeadSuspect, type LeadVerdict, type LeadWire, type SpamVerdict, } from "./core/lead.js";
|
|
9
|
+
export { CARD_FIELD, CARD_ID, CHANNEL_FIELD, channelOf, CONSENT_FIELD, isMessengerChannel, LEAD_CHANNELS, LEAD_ERROR_PARAM, LEAD_CARD_PARAM, leadErrorFor, leadErrorOf, LEAD_SCHEMA_VERSION, MAX_CONSENT, MAX_FIELD, readCandidate, rejectionOf, suspectOf, SUBMISSION_FIELD, SUBMISSION_ID, validateCallbackLead, validateCandidate, validateLead, type Lead, type LeadCandidate, type LeadChannel, type LeadError, type PageLeadError, type LeadConsent, type MessengerChannel, type LeadExtra, type LeadPrice, type LeadRejection, type LeadSchema, type LeadStore, type LeadSuspect, type LeadVerdict, type LeadWire, type SpamVerdict, } from "./core/lead.js";
|
|
10
10
|
export { isMobilePhone, isPlausiblePhone, normalizePhone, phoneProblem } from "./core/phone.js";
|
|
11
11
|
export { answeredUnknown, CENTS_BUCKETS, centsBucket, ESTIMATE_FIELD_PREFIX, ESTIMATE_UNKNOWN, estimateField, flowOf, labelOf, LEAD_FLOWS, mulBp, parsePricingModel, priceOf, PRICING_CURRENCY, PRICING_FORMAT, PRICING_LIMITS, PricingModelError, pricingProblems, pricingProblemsFor, readEstimateInputs, roundTo, type AddOption, type DiscountOption, type LeadFlow, type LeadFlows, type MultiplyOption, type NeedPricing, type Price, type PriceLine, type PricingInput, type PricingInputKind, type PricingInputs, type PricingLabels, type PricingModel, } from "./core/pricing/index.js";
|
|
12
12
|
export { flowTextOf, LEAD_CAPTURE_TEXT, type LeadCaptureFlowText, type LeadCaptureText } from "./core/lead-capture-text.js";
|
|
13
|
+
export { LEAD_CAPTURE_MESSENGER_TEXT, messengerTextOf, type LeadCaptureMessengerText } from "./core/messenger-text.js";
|
|
14
|
+
export { channelsAvailable, MESSAGE_REF, MESSAGE_REF_FIELD, MESSAGE_REF_PREFIX, messageRefOf, newMessageRef, TELEGRAM_BOT, telegramHref, type ChannelsAvailable, type MessengerFacts, type MessengerKind, type MessengerMode, type MessengerVariant, type RandomBytes, } from "./core/messenger.js";
|
|
15
|
+
export { MAX_MESSENGER_MESSAGE, messengerMessage, type MessengerMessageInput, type MessengerMessageText } from "./core/messenger-message.js";
|
|
13
16
|
export { fillText, formatCents, openingText } from "./core/lead-capture-format.js";
|
|
14
17
|
export { channelHref, resolveChannels, smsHref, type CaptureChannel, type ChannelFacts, type ChannelOptions, type ResolvedChannels, } from "./core/channels.js";
|
|
15
18
|
export { checkTiming, HONEYPOT_FIELD, LEGACY_HONEYPOT_FIELDS, MIN_FILL_MS, RATE_LIMIT_MAX_KEYS, RateLimiter, RENDERED_AT_FIELD, screen, type Screening, } from "./core/antispam.js";
|
|
16
|
-
export { createAcceptLead, ANALYTICS_ID_FIELD, EXPERIMENT_FIELD, FORM_ID_FIELD, leadRef, LOCALE_FIELD, LOCATION_FIELD, PRICE_CHANGED, SHOWN_CENTS_FIELD, VARIANT_FIELD, type AcceptDeps, type Outcome, type SubmitTags, } from "./core/accept.js";
|
|
19
|
+
export { createAcceptLead, ANALYTICS_ID_FIELD, CHANNELS_FIELD, EXPERIMENT_FIELD, FORM_ID_FIELD, leadRef, LOCALE_FIELD, LOCATION_FIELD, PRICE_CHANGED, SHOWN_CENTS_FIELD, submitTagsOf, VARIANT_FIELD, type AcceptDeps, type Outcome, type SubmitTags, } from "./core/accept.js";
|
|
17
20
|
export { areaServedNodes, breadcrumbNode, businessId, businessNode, faqPageNode, offerNodes, organizationId, organizationNode, placeGraph, websiteId, type OfferInput, type PageGraphCopy, type QuestionAnswer, } from "./core/seo/ld.js";
|
|
18
21
|
export { AI_CRAWLERS, ogImageUrl, robotsFor, sitemapFor, type RobotsFile, type RobotsRule, type SitemapEntry, } from "./core/seo/sitemap.js";
|
|
19
|
-
export { ALLOWED_PROPS, ANALYTICS_ID, analyticsSink, countsAsPageView, EVENTS, EXPERIMENT_SLUG, experimentProps, qaVisit, type AnalyticsTarget, type IntentChannel, type LeadField, type LeadStep, } from "./core/analytics.js";
|
|
22
|
+
export { ALLOWED_PROPS, ANALYTICS_ID, analyticsSink, channelsProps, countsAsPageView, EVENTS, EXPERIMENT_SLUG, experimentProps, qaVisit, type AnalyticsTarget, type IntentChannel, type LeadField, type LeadStep, } from "./core/analytics.js";
|
|
20
23
|
export { AB_COOKIE_PREFIX, AB_FORCE_PARAM, abAssignments, abRunning, abReset, abVariantUrl, cookieValue, type AbReset, } from "./core/ab-switcher.js";
|
|
21
24
|
export { abSwitcherVisible } from "./core/ab-gate.js";
|
|
22
25
|
export type { CallBarText, CopySlice, CoreText, PageMetaCopy, QuoteFormCopy, Said, StatusAction, StatusCopy, StatusScreenText, } from "./core/content.js";
|
package/dist/index.js
CHANGED
|
@@ -3,20 +3,23 @@
|
|
|
3
3
|
* the proxy (edge), a client island and a server module can all import it.
|
|
4
4
|
* Runtime-bound halves live on their own subpaths.
|
|
5
5
|
*/
|
|
6
|
-
export { assertLaunchable, bakedPlace, cardFact, contactOf, defineSite, ogLocaleOf, openLaunchBlockers, perLocale, } from "./core/site.js";
|
|
6
|
+
export { assertLaunchable, bakedPlace, cardFact, contactOf, defineSite, messengerFacts, ogLocaleOf, openLaunchBlockers, perLocale, } from "./core/site.js";
|
|
7
7
|
export { createPlaceView, DEFAULT_TIME_ZONE, freshRating, isOpenAt, nextOpening, parseInstant, isPublished, mergeLive, parsePlaceLive, placeHref, placeOrigin, placeUrl, publicationGaps, RATING_MAX_AGE_DAYS, servedLocalities, SERVICE_AREA_GATE, siteOrigin, STOREFRONT_GATE, storefrontOf, } from "./core/place/index.js";
|
|
8
8
|
export { createRouting, GONE, GONE_HEADER, gonePath, goneHeader, HOST_MARK, LANG_COOKIE, LANG_COOKIE_MAX_AGE, NON_PAGE_ROUTES, parseGoneHeader, parsePlaceParam, PASS_PATHS, placeParam, pointSuffixes, THANKS, } from "./core/routing.js";
|
|
9
|
-
export { CARD_FIELD, CARD_ID, CHANNEL_FIELD, channelOf, CONSENT_FIELD, LEAD_CHANNELS, LEAD_ERROR_PARAM, LEAD_CARD_PARAM, leadErrorFor, leadErrorOf, LEAD_SCHEMA_VERSION, MAX_CONSENT, MAX_FIELD, readCandidate, rejectionOf, suspectOf, SUBMISSION_FIELD, SUBMISSION_ID, validateCallbackLead, validateCandidate, validateLead, } from "./core/lead.js";
|
|
9
|
+
export { CARD_FIELD, CARD_ID, CHANNEL_FIELD, channelOf, CONSENT_FIELD, isMessengerChannel, LEAD_CHANNELS, LEAD_ERROR_PARAM, LEAD_CARD_PARAM, leadErrorFor, leadErrorOf, LEAD_SCHEMA_VERSION, MAX_CONSENT, MAX_FIELD, readCandidate, rejectionOf, suspectOf, SUBMISSION_FIELD, SUBMISSION_ID, validateCallbackLead, validateCandidate, validateLead, } from "./core/lead.js";
|
|
10
10
|
export { isMobilePhone, isPlausiblePhone, normalizePhone, phoneProblem } from "./core/phone.js";
|
|
11
11
|
export { answeredUnknown, CENTS_BUCKETS, centsBucket, ESTIMATE_FIELD_PREFIX, ESTIMATE_UNKNOWN, estimateField, flowOf, labelOf, LEAD_FLOWS, mulBp, parsePricingModel, priceOf, PRICING_CURRENCY, PRICING_FORMAT, PRICING_LIMITS, PricingModelError, pricingProblems, pricingProblemsFor, readEstimateInputs, roundTo, } from "./core/pricing/index.js";
|
|
12
12
|
export { flowTextOf, LEAD_CAPTURE_TEXT } from "./core/lead-capture-text.js";
|
|
13
|
+
export { LEAD_CAPTURE_MESSENGER_TEXT, messengerTextOf } from "./core/messenger-text.js";
|
|
14
|
+
export { channelsAvailable, MESSAGE_REF, MESSAGE_REF_FIELD, MESSAGE_REF_PREFIX, messageRefOf, newMessageRef, TELEGRAM_BOT, telegramHref, } from "./core/messenger.js";
|
|
15
|
+
export { MAX_MESSENGER_MESSAGE, messengerMessage } from "./core/messenger-message.js";
|
|
13
16
|
export { fillText, formatCents, openingText } from "./core/lead-capture-format.js";
|
|
14
17
|
export { channelHref, resolveChannels, smsHref, } from "./core/channels.js";
|
|
15
18
|
export { checkTiming, HONEYPOT_FIELD, LEGACY_HONEYPOT_FIELDS, MIN_FILL_MS, RATE_LIMIT_MAX_KEYS, RateLimiter, RENDERED_AT_FIELD, screen, } from "./core/antispam.js";
|
|
16
|
-
export { createAcceptLead, ANALYTICS_ID_FIELD, EXPERIMENT_FIELD, FORM_ID_FIELD, leadRef, LOCALE_FIELD, LOCATION_FIELD, PRICE_CHANGED, SHOWN_CENTS_FIELD, VARIANT_FIELD, } from "./core/accept.js";
|
|
19
|
+
export { createAcceptLead, ANALYTICS_ID_FIELD, CHANNELS_FIELD, EXPERIMENT_FIELD, FORM_ID_FIELD, leadRef, LOCALE_FIELD, LOCATION_FIELD, PRICE_CHANGED, SHOWN_CENTS_FIELD, submitTagsOf, VARIANT_FIELD, } from "./core/accept.js";
|
|
17
20
|
export { areaServedNodes, breadcrumbNode, businessId, businessNode, faqPageNode, offerNodes, organizationId, organizationNode, placeGraph, websiteId, } from "./core/seo/ld.js";
|
|
18
21
|
export { AI_CRAWLERS, ogImageUrl, robotsFor, sitemapFor, } from "./core/seo/sitemap.js";
|
|
19
|
-
export { ALLOWED_PROPS, ANALYTICS_ID, analyticsSink, countsAsPageView, EVENTS, EXPERIMENT_SLUG, experimentProps, qaVisit, } from "./core/analytics.js";
|
|
22
|
+
export { ALLOWED_PROPS, ANALYTICS_ID, analyticsSink, channelsProps, countsAsPageView, EVENTS, EXPERIMENT_SLUG, experimentProps, qaVisit, } from "./core/analytics.js";
|
|
20
23
|
export { AB_COOKIE_PREFIX, AB_FORCE_PARAM, abAssignments, abRunning, abReset, abVariantUrl, cookieValue, } from "./core/ab-switcher.js";
|
|
21
24
|
export { abSwitcherVisible } from "./core/ab-gate.js";
|
|
22
25
|
export { BOOKING_LIMITS, BOOKING_PROVIDERS, BOOKING_STATUSES, BookingConfigError, bookingConfigProblems, BOOKING_EXPERIMENT, bookingHref, bookingOf, bookingRequestedProperties, bookingRequestProblems, calComHostsProblems, DEFAULT_BOOKING, DEFAULT_CAL_COM_HOSTS, isBookingProvider, isLeadRef, isPageProvider, isPreferredPart, parseBookingConfig, parseBookingRequest, PREFERRED_PARTS, } from "./core/booking/index.js";
|
package/dist/next/quote-route.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { after } from "next/server.js";
|
|
2
2
|
import { CONFIRM_PATH, confirmQuery } from "./confirm-route.js";
|
|
3
|
-
import { createAcceptLead,
|
|
4
|
-
import { analyticsSink, EVENTS
|
|
3
|
+
import { createAcceptLead, PRICE_CHANGED, submitTagsOf } from "../core/accept.js";
|
|
4
|
+
import { analyticsSink, EVENTS } from "../core/analytics.js";
|
|
5
5
|
import { RateLimiter } from "../core/antispam.js";
|
|
6
6
|
import { CARD_FIELD, CARD_ID, channelOf, LEAD_CARD_PARAM, LEAD_ERROR_PARAM } from "../core/lead.js";
|
|
7
7
|
import { createPlaceView } from "../core/place/view.js";
|
|
@@ -54,10 +54,6 @@ function seeOther(location) {
|
|
|
54
54
|
function json(status, body) {
|
|
55
55
|
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
|
|
56
56
|
}
|
|
57
|
-
const text = (form, name) => {
|
|
58
|
-
const value = form.get(name);
|
|
59
|
-
return typeof value === "string" ? value : null;
|
|
60
|
-
};
|
|
61
57
|
/** `LeadCapture`'s own post asks for JSON; a plain form post never does. */
|
|
62
58
|
const wantsJson = (request) => (request.headers.get("accept") ?? "").includes("application/json");
|
|
63
59
|
export function quoteRoute(site, deps) {
|
|
@@ -171,6 +167,7 @@ export function quoteRoute(site, deps) {
|
|
|
171
167
|
capture: (lead, formId, tags) => sinkFor(lead.placeSlug).capture(EVENTS.leadSubmit, {
|
|
172
168
|
form_id: formId,
|
|
173
169
|
channel: channelOf(lead),
|
|
170
|
+
...(lead.messageRef ? { message_ref: lead.messageRef } : {}),
|
|
174
171
|
...tags,
|
|
175
172
|
}),
|
|
176
173
|
limiter: (limiter ??= new RateLimiter(env.leadRateLimit?.limit ?? 5, env.leadRateLimit?.windowMs ?? 600_000)),
|
|
@@ -194,7 +191,7 @@ export function quoteRoute(site, deps) {
|
|
|
194
191
|
return scripted ? json(200, { ok: true, location, lead: outcome.ref, ...price }) : seeOther(location);
|
|
195
192
|
}
|
|
196
193
|
case "invalid": {
|
|
197
|
-
const tags =
|
|
194
|
+
const tags = submitTagsOf(form);
|
|
198
195
|
// The field's role and why — never what was typed.
|
|
199
196
|
const repriced = outcome.field === PRICE_CHANGED;
|
|
200
197
|
const props = { form_id: outcome.formId, channel: outcome.channel, field: outcome.field, reason: repriced ? PRICE_CHANGED : "invalid", ...tags };
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { ContactLinkTracker } from "@evinvest/marketing/tracker";
|
|
4
4
|
import { usePathname } from "next/navigation.js";
|
|
5
5
|
import { useEffect, useMemo, useState } from "react";
|
|
6
|
-
import { analyticsSink, countsAsPageView, EVENTS, experimentProps } from "../core/analytics.js";
|
|
6
|
+
import { analyticsSink, channelsProps, countsAsPageView, EVENTS, experimentProps } from "../core/analytics.js";
|
|
7
7
|
import { AnalyticsIdContext, AnalyticsSinkContext, experimentOf } from "./analytics-context.js";
|
|
8
8
|
import { newSubmissionId } from "./use-lead-submit.js";
|
|
9
9
|
/**
|
|
@@ -11,7 +11,7 @@ import { newSubmissionId } from "./use-lead-submit.js";
|
|
|
11
11
|
* contact link — or one the tracker cannot classify (`sms:`).
|
|
12
12
|
*/
|
|
13
13
|
const INTENT_ATTR = "data-intent";
|
|
14
|
-
const INTENTS = ["form_open", "booking", "sms", "callback"];
|
|
14
|
+
const INTENTS = ["form_open", "booking", "sms", "callback", "telegram", "whatsapp_qr"];
|
|
15
15
|
function source() {
|
|
16
16
|
const utm = new URLSearchParams(window.location.search).get("utm_source");
|
|
17
17
|
if (utm)
|
|
@@ -54,5 +54,5 @@ export function AnalyticsBoundary({ target, placeSlug, qaCookie, children }) {
|
|
|
54
54
|
}, [sink]);
|
|
55
55
|
// A link inside an experiment's island (`LeadCapture`) carries the
|
|
56
56
|
// assignment on itself, which is what the tracker reads.
|
|
57
|
-
return (_jsx(AnalyticsIdContext.Provider, { value: key ? distinctId : null, children: _jsx(AnalyticsSinkContext.Provider, { value: sink, children: _jsx(ContactLinkTracker, { channels: ["phone", "whatsapp"], onContact: ({ channel, data }) => sink.capture(EVENTS.intent, { channel, ...experimentProps(data["experiment"], data["variant"]) }, { transport: "beacon" }), children: children }) }) }));
|
|
57
|
+
return (_jsx(AnalyticsIdContext.Provider, { value: key ? distinctId : null, children: _jsx(AnalyticsSinkContext.Provider, { value: sink, children: _jsx(ContactLinkTracker, { channels: ["phone", "whatsapp"], onContact: ({ channel, data }) => sink.capture(EVENTS.intent, { channel, ...experimentProps(data["experiment"], data["variant"]), ...channelsProps(data["channelsAvailable"]) }, { transport: "beacon" }), children: children }) }) }));
|
|
58
58
|
}
|
|
@@ -4,9 +4,11 @@ import type { PartClassNames } from "./parts.js";
|
|
|
4
4
|
* A form's choice that works before any script and looks like the kit after
|
|
5
5
|
* it. The server, and a browser without JavaScript, get the kit's
|
|
6
6
|
* `NativeSelect` — a real `<select name>` the form posts as is. Once the page
|
|
7
|
-
* has hydrated
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
7
|
+
* has hydrated and is idle, or as soon as the visitor reaches for the field,
|
|
8
|
+
* it becomes the kit's `Select`, drawn in the palette instead of the
|
|
9
|
+
* platform's menu, with the value in an input of the same `name`; until then
|
|
10
|
+
* the native select is live, its choices reported. Both are the same box
|
|
11
|
+
* (height, width, border, inset), so the swap moves nothing.
|
|
10
12
|
*
|
|
11
13
|
* Inside a `Field`, its label names whichever control is rendered: the
|
|
12
14
|
* `Field`'s id goes to the `<select>` first and to the trigger after.
|
package/dist/react/FormSelect.js
CHANGED
|
@@ -1,45 +1,91 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx
|
|
3
|
-
import { cn, NativeSelect, NativeSelectOption
|
|
4
|
-
import { useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn, NativeSelect, NativeSelectOption } from "@evinvest/uikit";
|
|
4
|
+
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { useFormSelectValue } from "./form-select-value.js";
|
|
6
|
+
import { lazyPart } from "./lazy-part.js";
|
|
6
7
|
const BOX = "relative inline-flex w-full";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// The chevron at full strength, as the native select draws it: the kit's
|
|
10
|
-
// trigger dims its own arrow.
|
|
11
|
-
const TRIGGER = "w-full min-w-0 text-ink [&>svg]:opacity-100";
|
|
12
|
-
const TRIGGER_TEXT = { sm: "text-base md:text-sm", md: "text-base md:text-sm", lg: "" };
|
|
8
|
+
/** The scripted state, in a chunk of its own (`FormSelectKit`). */
|
|
9
|
+
const KIT = lazyPart(() => import("./FormSelectKit.js").then(m => m.FormSelectKit));
|
|
13
10
|
const subscribe = () => () => { };
|
|
14
11
|
/** `false` on the server and while hydrating, `true` from the render after. */
|
|
15
12
|
const useHydrated = () => useSyncExternalStore(subscribe, () => true, () => false);
|
|
16
13
|
export function FormSelect(props) {
|
|
17
|
-
|
|
14
|
+
const hydrated = useHydrated();
|
|
15
|
+
const kit = useSyncExternalStore(KIT.subscribe, KIT.isLoaded, () => false);
|
|
16
|
+
return _jsx(FormSelectField, { ...props, hydrated: hydrated, kit: hydrated && kit });
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
20
19
|
* Both states on demand — for the widget gallery, which renders on the server
|
|
21
20
|
* and would only ever see the native one. Not part of the package's surface.
|
|
22
21
|
*/
|
|
23
22
|
export function FormSelectView({ scripted, ...props }) {
|
|
23
|
+
return _jsx(FormSelectField, { ...props, hydrated: scripted, kit: scripted });
|
|
24
|
+
}
|
|
25
|
+
/** Asks for the scripted state when the page has nothing better to do. */
|
|
26
|
+
function whenIdle(run) {
|
|
27
|
+
if (typeof window.requestIdleCallback === "function") {
|
|
28
|
+
const handle = window.requestIdleCallback(run, { timeout: 2000 });
|
|
29
|
+
return () => window.cancelIdleCallback(handle);
|
|
30
|
+
}
|
|
31
|
+
const handle = window.setTimeout(run, 200);
|
|
32
|
+
return () => window.clearTimeout(handle);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The native select until the scripted one is here. `hydrated`: the page's
|
|
36
|
+
* script runs — the native select reports its choices and asks for the
|
|
37
|
+
* scripted state on idle, or sooner on the visitor's reach (a mouse over it,
|
|
38
|
+
* the focus). `kit`: the scripted state is loaded. A press before that opens
|
|
39
|
+
* the platform's list at once, as before the script: never a wait on a chunk.
|
|
40
|
+
*/
|
|
41
|
+
function FormSelectField({ hydrated, kit, ...props }) {
|
|
24
42
|
const { name, options, placeholder, required, disabled, size = "md", id, className, classNames } = props;
|
|
25
43
|
const initial = props.defaultValue ?? (placeholder !== undefined ? "" : (options[0]?.value ?? ""));
|
|
26
44
|
const native = useRef(null);
|
|
27
45
|
// The box, not the trigger: `SelectTrigger` keeps its own ref for placing the list.
|
|
28
46
|
const box = useRef(null);
|
|
29
|
-
|
|
47
|
+
// The native select has the focus — its own list may be open (a phone's
|
|
48
|
+
// picker): it stays until the focus leaves, rather than vanish under the finger.
|
|
49
|
+
const [focused, setFocused] = useState(false);
|
|
50
|
+
const scripted = kit && !focused;
|
|
51
|
+
const state = useFormSelectValue({ initial, controlled: props.value, native, box, live: hydrated, scripted, onValueChange: props.onValueChange });
|
|
30
52
|
const hint = placeholder !== undefined ? { placeholder } : {};
|
|
31
53
|
const aria = { "aria-describedby": props["aria-describedby"], "aria-invalid": props["aria-invalid"] || state.invalid || undefined };
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
useEffect(() => (hydrated && !kit ? whenIdle(KIT.preload) : undefined), [hydrated, kit]);
|
|
55
|
+
// The native select is uncontrolled (see below): a value moved by the
|
|
56
|
+
// parent, or put back by a reset, is written into it here.
|
|
57
|
+
const written = useRef(state.value);
|
|
58
|
+
useLayoutEffect(() => {
|
|
59
|
+
if (written.current === state.value)
|
|
60
|
+
return;
|
|
61
|
+
written.current = state.value;
|
|
62
|
+
if (native.current && native.current.value !== state.value)
|
|
63
|
+
native.current.value = state.value;
|
|
64
|
+
}, [state.value]);
|
|
65
|
+
if (scripted)
|
|
66
|
+
return _jsx(KIT.Part, { field: props, box: box, value: state.value, choose: state.choose, open: state.open, setOpen: state.setOpen, onInvalid: state.onInvalid, aria: aria });
|
|
67
|
+
const live = hydrated
|
|
68
|
+
? {
|
|
69
|
+
onChange: (e) => {
|
|
70
|
+
const picked = e.currentTarget.value;
|
|
71
|
+
state.choose(picked);
|
|
72
|
+
// Under a `value` the parent moves it, as on a controlled input.
|
|
73
|
+
if (props.value !== undefined)
|
|
74
|
+
e.currentTarget.value = props.value;
|
|
75
|
+
},
|
|
76
|
+
onPointerEnter: (e) => {
|
|
77
|
+
if (e.pointerType === "mouse")
|
|
78
|
+
KIT.preload();
|
|
79
|
+
},
|
|
80
|
+
onFocus: () => {
|
|
81
|
+
setFocused(true);
|
|
82
|
+
KIT.preload();
|
|
83
|
+
},
|
|
84
|
+
onBlur: () => setFocused(false),
|
|
85
|
+
}
|
|
86
|
+
: {};
|
|
87
|
+
return (_jsx(NativeSelect, { ref: native, name: name, id: id, size: size, ...hint, required: required, disabled: disabled,
|
|
88
|
+
// Uncontrolled even under `value`: before the script nothing could
|
|
89
|
+
// follow a change, and a pick made then must survive into the swap.
|
|
90
|
+
defaultValue: state.value, wrapperClassName: cn(BOX, className), className: classNames?.trigger, ...aria, ...live, children: options.map(o => (_jsx(NativeSelectOption, { value: o.value, children: o.label }, o.value))) }));
|
|
45
91
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { InvalidEvent, RefObject } from "react";
|
|
2
|
+
import type { FormSelectProps } from "./FormSelect.js";
|
|
3
|
+
/** What the scripted state is handed by `FormSelect`, which keeps the value across the swap. */
|
|
4
|
+
export interface FormSelectKitProps {
|
|
5
|
+
field: FormSelectProps;
|
|
6
|
+
box: RefObject<HTMLSpanElement | null>;
|
|
7
|
+
value: string;
|
|
8
|
+
choose: (value: string) => void;
|
|
9
|
+
open: boolean;
|
|
10
|
+
setOpen: (open: boolean) => void;
|
|
11
|
+
onInvalid: (e: InvalidEvent<HTMLInputElement>) => void;
|
|
12
|
+
aria: {
|
|
13
|
+
"aria-describedby": string | undefined;
|
|
14
|
+
"aria-invalid": true | undefined;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `FormSelect` once scripted: the kit's `Select`, the list drawn in the
|
|
19
|
+
* palette, with the value in an input of the same `name`. A chunk of its own
|
|
20
|
+
* — the list's placing, dismissal and keys weigh more than the rest of the
|
|
21
|
+
* field — fetched once the page is idle, or as soon as the visitor reaches
|
|
22
|
+
* for the field.
|
|
23
|
+
*/
|
|
24
|
+
export declare function FormSelectKit({ field, box, value, choose, open, setOpen, onInvalid, aria }: FormSelectKitProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@evinvest/uikit";
|
|
4
|
+
const BOX = "relative inline-flex w-full";
|
|
5
|
+
// The trigger's geometry matched to `NativeSelect`'s at each size: full width,
|
|
6
|
+
// the same text size (16 px on phones, which keeps iOS from zooming).
|
|
7
|
+
// The chevron at full strength, as the native select draws it: the kit's
|
|
8
|
+
// trigger dims its own arrow.
|
|
9
|
+
const TRIGGER = "w-full min-w-0 text-ink [&>svg]:opacity-100";
|
|
10
|
+
const TRIGGER_TEXT = { sm: "text-base md:text-sm", md: "text-base md:text-sm", lg: "" };
|
|
11
|
+
/**
|
|
12
|
+
* `FormSelect` once scripted: the kit's `Select`, the list drawn in the
|
|
13
|
+
* palette, with the value in an input of the same `name`. A chunk of its own
|
|
14
|
+
* — the list's placing, dismissal and keys weigh more than the rest of the
|
|
15
|
+
* field — fetched once the page is idle, or as soon as the visitor reaches
|
|
16
|
+
* for the field.
|
|
17
|
+
*/
|
|
18
|
+
export function FormSelectKit({ field, box, value, choose, open, setOpen, onInvalid, aria }) {
|
|
19
|
+
const { name, options, placeholder, required, disabled, size = "md", id, className, classNames } = field;
|
|
20
|
+
const hint = placeholder !== undefined ? { placeholder } : {};
|
|
21
|
+
return (_jsxs("span", { ref: box, "data-slot": "form-select", className: cn(BOX, className), children: [_jsxs(Select, { value: value, onValueChange: choose, open: open, onOpenChange: setOpen, children: [_jsx(SelectTrigger, { id: id, size: size, disabled: disabled, "aria-required": required || undefined, className: cn(TRIGGER, TRIGGER_TEXT[size], classNames?.trigger), ...aria, children: _jsx(SelectValue, { ...hint }) }), _jsx(SelectContent, { className: classNames?.content, children: options.map(o => (_jsx(SelectItem, { value: o.value, className: cn(size === "lg" && "py-2.5 text-base", classNames?.item), children: o.label }, o.value))) })] }), required ? (
|
|
22
|
+
// Validated, so not `type="hidden"` (which the browser never checks);
|
|
23
|
+
// under the trigger, out of the tab order, the accessibility tree and
|
|
24
|
+
// the browser's autofill.
|
|
25
|
+
_jsx("input", { name: name, value: value, required: true, disabled: disabled, tabIndex: -1, "aria-hidden": "true", autoComplete: "off", onChange: () => { }, onInvalid: onInvalid, className: "pointer-events-none absolute inset-0 opacity-0" })) : (
|
|
26
|
+
// Nothing chosen posts nothing, as a native select on its disabled placeholder.
|
|
27
|
+
value !== "" && _jsx("input", { type: "hidden", name: name, value: value, disabled: disabled }))] }));
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type CaptureChannel } from "../core/channels.js";
|
|
3
3
|
import { type LeadError, type LeadWire, type PageLeadError } from "../core/lead.js";
|
|
4
|
+
import { type MessengerFacts, type MessengerVariant } from "../core/messenger.js";
|
|
4
5
|
import { type LeadCaptureText } from "../core/lead-capture-text.js";
|
|
5
6
|
import { type Place } from "../core/place/types.js";
|
|
6
7
|
import { type LeadFlows } from "../core/pricing/flow.js";
|
|
@@ -14,10 +15,11 @@ import { type FieldPart } from "./LeadCaptureFields.js";
|
|
|
14
15
|
import { type LeadCaptureLayout, type LeadNeedDisplay, type LeadNeedOption } from "./LeadCaptureNeed.js";
|
|
15
16
|
import { type PricePart } from "./LeadCapturePrice.js";
|
|
16
17
|
import { type StepsPart } from "./LeadCaptureSteps.js";
|
|
18
|
+
import type { MessengerPart } from "./messenger/types.js";
|
|
17
19
|
import { type LeadIntro } from "./lead-steps.js";
|
|
18
20
|
import type { PartClassNames } from "./parts.js";
|
|
19
21
|
import { type LeadSent } from "./use-lead-submit.js";
|
|
20
|
-
export type LeadCapturePart = "root" | "head" | "title" | "lede" | "form" | "contact" | "submit" | "trust" | "privacy" | "opening" | "others" | "done" | "needs" | "need" | "summary" | "icon" | "intro" | "introOption" | "stepNext" | FieldPart | ChannelPart | EstimatePart | PricePart | StepsPart | PricedPart | BookingPart;
|
|
22
|
+
export type LeadCapturePart = "root" | "head" | "title" | "lede" | "form" | "contact" | "submit" | "trust" | "privacy" | "opening" | "others" | "done" | "needs" | "need" | "summary" | "icon" | "intro" | "introOption" | "stepNext" | FieldPart | ChannelPart | EstimatePart | PricePart | StepsPart | PricedPart | BookingPart | MessengerPart;
|
|
21
23
|
export interface LeadCaptureProps {
|
|
22
24
|
/** Its hours order the channels; its service area suggests the commune. */
|
|
23
25
|
place: Place<string>;
|
|
@@ -117,6 +119,27 @@ export interface LeadCaptureProps {
|
|
|
117
119
|
channelIcons?: Partial<Record<ChannelIconKey, ReactNode>> | undefined;
|
|
118
120
|
/** Moved first when available — a `default_channel` experiment's arm. */
|
|
119
121
|
prefer?: CaptureChannel | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* The `lead_channel` experiment's arm: how the card offers WhatsApp and the
|
|
124
|
+
* brand's bot (`MessengerVariant`); absent → the control. A place without
|
|
125
|
+
* WhatsApp falls back to the control (`messengerShownOf`), and every event
|
|
126
|
+
* says which messengers were there (`channels_available`).
|
|
127
|
+
*/
|
|
128
|
+
messenger?: MessengerVariant | undefined;
|
|
129
|
+
/** `messengerFacts(site, place)`: the place's own WhatsApp number and bot, each `null` when off. */
|
|
130
|
+
messengers?: MessengerFacts | undefined;
|
|
131
|
+
/** The brand's prefix of the lead's chat reference (`AQ` → `AQ-7K3F`, `message_ref`); without it no reference. */
|
|
132
|
+
refPrefix?: string | undefined;
|
|
133
|
+
/** The brand's name: the prefilled message's greeting («Bonjour Aquafix»). */
|
|
134
|
+
brand?: string | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* The estimate's question whose answer is the message's timing line and
|
|
137
|
+
* in its preview (`{ input: "frequency" }` → «Délai souhaité : 2 sem.»);
|
|
138
|
+
* nothing until it is answered, nor for «I don't know».
|
|
139
|
+
*/
|
|
140
|
+
messengerTiming?: {
|
|
141
|
+
input: string;
|
|
142
|
+
} | undefined;
|
|
120
143
|
/** The site's assignment: on every event, and posted with the form. Slugs only. */
|
|
121
144
|
experiment?: Experiment | undefined;
|
|
122
145
|
timeZone?: string | undefined;
|