@evinvest/kitstart 0.16.0 → 0.18.0
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 +179 -19
- package/dist/core/ab-gate.js +3 -1
- package/dist/core/ab-switcher.d.ts +8 -0
- package/dist/core/ab-switcher.js +11 -0
- package/dist/core/accept.d.ts +14 -0
- package/dist/core/accept.js +21 -3
- package/dist/core/analytics.d.ts +28 -3
- package/dist/core/analytics.js +46 -2
- 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/site.d.ts +8 -1
- package/dist/core/site.js +22 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.js +8 -5
- package/dist/next/quote-route.d.ts +7 -0
- package/dist/next/quote-route.js +9 -9
- package/dist/react/AbSwitcherPanel.d.ts +3 -1
- package/dist/react/AbSwitcherPanel.js +10 -6
- package/dist/react/AnalyticsBoundary.d.ts +3 -2
- package/dist/react/AnalyticsBoundary.js +7 -20
- package/dist/react/LeadCapture.d.ts +24 -1
- package/dist/react/LeadCapture.js +96 -11
- 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/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 +24 -4
- 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/ab-switcher-types.d.ts +1 -1
- package/dist/react/analytics-context.d.ts +9 -3
- package/dist/react/analytics-context.js +7 -3
- package/dist/react/index.d.ts +1 -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 +5 -2
- package/template/app/quote/route.ts +3 -0
- package/template/flake.nix +1 -1
- package/template/package.json +2 -2
- package/template/tests/e2e/ab-switcher.spec.ts +2 -0
package/dist/core/channels.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { telHref, whatsappHref } from "@evinvest/marketing";
|
|
2
2
|
import { DEFAULT_TIME_ZONE, isOpenAt, nextOpening } from "./place/hours.js";
|
|
3
|
+
import { telegramHref } from "./messenger.js";
|
|
3
4
|
import { isMobilePhone, normalizePhone } from "./phone.js";
|
|
4
5
|
// Open: a person answers, so the call leads — calls convert many times what a
|
|
5
6
|
// form does. Closed: a call rings out, so the channels that wait for the
|
|
@@ -21,6 +22,9 @@ export function resolveChannels(facts, options) {
|
|
|
21
22
|
sms: facts.phone !== null && (facts.sms ?? isMobilePhone(facts.phone)),
|
|
22
23
|
callback: facts.callback ?? true,
|
|
23
24
|
form: true,
|
|
25
|
+
// Never in the lists below: a bot is offered by a `messenger` variant,
|
|
26
|
+
// with the lead posted first, not as one more bare link.
|
|
27
|
+
telegram: false,
|
|
24
28
|
};
|
|
25
29
|
const order = (open === false ? CLOSED : OPEN).filter(c => has[c]);
|
|
26
30
|
const prefer = options.prefer;
|
|
@@ -35,7 +39,11 @@ export function smsHref(phone, body) {
|
|
|
35
39
|
const number = normalizePhone(phone) ?? phone.replace(/[^\d+]/g, "");
|
|
36
40
|
return body ? `sms:${number}?&body=${encodeURIComponent(body)}` : `sms:${number}`;
|
|
37
41
|
}
|
|
38
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* The link a channel opens, or `null` for the two that are forms on the page.
|
|
44
|
+
* `message` is the prefilled text — for `telegram`, the bot's `start`
|
|
45
|
+
* parameter: the lead's `message_ref`.
|
|
46
|
+
*/
|
|
39
47
|
export function channelHref(channel, facts, message) {
|
|
40
48
|
switch (channel) {
|
|
41
49
|
case "phone":
|
|
@@ -44,6 +52,8 @@ export function channelHref(channel, facts, message) {
|
|
|
44
52
|
return facts.whatsapp ? whatsappHref(facts.whatsapp, message) : null;
|
|
45
53
|
case "sms":
|
|
46
54
|
return facts.phone ? smsHref(facts.phone, message) : null;
|
|
55
|
+
case "telegram":
|
|
56
|
+
return facts.telegram ? telegramHref(facts.telegram, message) : null;
|
|
47
57
|
case "callback":
|
|
48
58
|
case "form":
|
|
49
59
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LeadCaptureMessengerText } from "./messenger-text.js";
|
|
1
2
|
/**
|
|
2
3
|
* Every word `LeadCapture` prints. Plain strings, not the copy contract's
|
|
3
4
|
* `Said<F>`: the form is a client island, and a function cannot cross from a
|
|
@@ -10,9 +11,12 @@
|
|
|
10
11
|
*
|
|
11
12
|
* The flows' words (`LeadCaptureFlowText`) are optional here so a brand's own
|
|
12
13
|
* text object written before them still type-checks; a key left out falls
|
|
13
|
-
* back to the kit's, in the page's language (`flowTextOf`).
|
|
14
|
+
* back to the kit's, in the page's language (`flowTextOf`). The `messenger`
|
|
15
|
+
* variants' words (`LeadCaptureMessengerText`) likewise (`messengerTextOf`),
|
|
16
|
+
* their defaults in `LEAD_CAPTURE_MESSENGER_TEXT` — not spread in here: this
|
|
17
|
+
* module reaches the client island, and only a variant's chunk needs them.
|
|
14
18
|
*/
|
|
15
|
-
export interface LeadCaptureText extends Partial<LeadCaptureFlowText> {
|
|
19
|
+
export interface LeadCaptureText extends Partial<LeadCaptureFlowText>, Partial<LeadCaptureMessengerText> {
|
|
16
20
|
title: string;
|
|
17
21
|
lede: string;
|
|
18
22
|
needLabel: string;
|
package/dist/core/lead.d.ts
CHANGED
|
@@ -60,6 +60,12 @@ export interface LeadSchema<S extends string> {
|
|
|
60
60
|
* the landing contract holds it to the form's phone rule, so the server
|
|
61
61
|
* never refuses a number the form let through. A bare string is a reason
|
|
62
62
|
* about the whole form (`field: "form"`).
|
|
63
|
+
*
|
|
64
|
+
* A messenger lead (`whatsapp`, `telegram`) answers to it too: the
|
|
65
|
+
* candidate carries its `channel`, and `validateLead` asks no phone of one
|
|
66
|
+
* — the chat is how we answer. The contract holds a brand's own rule to
|
|
67
|
+
* that (`messengerRuleDisagreements`): a messenger lead is never refused
|
|
68
|
+
* for a postcode or a phone it was not asked for.
|
|
63
69
|
*/
|
|
64
70
|
validate?: (lead: LeadCandidate) => LeadVerdict;
|
|
65
71
|
/**
|
|
@@ -84,13 +90,17 @@ export interface LeadSchema<S extends string> {
|
|
|
84
90
|
flows?: LeadFlows;
|
|
85
91
|
}
|
|
86
92
|
/**
|
|
87
|
-
* How the lead was asked for: the quote form,
|
|
88
|
-
* to a phone number
|
|
89
|
-
*
|
|
93
|
+
* How the lead was asked for: the quote form, "call me back" — the form cut
|
|
94
|
+
* to a phone number — or a messenger the visitor was sent on to, the lead
|
|
95
|
+
* posted first (`whatsapp`, `telegram`). Posted as `CHANNEL_FIELD`; anything
|
|
96
|
+
* else, or nothing (a page cached before the field existed), is `form`.
|
|
90
97
|
*/
|
|
91
|
-
export type LeadChannel = "form" | "callback";
|
|
98
|
+
export type LeadChannel = "form" | "callback" | "whatsapp" | "telegram";
|
|
92
99
|
export declare const LEAD_CHANNELS: readonly LeadChannel[];
|
|
93
100
|
export declare const CHANNEL_FIELD = "channel";
|
|
101
|
+
/** A lead whose answer goes by chat: the phone is optional, no consent is asked. */
|
|
102
|
+
export type MessengerChannel = Extract<LeadChannel, "whatsapp" | "telegram">;
|
|
103
|
+
export declare function isMessengerChannel(channel: LeadChannel | undefined): channel is MessengerChannel;
|
|
94
104
|
/**
|
|
95
105
|
* The callback's consent: a checkbox whose value is the sentence it shows, in
|
|
96
106
|
* the language it shows it, so the row keeps what was agreed to. Absent → the
|
|
@@ -111,7 +121,8 @@ export declare const SUBMISSION_ID: RegExp;
|
|
|
111
121
|
export declare const LEAD_ERROR_PARAM = "lead_error";
|
|
112
122
|
/** A refusal the server sent back: which of the card's forms, and the field it is about. */
|
|
113
123
|
export interface LeadError {
|
|
114
|
-
|
|
124
|
+
/** A messenger lead is posted behind the visitor's back: no form of the card shows its refusal. */
|
|
125
|
+
channel: Extract<LeadChannel, "form" | "callback">;
|
|
115
126
|
field: string;
|
|
116
127
|
}
|
|
117
128
|
/** What a refused field may be named: a slug, never markup. */
|
|
@@ -150,8 +161,14 @@ export declare const CARD_FIELD = "card";
|
|
|
150
161
|
export declare const CARD_ID: RegExp;
|
|
151
162
|
/** A rule's answer as a rejection: a bare reason, or a field that is not a slug, is about the `form`. */
|
|
152
163
|
export declare function rejectionOf(verdict: LeadVerdict): LeadRejection | null;
|
|
153
|
-
/**
|
|
154
|
-
|
|
164
|
+
/**
|
|
165
|
+
* The default form rule: a number we can call — the one the form blocks on
|
|
166
|
+
* (`phoneProblem`). A messenger lead needs none, but one typed must be a
|
|
167
|
+
* number: the operator may call it.
|
|
168
|
+
*/
|
|
169
|
+
export declare function validateLead(lead: Pick<LeadCandidate, "mobile"> & {
|
|
170
|
+
channel?: LeadChannel | undefined;
|
|
171
|
+
}): LeadRejection | null;
|
|
155
172
|
/** The default callback rule: the same number rule, worded for the log. */
|
|
156
173
|
export declare function validateCallbackLead(lead: Pick<LeadCandidate, "mobile">): LeadRejection | null;
|
|
157
174
|
/**
|
|
@@ -189,6 +206,12 @@ export interface Lead {
|
|
|
189
206
|
flow?: LeadFlow;
|
|
190
207
|
/** Only on an `estimate` or `fixed` lead: the price the server computed. */
|
|
191
208
|
price?: LeadPrice;
|
|
209
|
+
/**
|
|
210
|
+
* The reference the visitor's chat message carries (`AQ-7K3F`,
|
|
211
|
+
* `MESSAGE_REF`), posted by the card that minted it — on a form lead too,
|
|
212
|
+
* when its success sends the visitor on to a messenger. Not personal data.
|
|
213
|
+
*/
|
|
214
|
+
messageRef?: string;
|
|
192
215
|
}
|
|
193
216
|
/**
|
|
194
217
|
* The price a lead was taken at — the server's own number, from the posted
|
|
@@ -250,10 +273,10 @@ export interface LeadStore {
|
|
|
250
273
|
* The lead schema's version, shared by every adapter: 1 the Rust server's
|
|
251
274
|
* table (`job`, `zip`, `mobile`, `at`), 2 + the place, 3 + the spam verdict,
|
|
252
275
|
* 4 + the brand's extras, 5 + the channel and the callback's consent, 6 + the
|
|
253
|
-
* script's submission id, unique, 7 + the flow and the price it was taken at
|
|
254
|
-
* Append only.
|
|
276
|
+
* script's submission id, unique, 7 + the flow and the price it was taken at,
|
|
277
|
+
* 8 + the messenger reference (`message_ref`). Append only.
|
|
255
278
|
*/
|
|
256
|
-
export declare const LEAD_SCHEMA_VERSION =
|
|
279
|
+
export declare const LEAD_SCHEMA_VERSION = 8;
|
|
257
280
|
/** A field is capped, not rejected: a long answer is still a customer. */
|
|
258
281
|
export declare const MAX_FIELD = 200;
|
|
259
282
|
/**
|
package/dist/core/lead.js
CHANGED
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
* `LeadSchema.wire` names the posted fields (a page cached before a rename
|
|
10
10
|
* still delivers) — both deliberate departures from the first design.
|
|
11
11
|
*/
|
|
12
|
+
import { MESSAGE_REF_FIELD, messageRefOf } from "./messenger.js";
|
|
12
13
|
import { normalizePhone, phoneProblem } from "./phone.js";
|
|
13
|
-
export const LEAD_CHANNELS = ["form", "callback"];
|
|
14
|
+
export const LEAD_CHANNELS = ["form", "callback", "whatsapp", "telegram"];
|
|
14
15
|
export const CHANNEL_FIELD = "channel";
|
|
16
|
+
export function isMessengerChannel(channel) {
|
|
17
|
+
return channel === "whatsapp" || channel === "telegram";
|
|
18
|
+
}
|
|
15
19
|
/**
|
|
16
20
|
* The callback's consent: a checkbox whose value is the sentence it shows, in
|
|
17
21
|
* the language it shows it, so the row keeps what was agreed to. Absent → the
|
|
@@ -84,8 +88,14 @@ export function rejectionOf(verdict) {
|
|
|
84
88
|
return { field: "form", why: verdict };
|
|
85
89
|
return FIELD_SLUG.test(verdict.field) ? verdict : { field: "form", why: verdict.why };
|
|
86
90
|
}
|
|
87
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* The default form rule: a number we can call — the one the form blocks on
|
|
93
|
+
* (`phoneProblem`). A messenger lead needs none, but one typed must be a
|
|
94
|
+
* number: the operator may call it.
|
|
95
|
+
*/
|
|
88
96
|
export function validateLead(lead) {
|
|
97
|
+
if (isMessengerChannel(lead.channel) && lead.mobile.trim() === "")
|
|
98
|
+
return null;
|
|
89
99
|
const problem = phoneProblem(lead.mobile);
|
|
90
100
|
return problem === null ? null : { field: "phone", why: problem === "required" ? "a phone number" : "a phone number we can call" };
|
|
91
101
|
}
|
|
@@ -113,10 +123,10 @@ export function channelOf(lead) {
|
|
|
113
123
|
* The lead schema's version, shared by every adapter: 1 the Rust server's
|
|
114
124
|
* table (`job`, `zip`, `mobile`, `at`), 2 + the place, 3 + the spam verdict,
|
|
115
125
|
* 4 + the brand's extras, 5 + the channel and the callback's consent, 6 + the
|
|
116
|
-
* script's submission id, unique, 7 + the flow and the price it was taken at
|
|
117
|
-
* Append only.
|
|
126
|
+
* script's submission id, unique, 7 + the flow and the price it was taken at,
|
|
127
|
+
* 8 + the messenger reference (`message_ref`). Append only.
|
|
118
128
|
*/
|
|
119
|
-
export const LEAD_SCHEMA_VERSION =
|
|
129
|
+
export const LEAD_SCHEMA_VERSION = 8;
|
|
120
130
|
/** A field is capped, not rejected: a long answer is still a customer. */
|
|
121
131
|
export const MAX_FIELD = 200;
|
|
122
132
|
// C0 and C1 controls, DEL, and the Unicode line and paragraph separators.
|
|
@@ -148,18 +158,21 @@ export function readCandidate(schema, form, placeSlug) {
|
|
|
148
158
|
extras[extra.name] = value;
|
|
149
159
|
}
|
|
150
160
|
const mobile = field(form, schema.wire.mobile, MAX_FIELD) ?? "";
|
|
151
|
-
const
|
|
152
|
-
const
|
|
161
|
+
const posted = form.get(CHANNEL_FIELD);
|
|
162
|
+
const channel = LEAD_CHANNELS.find(c => c === posted) ?? "form";
|
|
163
|
+
const consent = channel === "callback" ? field(form, CONSENT_FIELD, MAX_CONSENT) : null;
|
|
153
164
|
const submission = field(form, SUBMISSION_FIELD, 64);
|
|
165
|
+
const messageRef = messageRefOf(form.get(MESSAGE_REF_FIELD));
|
|
154
166
|
return {
|
|
155
167
|
subject: field(form, schema.wire.subject, MAX_FIELD) ?? "",
|
|
156
168
|
locality: field(form, schema.wire.locality, MAX_FIELD) ?? "",
|
|
157
169
|
mobile: schema.mobileFormat === "e164" ? (normalizePhone(mobile) ?? mobile) : mobile,
|
|
158
170
|
extras,
|
|
159
171
|
placeSlug,
|
|
160
|
-
channel
|
|
172
|
+
channel,
|
|
161
173
|
...(consent ? { consentText: consent } : {}),
|
|
162
174
|
...(submission && SUBMISSION_ID.test(submission) ? { submissionId: submission } : {}),
|
|
175
|
+
...(messageRef ? { messageRef } : {}),
|
|
163
176
|
};
|
|
164
177
|
}
|
|
165
178
|
/** The rule the candidate's channel answers to: the field a refusal is about, or `null`. */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The words of the prefilled message, one template per line; `{brand}`, `{need}`, … filled in. */
|
|
2
|
+
export interface MessengerMessageText {
|
|
3
|
+
messageHello: string;
|
|
4
|
+
messageNeed: string;
|
|
5
|
+
messagePostcode: string;
|
|
6
|
+
messagePrice: string;
|
|
7
|
+
messageTiming: string;
|
|
8
|
+
messageRef: string;
|
|
9
|
+
}
|
|
10
|
+
/** What the message says. Nothing typed about the person — no phone, no name. */
|
|
11
|
+
export interface MessengerMessageInput {
|
|
12
|
+
brand: string;
|
|
13
|
+
/** The need's label. */
|
|
14
|
+
need?: string | null | undefined;
|
|
15
|
+
postcode?: string | null | undefined;
|
|
16
|
+
/** The estimate the page showed, formatted (`≈ 77 €`). */
|
|
17
|
+
price?: string | null | undefined;
|
|
18
|
+
/** When the job is wanted — an urgency or a frequency label. */
|
|
19
|
+
timing?: string | null | undefined;
|
|
20
|
+
ref?: string | null | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** WhatsApp shows a long message whole, but the link has to fit a QR code a phone can read off a screen. */
|
|
23
|
+
export declare const MAX_MESSENGER_MESSAGE = 400;
|
|
24
|
+
/**
|
|
25
|
+
* The prefilled message: a greeting, the need, the postcode, the estimate
|
|
26
|
+
* and the timing when known, and the reference last — the line the operator
|
|
27
|
+
* matches the chat by. A line without its value is left out. Past
|
|
28
|
+
* `MAX_MESSENGER_MESSAGE`, the optional lines go from the bottom up; the
|
|
29
|
+
* greeting and the reference stay.
|
|
30
|
+
*/
|
|
31
|
+
export declare function messengerMessage(input: MessengerMessageInput, text: MessengerMessageText): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { fillText } from "./lead-capture-format.js";
|
|
2
|
+
/** WhatsApp shows a long message whole, but the link has to fit a QR code a phone can read off a screen. */
|
|
3
|
+
export const MAX_MESSENGER_MESSAGE = 400;
|
|
4
|
+
// C0 and C1 controls, DEL, and the Unicode line and paragraph separators: a value is one line.
|
|
5
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g;
|
|
6
|
+
/** One line of the message, capped: a value is a label, not a letter. */
|
|
7
|
+
const clean = (value, max = 80) => (value ?? "").replace(CONTROL, " ").trim().slice(0, max);
|
|
8
|
+
/**
|
|
9
|
+
* The prefilled message: a greeting, the need, the postcode, the estimate
|
|
10
|
+
* and the timing when known, and the reference last — the line the operator
|
|
11
|
+
* matches the chat by. A line without its value is left out. Past
|
|
12
|
+
* `MAX_MESSENGER_MESSAGE`, the optional lines go from the bottom up; the
|
|
13
|
+
* greeting and the reference stay.
|
|
14
|
+
*/
|
|
15
|
+
export function messengerMessage(input, text) {
|
|
16
|
+
const line = (template, key, value) => {
|
|
17
|
+
const v = clean(value);
|
|
18
|
+
return v === "" ? null : fillText(template, { [key]: v });
|
|
19
|
+
};
|
|
20
|
+
const hello = fillText(text.messageHello, { brand: clean(input.brand) });
|
|
21
|
+
const ref = input.ref ? line(text.messageRef, "ref", input.ref) : null;
|
|
22
|
+
const optional = [
|
|
23
|
+
line(text.messageNeed, "need", input.need),
|
|
24
|
+
line(text.messagePostcode, "postcode", input.postcode),
|
|
25
|
+
line(text.messagePrice, "price", input.price),
|
|
26
|
+
line(text.messageTiming, "timing", input.timing),
|
|
27
|
+
].filter((l) => l !== null);
|
|
28
|
+
const compose = (lines) => [hello, ...lines, ...(ref ? [ref] : [])].join("\n");
|
|
29
|
+
let kept = optional;
|
|
30
|
+
while (kept.length > 0 && compose(kept).length > MAX_MESSENGER_MESSAGE)
|
|
31
|
+
kept = kept.slice(0, -1);
|
|
32
|
+
return compose(kept).slice(0, MAX_MESSENGER_MESSAGE);
|
|
33
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { MessengerMessageText } from "./messenger-message.js";
|
|
2
|
+
/**
|
|
3
|
+
* What a `messenger` variant of `LeadCapture` prints — and the prefilled
|
|
4
|
+
* message's lines. Optional in `LeadCaptureText`, as the flows' words are: a
|
|
5
|
+
* brand's text from before the variants still type-checks, and a key left
|
|
6
|
+
* out falls back to the kit's in the page's language (`messengerTextOf`).
|
|
7
|
+
*
|
|
8
|
+
* Apart from the flows' words on purpose: only a variant's own chunk reads
|
|
9
|
+
* them, so the control's page never downloads them.
|
|
10
|
+
*/
|
|
11
|
+
export interface LeadCaptureMessengerText extends MessengerMessageText {
|
|
12
|
+
/** The WhatsApp button. */
|
|
13
|
+
messengerWhatsappCta: string;
|
|
14
|
+
/** `saga`: the WhatsApp step's button. */
|
|
15
|
+
messengerOpenWhatsapp: string;
|
|
16
|
+
/** The Telegram button. */
|
|
17
|
+
messengerTelegramCta: string;
|
|
18
|
+
/** Under the submit when the place has a bot: the way to it. */
|
|
19
|
+
messengerViaTelegram: string;
|
|
20
|
+
/** A channel's name in a picker, a tile, a segment — and its line under it. */
|
|
21
|
+
messengerOptionWhatsapp: string;
|
|
22
|
+
messengerOptionWhatsappNote: string;
|
|
23
|
+
messengerOptionCall: string;
|
|
24
|
+
messengerOptionCallNote: string;
|
|
25
|
+
messengerOptionTelegram: string;
|
|
26
|
+
messengerOptionTelegramNote: string;
|
|
27
|
+
/** The badge on the recommended channel. */
|
|
28
|
+
messengerRecommended: string;
|
|
29
|
+
/** Names a channel picker for assistive technology. */
|
|
30
|
+
messengerChannelLabel: string;
|
|
31
|
+
/** The way to the phone, beside the messengers. */
|
|
32
|
+
messengerCallback: string;
|
|
33
|
+
/** `swap`, `split`: the square back to WhatsApp, for assistive technology. */
|
|
34
|
+
messengerBackToWhatsapp: string;
|
|
35
|
+
/** The phone field's placeholder when the phone is optional. */
|
|
36
|
+
messengerPhoneOptional: string;
|
|
37
|
+
/** `select`: under the field, by channel. */
|
|
38
|
+
messengerWhatsappHint: string;
|
|
39
|
+
messengerTelegramHint: string;
|
|
40
|
+
/** The message's preview in the slot: its heading, and an optional line after the message. */
|
|
41
|
+
messengerPreviewTitle: string;
|
|
42
|
+
/** The estimate in the preview, `{price}` the amount («env. {price}»). */
|
|
43
|
+
messengerPreviewPrice: string;
|
|
44
|
+
messengerPreviewNote: string;
|
|
45
|
+
/** Telegram's card in the slot. */
|
|
46
|
+
messengerBotTitle: string;
|
|
47
|
+
messengerBotLede: string;
|
|
48
|
+
/** `tiles`: over the tiles. */
|
|
49
|
+
messengerTilesLabel: string;
|
|
50
|
+
/** `thanks`: the success's ask, and its WhatsApp button. */
|
|
51
|
+
messengerThanksTitle: string;
|
|
52
|
+
messengerThanksCta: string;
|
|
53
|
+
/** `saga`: the channel screen. */
|
|
54
|
+
messengerSagaTitle: string;
|
|
55
|
+
messengerSagaLede: string;
|
|
56
|
+
messengerSagaFoot: string;
|
|
57
|
+
/** `saga`, `sheet`: back to the channels. */
|
|
58
|
+
messengerChange: string;
|
|
59
|
+
/** `saga`, `sheet`: the second screen's heading and lede, by channel. */
|
|
60
|
+
messengerWhatsappTitle: string;
|
|
61
|
+
messengerWhatsappLede: string;
|
|
62
|
+
messengerTelegramTitle: string;
|
|
63
|
+
messengerTelegramLede: string;
|
|
64
|
+
messengerCallTitle: string;
|
|
65
|
+
messengerCallLede: string;
|
|
66
|
+
/** `urgency`: the question, its two answers, and what each leads to. */
|
|
67
|
+
messengerUrgencyLabel: string;
|
|
68
|
+
messengerUrgencyYes: string;
|
|
69
|
+
messengerUrgencyNo: string;
|
|
70
|
+
/** The message's timing line for an urgent need, and for one that can wait. */
|
|
71
|
+
messengerUrgencyToday: string;
|
|
72
|
+
messengerUrgencyLater: string;
|
|
73
|
+
messengerUrgentSubmit: string;
|
|
74
|
+
messengerNotUrgentTitle: string;
|
|
75
|
+
/** `{ref}` the reference. */
|
|
76
|
+
messengerNotUrgentLede: string;
|
|
77
|
+
/** `sheet`: the card's one button and the drawer's heading. */
|
|
78
|
+
messengerSheetCta: string;
|
|
79
|
+
messengerSheetTitle: string;
|
|
80
|
+
/** `chip`: the chip, by channel. */
|
|
81
|
+
messengerChipWhatsapp: string;
|
|
82
|
+
messengerChipCall: string;
|
|
83
|
+
messengerChipTelegram: string;
|
|
84
|
+
/** `split`: the WhatsApp half of the row. */
|
|
85
|
+
messengerSplitCta: string;
|
|
86
|
+
/** In an app's own browser (Instagram, TikTok…): WhatsApp may not open from it. */
|
|
87
|
+
messengerInappHint: string;
|
|
88
|
+
messengerCopy: string;
|
|
89
|
+
messengerCopied: string;
|
|
90
|
+
/** Back from the messenger: `{channel}` its name, `{ref}` the reference. */
|
|
91
|
+
messengerReturnTitle: string;
|
|
92
|
+
messengerReturnBody: string;
|
|
93
|
+
messengerReopen: string;
|
|
94
|
+
messengerReturnFailed: string;
|
|
95
|
+
messengerReturnDone: string;
|
|
96
|
+
/** On a computer: the QR code that opens WhatsApp on the phone. */
|
|
97
|
+
messengerQrTitle: string;
|
|
98
|
+
messengerQrLede: string;
|
|
99
|
+
messengerQrWeb: string;
|
|
100
|
+
messengerQrAlt: string;
|
|
101
|
+
}
|
|
102
|
+
export declare const LEAD_CAPTURE_MESSENGER_TEXT: Readonly<Record<"fr" | "en", LeadCaptureMessengerText>>;
|
|
103
|
+
/** The variants' words for a page: the brand's, else the kit's in its language (English for `en*`, French otherwise). */
|
|
104
|
+
export declare function messengerTextOf(text: Partial<LeadCaptureMessengerText>, locale: string): LeadCaptureMessengerText;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const FR = {
|
|
2
|
+
messageHello: "Bonjour {brand} 👋",
|
|
3
|
+
messageNeed: "Je souhaite un devis : {need}",
|
|
4
|
+
messagePostcode: "Code postal : {postcode}",
|
|
5
|
+
messagePrice: "Estimation vue sur le site : {price}",
|
|
6
|
+
messageTiming: "Délai souhaité : {timing}",
|
|
7
|
+
messageRef: "Réf. {ref}",
|
|
8
|
+
messengerWhatsappCta: "Recevoir mon devis sur WhatsApp",
|
|
9
|
+
messengerOpenWhatsapp: "Ouvrir WhatsApp",
|
|
10
|
+
messengerTelegramCta: "Ouvrir Telegram",
|
|
11
|
+
messengerViaTelegram: "ou via Telegram",
|
|
12
|
+
messengerOptionWhatsapp: "WhatsApp",
|
|
13
|
+
messengerOptionWhatsappNote: "Recommandé · demande déjà rédigée",
|
|
14
|
+
messengerOptionCall: "Appel",
|
|
15
|
+
messengerOptionCallNote: "On vous rappelle rapidement",
|
|
16
|
+
messengerOptionTelegram: "Telegram",
|
|
17
|
+
messengerOptionTelegramNote: "Via notre bot Telegram",
|
|
18
|
+
messengerRecommended: "Recommandé",
|
|
19
|
+
messengerChannelLabel: "Canal de réponse",
|
|
20
|
+
messengerCallback: "Être rappelé",
|
|
21
|
+
messengerBackToWhatsapp: "Revenir à WhatsApp",
|
|
22
|
+
messengerPhoneOptional: "Téléphone (facultatif)",
|
|
23
|
+
messengerWhatsappHint: "WhatsApp s’ouvre, demande déjà rédigée. Numéro facultatif.",
|
|
24
|
+
messengerTelegramHint: "Telegram s’ouvre : notre bot vous répond.",
|
|
25
|
+
messengerPreviewTitle: "Votre message est prêt",
|
|
26
|
+
messengerPreviewPrice: "{price}",
|
|
27
|
+
messengerPreviewNote: "",
|
|
28
|
+
messengerBotTitle: "Notre bot vous envoie le devis",
|
|
29
|
+
messengerBotLede: "Telegram s’ouvre : appuyez sur « Démarrer », le devis arrive dans le chat.",
|
|
30
|
+
messengerTilesLabel: "Recevoir mon devis par",
|
|
31
|
+
messengerThanksTitle: "Plus rapide : envoyez une photo",
|
|
32
|
+
messengerThanksCta: "Envoyer la photo sur WhatsApp",
|
|
33
|
+
messengerSagaTitle: "Comment voulez-vous nous joindre ?",
|
|
34
|
+
messengerSagaLede: "Réponse rapide, sans engagement.",
|
|
35
|
+
messengerSagaFoot: "Réponse rapide · sans engagement",
|
|
36
|
+
messengerChange: "Changer de canal",
|
|
37
|
+
messengerWhatsappTitle: "Sur WhatsApp",
|
|
38
|
+
messengerWhatsappLede: "Puis WhatsApp s’ouvre, message prêt.",
|
|
39
|
+
messengerTelegramTitle: "Sur Telegram",
|
|
40
|
+
messengerTelegramLede: "Puis Telegram s’ouvre : notre bot vous répond.",
|
|
41
|
+
messengerCallTitle: "On vous rappelle",
|
|
42
|
+
messengerCallLede: "Laissez votre numéro.",
|
|
43
|
+
messengerUrgencyLabel: "C’est urgent ?",
|
|
44
|
+
messengerUrgencyYes: "Oui, aujourd’hui",
|
|
45
|
+
messengerUrgencyNo: "Non, je compare",
|
|
46
|
+
messengerUrgencyToday: "aujourd’hui",
|
|
47
|
+
messengerUrgencyLater: "pas pressé",
|
|
48
|
+
messengerUrgentSubmit: "Rappelez-moi tout de suite",
|
|
49
|
+
messengerNotUrgentTitle: "Pas pressé ? Message prêt",
|
|
50
|
+
messengerNotUrgentLede: "Un message sur WhatsApp suffit. Réf. {ref}.",
|
|
51
|
+
messengerSheetCta: "Recevoir mon devis",
|
|
52
|
+
messengerSheetTitle: "Où recevoir votre devis ?",
|
|
53
|
+
messengerChipWhatsapp: "Réponse sur WhatsApp",
|
|
54
|
+
messengerChipCall: "Réponse par appel",
|
|
55
|
+
messengerChipTelegram: "Réponse sur Telegram",
|
|
56
|
+
messengerSplitCta: "Devis sur WhatsApp",
|
|
57
|
+
messengerInappHint: "WhatsApp ne s’ouvre pas ? Menu ⋯ → « Ouvrir dans le navigateur ».",
|
|
58
|
+
messengerCopy: "Copier le message",
|
|
59
|
+
messengerCopied: "Message copié",
|
|
60
|
+
messengerReturnTitle: "Message envoyé ?",
|
|
61
|
+
messengerReturnBody: "Si oui, on vous répond sur {channel} rapidement, référence {ref}.",
|
|
62
|
+
messengerReopen: "Rouvrir {channel}",
|
|
63
|
+
messengerReturnFailed: "Je n’ai pas pu envoyer → être rappelé",
|
|
64
|
+
messengerReturnDone: "C’est envoyé",
|
|
65
|
+
messengerQrTitle: "Scannez avec votre téléphone",
|
|
66
|
+
messengerQrLede: "WhatsApp s’ouvre avec votre demande déjà rédigée.",
|
|
67
|
+
messengerQrWeb: "ou ouvrir WhatsApp Web →",
|
|
68
|
+
messengerQrAlt: "QR code qui ouvre WhatsApp avec votre demande",
|
|
69
|
+
};
|
|
70
|
+
const EN = {
|
|
71
|
+
messageHello: "Hello {brand} 👋",
|
|
72
|
+
messageNeed: "I would like a quote: {need}",
|
|
73
|
+
messagePostcode: "Postcode: {postcode}",
|
|
74
|
+
messagePrice: "Estimate seen on the site: {price}",
|
|
75
|
+
messageTiming: "When: {timing}",
|
|
76
|
+
messageRef: "Ref. {ref}",
|
|
77
|
+
messengerWhatsappCta: "Get my quote on WhatsApp",
|
|
78
|
+
messengerOpenWhatsapp: "Open WhatsApp",
|
|
79
|
+
messengerTelegramCta: "Open Telegram",
|
|
80
|
+
messengerViaTelegram: "or via Telegram",
|
|
81
|
+
messengerOptionWhatsapp: "WhatsApp",
|
|
82
|
+
messengerOptionWhatsappNote: "Recommended · request already written",
|
|
83
|
+
messengerOptionCall: "Call",
|
|
84
|
+
messengerOptionCallNote: "We call you back shortly",
|
|
85
|
+
messengerOptionTelegram: "Telegram",
|
|
86
|
+
messengerOptionTelegramNote: "Through our Telegram bot",
|
|
87
|
+
messengerRecommended: "Recommended",
|
|
88
|
+
messengerChannelLabel: "Where to answer you",
|
|
89
|
+
messengerCallback: "Call me back",
|
|
90
|
+
messengerBackToWhatsapp: "Back to WhatsApp",
|
|
91
|
+
messengerPhoneOptional: "Phone (optional)",
|
|
92
|
+
messengerWhatsappHint: "WhatsApp opens with your request written. Phone optional.",
|
|
93
|
+
messengerTelegramHint: "Telegram opens: our bot answers you.",
|
|
94
|
+
messengerPreviewTitle: "Your message is ready",
|
|
95
|
+
messengerPreviewPrice: "{price}",
|
|
96
|
+
messengerPreviewNote: "",
|
|
97
|
+
messengerBotTitle: "Our bot sends you the quote",
|
|
98
|
+
messengerBotLede: "Telegram opens: tap “Start” and the quote arrives in the chat.",
|
|
99
|
+
messengerTilesLabel: "Get my quote by",
|
|
100
|
+
messengerThanksTitle: "Faster: send a photo",
|
|
101
|
+
messengerThanksCta: "Send the photo on WhatsApp",
|
|
102
|
+
messengerSagaTitle: "How would you like to reach us?",
|
|
103
|
+
messengerSagaLede: "A quick answer, no commitment.",
|
|
104
|
+
messengerSagaFoot: "Quick answer · no commitment",
|
|
105
|
+
messengerChange: "Change channel",
|
|
106
|
+
messengerWhatsappTitle: "On WhatsApp",
|
|
107
|
+
messengerWhatsappLede: "Then WhatsApp opens with the message ready.",
|
|
108
|
+
messengerTelegramTitle: "On Telegram",
|
|
109
|
+
messengerTelegramLede: "Then Telegram opens: our bot answers you.",
|
|
110
|
+
messengerCallTitle: "We call you back",
|
|
111
|
+
messengerCallLede: "Leave your number.",
|
|
112
|
+
messengerUrgencyLabel: "Is it urgent?",
|
|
113
|
+
messengerUrgencyYes: "Yes, today",
|
|
114
|
+
messengerUrgencyNo: "No, comparing",
|
|
115
|
+
messengerUrgencyToday: "today",
|
|
116
|
+
messengerUrgencyLater: "no rush",
|
|
117
|
+
messengerUrgentSubmit: "Call me right away",
|
|
118
|
+
messengerNotUrgentTitle: "No rush? Message ready",
|
|
119
|
+
messengerNotUrgentLede: "A WhatsApp message is enough. Ref. {ref}.",
|
|
120
|
+
messengerSheetCta: "Get my quote",
|
|
121
|
+
messengerSheetTitle: "Where should we send your quote?",
|
|
122
|
+
messengerChipWhatsapp: "Answer on WhatsApp",
|
|
123
|
+
messengerChipCall: "Answer by phone",
|
|
124
|
+
messengerChipTelegram: "Answer on Telegram",
|
|
125
|
+
messengerSplitCta: "Quote on WhatsApp",
|
|
126
|
+
messengerInappHint: "WhatsApp does not open? Menu ⋯ → “Open in browser”.",
|
|
127
|
+
messengerCopy: "Copy the message",
|
|
128
|
+
messengerCopied: "Message copied",
|
|
129
|
+
messengerReturnTitle: "Message sent?",
|
|
130
|
+
messengerReturnBody: "If so, we answer you on {channel} shortly, reference {ref}.",
|
|
131
|
+
messengerReopen: "Open {channel} again",
|
|
132
|
+
messengerReturnFailed: "I could not send it → call me back",
|
|
133
|
+
messengerReturnDone: "It is sent",
|
|
134
|
+
messengerQrTitle: "Scan with your phone",
|
|
135
|
+
messengerQrLede: "WhatsApp opens with your request already written.",
|
|
136
|
+
messengerQrWeb: "or open WhatsApp Web →",
|
|
137
|
+
messengerQrAlt: "QR code that opens WhatsApp with your request",
|
|
138
|
+
};
|
|
139
|
+
export const LEAD_CAPTURE_MESSENGER_TEXT = { fr: FR, en: EN };
|
|
140
|
+
/** The variants' words for a page: the brand's, else the kit's in its language (English for `en*`, French otherwise). */
|
|
141
|
+
export function messengerTextOf(text, locale) {
|
|
142
|
+
const kit = locale.startsWith("en") ? EN : FR;
|
|
143
|
+
const out = { ...kit };
|
|
144
|
+
for (const key of Object.keys(kit)) {
|
|
145
|
+
const own = text[key];
|
|
146
|
+
if (own !== undefined)
|
|
147
|
+
out[key] = own;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The messenger channels: WhatsApp, where the customer sends our prefilled
|
|
3
|
+
* message, and Telegram, the brand's bot opened with a deep link. Either way
|
|
4
|
+
* the lead is posted first, carrying a short reference the customer's message
|
|
5
|
+
* carries too (`message_ref`) — the one thing that joins the chat to the lead.
|
|
6
|
+
*/
|
|
7
|
+
/** The form field the reference is posted under. */
|
|
8
|
+
export declare const MESSAGE_REF_FIELD = "message_ref";
|
|
9
|
+
/**
|
|
10
|
+
* `<PREFIX>-<4..8 chars>` in Crockford base32 without I, L, O, U — read aloud
|
|
11
|
+
* and typed back without a mix-up. The panel's own rule, the same regex.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MESSAGE_REF: RegExp;
|
|
14
|
+
/** A brand's reference prefix (`AQ`, `VF`). */
|
|
15
|
+
export declare const MESSAGE_REF_PREFIX: RegExp;
|
|
16
|
+
/** The random bytes a reference is drawn from; a test passes its own. */
|
|
17
|
+
export type RandomBytes = (length: number) => Uint8Array;
|
|
18
|
+
/**
|
|
19
|
+
* A fresh reference, `AQ-7K3F`. Not a secret and not unique — 32⁴ per prefix
|
|
20
|
+
* collide sooner or later, and the panel resolves a reference to the brand's
|
|
21
|
+
* newest lead — just short enough to survive a chat.
|
|
22
|
+
*/
|
|
23
|
+
export declare function newMessageRef(prefix: string, random?: RandomBytes): string;
|
|
24
|
+
/** A posted reference, when it is one; anything else is dropped, never refused. */
|
|
25
|
+
export declare function messageRefOf(value: unknown): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* A Telegram bot's username, without `@`: Telegram's own rule for a bot — 5
|
|
28
|
+
* to 32 characters, a letter first, ending in `bot` — and the panel's.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TELEGRAM_BOT: RegExp;
|
|
31
|
+
/**
|
|
32
|
+
* The bot's deep link, `https://t.me/<bot>?start=<ref>`. The `start`
|
|
33
|
+
* parameter allows `A-Za-z0-9_-` only — a reference always is — so anything
|
|
34
|
+
* else is left off rather than encoded into a link the bot would not read.
|
|
35
|
+
*/
|
|
36
|
+
export declare function telegramHref(bot: string, start?: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* What a card may offer for the messengers (`messengerFacts`): the place's
|
|
39
|
+
* own WhatsApp number — never the brand's phone as a fallback, the chat is
|
|
40
|
+
* answered by hand — and its bot. `null` → not offered.
|
|
41
|
+
*/
|
|
42
|
+
export interface MessengerFacts {
|
|
43
|
+
whatsapp: string | null;
|
|
44
|
+
telegram: string | null;
|
|
45
|
+
}
|
|
46
|
+
/** Which messengers a card offered, as every event of a `LeadCapture` says it (`channels_available`). */
|
|
47
|
+
export type ChannelsAvailable = "wa,tg" | "wa" | "tg" | "none";
|
|
48
|
+
export declare function channelsAvailable(facts: MessengerFacts | null | undefined): ChannelsAvailable;
|
|
49
|
+
/**
|
|
50
|
+
* How a `LeadCapture` offers the messengers — an arm of the site's
|
|
51
|
+
* `lead_channel` experiment; `undefined` is the control. Every kind falls
|
|
52
|
+
* back to the control when the place has no WhatsApp (`urgency` keeps its
|
|
53
|
+
* question; `thanks` keeps its Telegram button), and loses its Telegram
|
|
54
|
+
* pieces when the place has no bot.
|
|
55
|
+
*
|
|
56
|
+
* - `select`: a channel select inside the phone field, before or after it;
|
|
57
|
+
* - `segment`: «WhatsApp | Appel» over a fixed slot — the message or the phone;
|
|
58
|
+
* - `tiles`: WhatsApp / Telegram / Appel tiles over the slot;
|
|
59
|
+
* - `thanks`: the control's form; its success offers WhatsApp (a photo) and Telegram;
|
|
60
|
+
* - `swap`: no phone until «Être rappelé» swaps the WhatsApp button for it;
|
|
61
|
+
* - `saga`: the channel first, on a screen of its own, then the job;
|
|
62
|
+
* - `urgency`: «C’est urgent ?» — posted as the brand's extra `field` — picks a call or WhatsApp;
|
|
63
|
+
* - `sheet`: one button, the channels in a drawer;
|
|
64
|
+
* - `chip`: the lede becomes a channel chip with a menu;
|
|
65
|
+
* - `split`: a WhatsApp button with Telegram and call squares beside it.
|
|
66
|
+
*/
|
|
67
|
+
export type MessengerVariant = {
|
|
68
|
+
kind: "select";
|
|
69
|
+
side: "prefix" | "suffix";
|
|
70
|
+
} | {
|
|
71
|
+
kind: "segment";
|
|
72
|
+
} | {
|
|
73
|
+
kind: "tiles";
|
|
74
|
+
} | {
|
|
75
|
+
kind: "thanks";
|
|
76
|
+
} | {
|
|
77
|
+
kind: "swap";
|
|
78
|
+
} | {
|
|
79
|
+
kind: "saga";
|
|
80
|
+
} | {
|
|
81
|
+
kind: "urgency";
|
|
82
|
+
field: string;
|
|
83
|
+
} | {
|
|
84
|
+
kind: "sheet";
|
|
85
|
+
} | {
|
|
86
|
+
kind: "chip";
|
|
87
|
+
} | {
|
|
88
|
+
kind: "split";
|
|
89
|
+
};
|
|
90
|
+
export type MessengerKind = MessengerVariant["kind"];
|
|
91
|
+
/** A channel the visitor picked in a variant: a messenger, or the phone. */
|
|
92
|
+
export type MessengerMode = "whatsapp" | "telegram" | "call";
|