@escapenavigator/hooks 2.1.22 → 2.1.23
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/consent-checkboxes/build-consent-urls.d.ts +5 -1
- package/dist/consent-checkboxes/build-consent-urls.js +9 -3
- package/dist/consent-checkboxes/index.d.ts +10 -2
- package/dist/consent-checkboxes/index.js +14 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lead-form/lead-field.js +41 -15
- package/dist/lead-form/lead-form.d.ts +5 -0
- package/dist/lead-form/lead-form.js +27 -9
- package/dist/lead-form/use-lead-t.js +11 -2
- package/dist/personal-data-consent/index.d.ts +46 -0
- package/dist/personal-data-consent/index.js +66 -0
- package/package.json +5 -5
|
@@ -15,11 +15,15 @@ type BuildConsentUrlsParams = {
|
|
|
15
15
|
export type ConsentUrls = {
|
|
16
16
|
agreementUrl: string;
|
|
17
17
|
policyUrl: string;
|
|
18
|
+
/** Согласие на обработку ПДн площадки (нужно только площадкам из РФ). */
|
|
19
|
+
pdConsentUrl: string;
|
|
18
20
|
};
|
|
19
21
|
/**
|
|
20
22
|
* Строит публичные ссылки для блока согласий:
|
|
21
23
|
* - агримент: `/company/:slug/terms-of-services` (пусто, если нет slug);
|
|
22
|
-
* - политика: `/
|
|
24
|
+
* - политика: `/company/:slug/privacy-policy` — оператор ПДн площадка,
|
|
25
|
+
* без slug (или без своей политики) — `/documents/privacy_policy/:language`;
|
|
26
|
+
* - согласие на ПДн: `/company/:slug/personal-data-consent`.
|
|
23
27
|
*/
|
|
24
28
|
export declare const buildConsentUrls: ({ baseUrl, slug, language, }: BuildConsentUrlsParams) => ConsentUrls;
|
|
25
29
|
export {};
|
|
@@ -4,14 +4,20 @@ exports.buildConsentUrls = void 0;
|
|
|
4
4
|
/**
|
|
5
5
|
* Строит публичные ссылки для блока согласий:
|
|
6
6
|
* - агримент: `/company/:slug/terms-of-services` (пусто, если нет slug);
|
|
7
|
-
* - политика: `/
|
|
7
|
+
* - политика: `/company/:slug/privacy-policy` — оператор ПДн площадка,
|
|
8
|
+
* без slug (или без своей политики) — `/documents/privacy_policy/:language`;
|
|
9
|
+
* - согласие на ПДн: `/company/:slug/personal-data-consent`.
|
|
8
10
|
*/
|
|
9
11
|
const buildConsentUrls = ({ baseUrl, slug, language, }) => {
|
|
10
12
|
const base = (baseUrl || '').replace(/\/+$/, '');
|
|
11
13
|
const lang = language || 'en';
|
|
14
|
+
const company = slug ? `${base}/company/${slug}` : '';
|
|
12
15
|
return {
|
|
13
|
-
agreementUrl:
|
|
14
|
-
policyUrl:
|
|
16
|
+
agreementUrl: company ? `${company}/terms-of-services` : '',
|
|
17
|
+
policyUrl: company
|
|
18
|
+
? `${company}/privacy-policy`
|
|
19
|
+
: `${base}/documents/privacy_policy/${lang}`,
|
|
20
|
+
pdConsentUrl: company ? `${company}/personal-data-consent` : '',
|
|
15
21
|
};
|
|
16
22
|
};
|
|
17
23
|
exports.buildConsentUrls = buildConsentUrls;
|
|
@@ -2,16 +2,24 @@ import React from 'react';
|
|
|
2
2
|
import { buildConsentUrls, ConsentUrls } from './build-consent-urls';
|
|
3
3
|
export type { ConsentUrls };
|
|
4
4
|
export { buildConsentUrls };
|
|
5
|
-
export type ConsentField = 'agreeTerms' | 'agreePolicy' | 'sendAds';
|
|
5
|
+
export type ConsentField = 'agreeTerms' | 'agreePolicy' | 'sendAds' | 'pdConsent';
|
|
6
6
|
export type ConsentValues = Record<ConsentField, boolean>;
|
|
7
7
|
export declare const INITIAL_CONSENT_VALUES: ConsentValues;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Обязательные согласия отмечены — можно подтверждать заказ/подпись.
|
|
10
|
+
* `pdConsent` без отдельной галочки (вне РФ) ставится вместе с основной.
|
|
11
|
+
*/
|
|
9
12
|
export declare const areRequiredConsentsAccepted: (values: ConsentValues) => boolean;
|
|
10
13
|
type Props = {
|
|
11
14
|
/** Ссылка на клиентское соглашение оператора. */
|
|
12
15
|
agreementUrl: string;
|
|
13
16
|
/** Ссылка на политику конфиденциальности. */
|
|
14
17
|
policyUrl: string;
|
|
18
|
+
/**
|
|
19
|
+
* Ссылка на согласие на обработку ПДн. Передаётся только площадкам из РФ:
|
|
20
|
+
* по 152-ФЗ согласие — отдельная галочка, не смешанная с другими документами.
|
|
21
|
+
*/
|
|
22
|
+
pdConsentUrl?: string;
|
|
15
23
|
values: ConsentValues;
|
|
16
24
|
onChange: (field: ConsentField, checked: boolean) => void;
|
|
17
25
|
/**
|
|
@@ -8,6 +8,7 @@ exports.ConsentCheckboxes = exports.areRequiredConsentsAccepted = exports.INITIA
|
|
|
8
8
|
const ui_1 = require("@alphakits/ui");
|
|
9
9
|
const react_1 = __importDefault(require("react"));
|
|
10
10
|
const react_i18next_1 = require("react-i18next");
|
|
11
|
+
const personal_data_consent_1 = require("../personal-data-consent");
|
|
11
12
|
const build_consent_urls_1 = require("./build-consent-urls");
|
|
12
13
|
Object.defineProperty(exports, "buildConsentUrls", { enumerable: true, get: function () { return build_consent_urls_1.buildConsentUrls; } });
|
|
13
14
|
const index_module_css_1 = __importDefault(require("./index.module.css"));
|
|
@@ -15,20 +16,28 @@ exports.INITIAL_CONSENT_VALUES = {
|
|
|
15
16
|
agreeTerms: false,
|
|
16
17
|
agreePolicy: false,
|
|
17
18
|
sendAds: false,
|
|
19
|
+
pdConsent: false,
|
|
18
20
|
};
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Обязательные согласия отмечены — можно подтверждать заказ/подпись.
|
|
23
|
+
* `pdConsent` без отдельной галочки (вне РФ) ставится вместе с основной.
|
|
24
|
+
*/
|
|
25
|
+
const areRequiredConsentsAccepted = (values) => values.agreeTerms && values.agreePolicy && values.pdConsent;
|
|
21
26
|
exports.areRequiredConsentsAccepted = areRequiredConsentsAccepted;
|
|
22
27
|
const labelColor = (invalid) => invalid ? 'negative' : 'secondary';
|
|
23
|
-
const ConsentCheckboxes = ({ agreementUrl, policyUrl, values, onChange, hideMarketing, showRequiredError, }) => {
|
|
28
|
+
const ConsentCheckboxes = ({ agreementUrl, policyUrl, pdConsentUrl, values, onChange, hideMarketing, showRequiredError, }) => {
|
|
24
29
|
const { t } = (0, react_i18next_1.useTranslation)('common');
|
|
30
|
+
const separatePdConsent = !!pdConsentUrl;
|
|
25
31
|
// Агримент + политику объединили в одну обязательную галочку: меньше
|
|
26
32
|
// «трения» на экране оплаты. Обе ссылки внутри одной переводимой строки.
|
|
27
33
|
const requiredAccepted = values.agreeTerms && values.agreePolicy;
|
|
28
34
|
const requiredInvalid = !!showRequiredError && !requiredAccepted;
|
|
35
|
+
const pdConsentInvalid = !!showRequiredError && !values.pdConsent;
|
|
29
36
|
const setRequired = (checked) => {
|
|
30
37
|
onChange('agreeTerms', checked);
|
|
31
38
|
onChange('agreePolicy', checked);
|
|
39
|
+
if (!separatePdConsent)
|
|
40
|
+
onChange('pdConsent', checked);
|
|
32
41
|
};
|
|
33
42
|
return (react_1.default.createElement("div", { className: [index_module_css_1.default.wrapper, showRequiredError ? index_module_css_1.default.shake : ''].join(' ').trim() },
|
|
34
43
|
react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 8 },
|
|
@@ -37,6 +46,8 @@ const ConsentCheckboxes = ({ agreementUrl, policyUrl, values, onChange, hideMark
|
|
|
37
46
|
react_1.default.createElement(ui_1.Link, { key: 'agreement', size: 's', target: '_blank', rel: 'noreferrer', className: index_module_css_1.default.link, href: agreementUrl }),
|
|
38
47
|
react_1.default.createElement(ui_1.Link, { key: 'policy', size: 's', target: '_blank', rel: 'noreferrer', className: index_module_css_1.default.link, href: policyUrl }),
|
|
39
48
|
] })) }),
|
|
49
|
+
separatePdConsent && (react_1.default.createElement(ui_1.Checkbox, { size: 's', align: 'start', block: true, checked: values.pdConsent, onChange: (_e, { checked }) => onChange('pdConsent', checked), label: react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: labelColor(pdConsentInvalid) },
|
|
50
|
+
react_1.default.createElement(personal_data_consent_1.PersonalDataConsentLabel, { consentUrl: pdConsentUrl })) })),
|
|
40
51
|
!hideMarketing && (react_1.default.createElement(ui_1.Checkbox, { size: 's', align: 'start', block: true, checked: values.sendAds, onChange: (_e, { checked }) => onChange('sendAds', checked), label: react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'secondary' }, t('consent.marketing', "I'd like to receive special offers, promotions and news — no spam, unsubscribe anytime")) })),
|
|
41
52
|
showRequiredError && (react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'negative' }, t('consent.required', 'Please accept the required agreements to continue'))))));
|
|
42
53
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './functional-popover';
|
|
|
3
3
|
export * from './input-tel';
|
|
4
4
|
export * from './lazy-with-retry';
|
|
5
5
|
export * from './lead-form';
|
|
6
|
+
export * from './personal-data-consent';
|
|
6
7
|
export * from './players-and-variation-picker';
|
|
7
8
|
export * from './use-debounced-value';
|
|
8
9
|
export * from './use-localstore-state';
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ __exportStar(require("./functional-popover"), exports);
|
|
|
19
19
|
__exportStar(require("./input-tel"), exports);
|
|
20
20
|
__exportStar(require("./lazy-with-retry"), exports);
|
|
21
21
|
__exportStar(require("./lead-form"), exports);
|
|
22
|
+
__exportStar(require("./personal-data-consent"), exports);
|
|
22
23
|
__exportStar(require("./players-and-variation-picker"), exports);
|
|
23
24
|
__exportStar(require("./use-debounced-value"), exports);
|
|
24
25
|
__exportStar(require("./use-localstore-state"), exports);
|
|
@@ -9,6 +9,7 @@ const MinusM_1 = __importDefault(require("@alphakits/icons/dist/MinusM"));
|
|
|
9
9
|
const PlusM_1 = __importDefault(require("@alphakits/icons/dist/PlusM"));
|
|
10
10
|
const ui_1 = require("@alphakits/ui");
|
|
11
11
|
const lead_field_type_enum_1 = require("@escapenavigator/types/dist/lead/enums/lead-field-type.enum");
|
|
12
|
+
const lead_time_constants_1 = require("@escapenavigator/types/dist/lead/lead-time.constants");
|
|
12
13
|
const react_1 = __importDefault(require("react"));
|
|
13
14
|
const input_tel_1 = require("../input-tel");
|
|
14
15
|
const index_module_css_1 = __importDefault(require("./index.module.css"));
|
|
@@ -25,6 +26,20 @@ const buildTimeOptions = () => {
|
|
|
25
26
|
return out;
|
|
26
27
|
};
|
|
27
28
|
const TIME_OPTIONS = buildTimeOptions();
|
|
29
|
+
/** Больше трёх вариантов в ряд пилюль не влезает на мобиле — раскладываем плитками. */
|
|
30
|
+
const TILES_FROM = 4;
|
|
31
|
+
/**
|
|
32
|
+
* Радио и чекбоксы площадки — кнопками-вариантами: один тап вместо попадания
|
|
33
|
+
* в кружок, и форма читается как выбор, а не как анкета.
|
|
34
|
+
*/
|
|
35
|
+
const ChoiceGroup = ({ label, options, isSelected, onToggle, hint, error, }) => (react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 8 },
|
|
36
|
+
react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium', color: 'primary' }, label),
|
|
37
|
+
react_1.default.createElement("div", { role: 'group', "aria-label": label, className: options.length >= TILES_FROM ? index_module_css_1.default.choiceTiles : index_module_css_1.default.choicePills }, options.map((option) => {
|
|
38
|
+
const selected = isSelected(option.id);
|
|
39
|
+
return (react_1.default.createElement("button", { key: option.id, type: 'button', "aria-pressed": selected, className: index_module_css_1.default.choice, "data-selected": selected, "data-invalid": !!error, onClick: () => onToggle(option.id) }, option.label));
|
|
40
|
+
})),
|
|
41
|
+
hint,
|
|
42
|
+
!!error && (react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'negative' }, error))));
|
|
28
43
|
const LeadField = ({ field, value, error, language, profileCountry, questrooms, locations, onChange, onPhoneValidChange, }) => {
|
|
29
44
|
var _a, _b;
|
|
30
45
|
const t = (0, use_lead_t_1.useLeadT)();
|
|
@@ -41,6 +56,10 @@ const LeadField = ({ field, value, error, language, profileCountry, questrooms,
|
|
|
41
56
|
hint));
|
|
42
57
|
};
|
|
43
58
|
const activeOptions = (field.options || []).filter((option) => option.active !== false);
|
|
59
|
+
const choiceOptions = activeOptions.map((option) => ({
|
|
60
|
+
id: option.id,
|
|
61
|
+
label: (0, lib_1.optionLabel)(option, language, option.id),
|
|
62
|
+
}));
|
|
44
63
|
const referenceOptions = (items) => items.map((item) => ({ key: String(item.id), content: item.title }));
|
|
45
64
|
switch (field.type) {
|
|
46
65
|
case lead_field_type_enum_1.LeadFieldTypeEnum.PHONE:
|
|
@@ -53,9 +72,24 @@ const LeadField = ({ field, value, error, language, profileCountry, questrooms,
|
|
|
53
72
|
case lead_field_type_enum_1.LeadFieldTypeEnum.DATE:
|
|
54
73
|
return withHint(react_1.default.createElement(ui_1.CalendarInput, { block: true, label: label, lang: language, value: String(value || ''), error: error, onChange: (_e, payload) => onChange(payload.value) }));
|
|
55
74
|
case lead_field_type_enum_1.LeadFieldTypeEnum.TIME:
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
return (react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 8 },
|
|
76
|
+
react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium', color: 'primary' }, label),
|
|
77
|
+
react_1.default.createElement("div", { className: index_module_css_1.default.timeChoices },
|
|
78
|
+
react_1.default.createElement(ui_1.Select, { block: true, optionsListWidth: 'field', options: value &&
|
|
79
|
+
value !== lead_time_constants_1.LEAD_TIME_UNDECIDED_VALUE &&
|
|
80
|
+
!TIME_OPTIONS.some((option) => option.key === String(value))
|
|
81
|
+
? [
|
|
82
|
+
{ key: String(value), content: String(value) },
|
|
83
|
+
...TIME_OPTIONS,
|
|
84
|
+
]
|
|
85
|
+
: TIME_OPTIONS, selected: value === lead_time_constants_1.LEAD_TIME_UNDECIDED_VALUE ? '' : String(value || ''), error: error, onChange: ({ selected }) => onChange(selected === null || selected === void 0 ? void 0 : selected.key) }),
|
|
86
|
+
react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.timeUndecided, "data-selected": value === lead_time_constants_1.LEAD_TIME_UNDECIDED_VALUE, "aria-pressed": value === lead_time_constants_1.LEAD_TIME_UNDECIDED_VALUE, onClick: () => onChange(lead_time_constants_1.LEAD_TIME_UNDECIDED_VALUE) },
|
|
87
|
+
react_1.default.createElement("span", { className: index_module_css_1.default.timeUndecidedIcon, "aria-hidden": 'true' },
|
|
88
|
+
react_1.default.createElement("svg", { viewBox: '0 0 24 24', fill: 'none', "aria-hidden": 'true' },
|
|
89
|
+
react_1.default.createElement("circle", { cx: '12', cy: '12', r: '8.25' }),
|
|
90
|
+
react_1.default.createElement("path", { d: 'M12 7.75v4.6l3.05 1.8' }))),
|
|
91
|
+
react_1.default.createElement("span", null, t('lead.timeUndecided')))),
|
|
92
|
+
hint));
|
|
59
93
|
case lead_field_type_enum_1.LeadFieldTypeEnum.GUESTS: {
|
|
60
94
|
const min = (_a = field.min) !== null && _a !== void 0 ? _a : 1;
|
|
61
95
|
const max = (_b = field.max) !== null && _b !== void 0 ? _b : 30;
|
|
@@ -78,20 +112,12 @@ const LeadField = ({ field, value, error, language, profileCountry, questrooms,
|
|
|
78
112
|
content: (0, lib_1.optionLabel)(option, language, option.id),
|
|
79
113
|
})), selected: String(value || ''), error: error, onChange: ({ selected }) => onChange(selected === null || selected === void 0 ? void 0 : selected.key) }));
|
|
80
114
|
case lead_field_type_enum_1.LeadFieldTypeEnum.RADIO:
|
|
81
|
-
return (react_1.default.createElement(
|
|
82
|
-
react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium', color: 'primary' }, label),
|
|
83
|
-
activeOptions.map((option) => (react_1.default.createElement(ui_1.Radio, { key: option.id, label: (0, lib_1.optionLabel)(option, language, option.id), checked: value === option.id, onChange: () => onChange(option.id) }))),
|
|
84
|
-
hint,
|
|
85
|
-
!!error && (react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'negative' }, error))));
|
|
115
|
+
return (react_1.default.createElement(ChoiceGroup, { label: label, options: choiceOptions, isSelected: (id) => value === id, onToggle: (id) => onChange(id), hint: hint, error: error }));
|
|
86
116
|
case lead_field_type_enum_1.LeadFieldTypeEnum.CHECKBOX: {
|
|
87
117
|
const selected = Array.isArray(value) ? value : [];
|
|
88
|
-
return (react_1.default.createElement(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
? selected.filter((item) => item !== option.id)
|
|
92
|
-
: [...selected, option.id]) }))),
|
|
93
|
-
hint,
|
|
94
|
-
!!error && (react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'negative' }, error))));
|
|
118
|
+
return (react_1.default.createElement(ChoiceGroup, { label: label, options: choiceOptions, isSelected: (id) => selected.includes(id), onToggle: (id) => onChange(selected.includes(id)
|
|
119
|
+
? selected.filter((item) => item !== id)
|
|
120
|
+
: [...selected, id]), hint: hint, error: error }));
|
|
95
121
|
}
|
|
96
122
|
case lead_field_type_enum_1.LeadFieldTypeEnum.QUESTROOM:
|
|
97
123
|
case lead_field_type_enum_1.LeadFieldTypeEnum.LOCATION:
|
|
@@ -10,6 +10,7 @@ export type LeadFormSubmitPayload = {
|
|
|
10
10
|
answers: SubmitLeadAnswerDto[];
|
|
11
11
|
source: LeadSourceEnum;
|
|
12
12
|
trap?: string;
|
|
13
|
+
pdConsent?: boolean;
|
|
13
14
|
};
|
|
14
15
|
/**
|
|
15
16
|
* Отправку делает вызывающая сторона: из виджета заявка уходит с
|
|
@@ -41,6 +42,10 @@ type Props = {
|
|
|
41
42
|
doneLabel?: string;
|
|
42
43
|
/** Контейнер прячет свою шапку, чтобы экран успеха не конкурировал с названием. */
|
|
43
44
|
onSuccess?: () => void;
|
|
45
|
+
/** Политика площадки в отношении ПДн — ссылка под формой. */
|
|
46
|
+
policyUrl?: string;
|
|
47
|
+
/** Документ согласия на обработку ПДн (галочка есть только в РФ). */
|
|
48
|
+
pdConsentUrl?: string;
|
|
44
49
|
};
|
|
45
50
|
export declare const LeadForm: React.FC<Props>;
|
|
46
51
|
export {};
|
|
@@ -49,12 +49,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
49
49
|
exports.LeadForm = void 0;
|
|
50
50
|
const ui_1 = require("@alphakits/ui");
|
|
51
51
|
const react_1 = __importStar(require("react"));
|
|
52
|
+
const personal_data_consent_1 = require("../personal-data-consent");
|
|
52
53
|
const index_module_css_1 = __importDefault(require("./index.module.css"));
|
|
53
54
|
const lead_field_1 = require("./lead-field");
|
|
54
55
|
const lib_1 = require("./lib");
|
|
55
56
|
const use_lead_t_1 = require("./use-lead-t");
|
|
56
|
-
const
|
|
57
|
+
const ArrowRight = () => (react_1.default.createElement("svg", { width: '18', height: '18', viewBox: '0 0 24 24', fill: 'none', "aria-hidden": 'true' },
|
|
58
|
+
react_1.default.createElement("path", { d: 'M5 12h14m-6-6 6 6-6 6', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' })));
|
|
59
|
+
const LeadForm = ({ form, source, language, profileCountry, submit, prefill, contextNote, withHeader = true, onDone, doneLabel, onSuccess, policyUrl, pdConsentUrl, }) => {
|
|
57
60
|
const t = (0, use_lead_t_1.useLeadT)();
|
|
61
|
+
const consentRequired = (0, personal_data_consent_1.requiresPersonalDataConsent)(profileCountry);
|
|
62
|
+
const [consent, setConsent] = (0, react_1.useState)(false);
|
|
63
|
+
const [showConsentError, setShowConsentError] = (0, react_1.useState)(false);
|
|
58
64
|
const [values, setValues] = (0, react_1.useState)(() => (0, lib_1.buildInitialValues)(form, prefill));
|
|
59
65
|
const [errors, setErrors] = (0, react_1.useState)({});
|
|
60
66
|
const [formError, setFormError] = (0, react_1.useState)();
|
|
@@ -73,7 +79,9 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
73
79
|
if (sending)
|
|
74
80
|
return;
|
|
75
81
|
const result = (0, lib_1.validate)({ form, values, phoneValid, t });
|
|
76
|
-
|
|
82
|
+
const consentMissing = consentRequired && !consent;
|
|
83
|
+
setShowConsentError(consentMissing);
|
|
84
|
+
if (Object.keys(result.errors).length || result.formError || consentMissing) {
|
|
77
85
|
setErrors(result.errors);
|
|
78
86
|
setFormError(result.formError);
|
|
79
87
|
return;
|
|
@@ -85,6 +93,7 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
85
93
|
answers: (0, lib_1.buildAnswers)(form, values, contextNote),
|
|
86
94
|
source,
|
|
87
95
|
trap: trap || undefined,
|
|
96
|
+
pdConsent: consentRequired ? consent : undefined,
|
|
88
97
|
});
|
|
89
98
|
setSending(false);
|
|
90
99
|
if (!(response === null || response === void 0 ? void 0 : response.data)) {
|
|
@@ -109,17 +118,26 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
109
118
|
}
|
|
110
119
|
const title = (0, lib_1.localized)(form.clientTitle, language) || t('lead.title');
|
|
111
120
|
const description = (0, lib_1.localized)(form.description, language);
|
|
112
|
-
return (react_1.default.createElement(
|
|
113
|
-
withHeader && (react_1.default.createElement(
|
|
121
|
+
return (react_1.default.createElement("div", { className: index_module_css_1.default.form },
|
|
122
|
+
withHeader && (react_1.default.createElement("div", { className: index_module_css_1.default.header },
|
|
123
|
+
react_1.default.createElement("span", { className: index_module_css_1.default.headerAccent, "aria-hidden": 'true' }),
|
|
114
124
|
react_1.default.createElement(ui_1.Typography.Title, { tag: 'h1', view: 'small', weight: 'bold' }, title),
|
|
115
125
|
!!description && (react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', color: 'secondary' }, description)))),
|
|
116
126
|
!!formError && react_1.default.createElement(ui_1.ToastPlate, { view: 'negative' }, formError),
|
|
117
|
-
form.fields
|
|
127
|
+
react_1.default.createElement("div", { className: index_module_css_1.default.fields }, form.fields
|
|
118
128
|
.filter((field) => (0, lib_1.isVisible)(field, values))
|
|
119
|
-
.map((field) => (react_1.default.createElement(
|
|
129
|
+
.map((field) => (react_1.default.createElement("div", { key: field.id, className: index_module_css_1.default.fieldCard, "data-field-type": field.type },
|
|
130
|
+
react_1.default.createElement(lead_field_1.LeadField, { field: field, value: values[field.id], error: errors[field.id], language: language, profileCountry: profileCountry, questrooms: form.questrooms, locations: form.locations, onChange: (value) => setValue(field.id, value), onPhoneValidChange: setPhoneValid }))))),
|
|
120
131
|
react_1.default.createElement("input", { className: index_module_css_1.default.trap, tabIndex: -1, autoComplete: 'off', "aria-hidden": 'true', value: trap, onChange: (event) => setTrap(event.target.value) }),
|
|
121
|
-
react_1.default.createElement(
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
react_1.default.createElement("div", { className: index_module_css_1.default.footer },
|
|
133
|
+
(consentRequired || !!policyUrl) && (react_1.default.createElement(personal_data_consent_1.PersonalDataConsent, { required: consentRequired, consentUrl: pdConsentUrl || '', policyUrl: policyUrl || '', checked: consent, showError: showConsentError, onChange: (checked) => {
|
|
134
|
+
setConsent(checked);
|
|
135
|
+
if (checked)
|
|
136
|
+
setShowConsentError(false);
|
|
137
|
+
} })),
|
|
138
|
+
react_1.default.createElement(ui_1.Button, { view: 'primary', size: 's', block: true, loading: sending, rightAddons: sending ? undefined : react_1.default.createElement(ArrowRight, null), onClick: handleSubmit }, sending
|
|
139
|
+
? t('lead.sending')
|
|
140
|
+
: (0, lib_1.localized)(form.submitLabel, language) || t('lead.submit')),
|
|
141
|
+
react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'secondary', className: index_module_css_1.default.footnote, tag: 'div' }, t('lead.footnote')))));
|
|
124
142
|
};
|
|
125
143
|
exports.LeadForm = LeadForm;
|
|
@@ -25,8 +25,10 @@ const DEFAULTS = {
|
|
|
25
25
|
'lead.unavailable': 'The request form is not available right now',
|
|
26
26
|
'lead.guestsMinus': 'One guest less',
|
|
27
27
|
'lead.guestsPlus': 'One guest more',
|
|
28
|
+
'lead.timeUndecided': 'Not sure about the time yet',
|
|
28
29
|
'lead.close': 'Close',
|
|
29
30
|
'lead.another': 'Send another request',
|
|
31
|
+
'lead.footnote': 'We will contact you and suggest options. A request is not a booking yet.',
|
|
30
32
|
// Кнопка «оставить заявку» рядом с расписанием.
|
|
31
33
|
'lead.ctaTitle': 'Cannot find a suitable time?',
|
|
32
34
|
'lead.ctaHint': 'Leave a request and we will arrange the booking for you',
|
|
@@ -35,8 +37,15 @@ const DEFAULTS = {
|
|
|
35
37
|
'lead.certCtaTitle': 'Need help with a gift?',
|
|
36
38
|
'lead.certCtaHint': 'Leave a request and we will pick a certificate for you',
|
|
37
39
|
};
|
|
40
|
+
/** Новые ключи, которых ещё нет в сервисе: русский аудитории показываем сразу. */
|
|
41
|
+
const RU_DEFAULTS = {
|
|
42
|
+
'lead.footnote': 'Менеджер свяжется с вами и предложит варианты. Заявка пока не бронирует игру.',
|
|
43
|
+
'lead.timeUndecided': 'Пока не определился со временем',
|
|
44
|
+
};
|
|
38
45
|
const useLeadT = () => {
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
var _a;
|
|
47
|
+
const { t, i18n } = (0, react_i18next_1.useTranslation)();
|
|
48
|
+
const isRuLang = (_a = i18n.language) === null || _a === void 0 ? void 0 : _a.startsWith('ru');
|
|
49
|
+
return (0, react_1.useCallback)((key, options) => t(key, Object.assign({ defaultValue: (isRuLang && RU_DEFAULTS[key]) || DEFAULTS[key] || key }, options)), [t, isRuLang]);
|
|
41
50
|
};
|
|
42
51
|
exports.useLeadT = useLeadT;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { requiresPersonalDataConsent } from '@escapenavigator/utils/dist/personal-data-consent';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export { requiresPersonalDataConsent };
|
|
4
|
+
/**
|
|
5
|
+
* Ключи пока не заведены в translations-сервисе, а блок видят в основном
|
|
6
|
+
* русскоязычные посетители — английский дефолт им показывать нельзя.
|
|
7
|
+
*/
|
|
8
|
+
declare const DEFAULTS: {
|
|
9
|
+
readonly 'consent.personalData': {
|
|
10
|
+
readonly ru: "Я даю <0>согласие на обработку персональных данных</0>";
|
|
11
|
+
readonly en: "I give my <0>consent to the processing of personal data</0>";
|
|
12
|
+
};
|
|
13
|
+
readonly 'consent.personalDataRequired': {
|
|
14
|
+
readonly ru: "Подтвердите согласие на обработку персональных данных";
|
|
15
|
+
readonly en: "Please confirm your consent to the processing of personal data";
|
|
16
|
+
};
|
|
17
|
+
readonly 'consent.privacyNotice': {
|
|
18
|
+
readonly ru: "Данные обрабатываются в соответствии с <0>политикой конфиденциальности</0>";
|
|
19
|
+
readonly en: "Your data is processed in accordance with the <0>privacy policy</0>";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
type DefaultKey = keyof typeof DEFAULTS;
|
|
23
|
+
export declare const usePersonalDataConsentT: () => {
|
|
24
|
+
t: import("i18next").TFunction<"common", undefined, "common">;
|
|
25
|
+
defaults: (key: DefaultKey) => "Я даю <0>согласие на обработку персональных данных</0>" | "I give my <0>consent to the processing of personal data</0>" | "Подтвердите согласие на обработку персональных данных" | "Please confirm your consent to the processing of personal data" | "Данные обрабатываются в соответствии с <0>политикой конфиденциальности</0>" | "Your data is processed in accordance with the <0>privacy policy</0>";
|
|
26
|
+
};
|
|
27
|
+
/** Текст галочки согласия со ссылкой на сам документ согласия. */
|
|
28
|
+
export declare const PersonalDataConsentLabel: React.FC<{
|
|
29
|
+
consentUrl: string;
|
|
30
|
+
}>;
|
|
31
|
+
type Props = {
|
|
32
|
+
/**
|
|
33
|
+
* Нужна отдельная галочка (152-ФЗ, площадки из РФ). Иначе (GDPR) хватает
|
|
34
|
+
* уведомления со ссылкой на политику: данные нужны для заявки/договора.
|
|
35
|
+
*/
|
|
36
|
+
required: boolean;
|
|
37
|
+
/** Документ согласия площадки. */
|
|
38
|
+
consentUrl: string;
|
|
39
|
+
/** Политика площадки в отношении ПДн. */
|
|
40
|
+
policyUrl: string;
|
|
41
|
+
checked: boolean;
|
|
42
|
+
onChange: (checked: boolean) => void;
|
|
43
|
+
/** Клиент пытался отправить без галочки: красим, трясём и пишем почему. */
|
|
44
|
+
showError?: boolean;
|
|
45
|
+
};
|
|
46
|
+
export declare const PersonalDataConsent: React.FC<Props>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.PersonalDataConsent = exports.PersonalDataConsentLabel = exports.usePersonalDataConsentT = exports.requiresPersonalDataConsent = void 0;
|
|
8
|
+
const ui_1 = require("@alphakits/ui");
|
|
9
|
+
const personal_data_consent_1 = require("@escapenavigator/utils/dist/personal-data-consent");
|
|
10
|
+
Object.defineProperty(exports, "requiresPersonalDataConsent", { enumerable: true, get: function () { return personal_data_consent_1.requiresPersonalDataConsent; } });
|
|
11
|
+
const react_1 = __importDefault(require("react"));
|
|
12
|
+
const react_i18next_1 = require("react-i18next");
|
|
13
|
+
const index_module_css_1 = __importDefault(require("./index.module.css"));
|
|
14
|
+
/**
|
|
15
|
+
* Ключи пока не заведены в translations-сервисе, а блок видят в основном
|
|
16
|
+
* русскоязычные посетители — английский дефолт им показывать нельзя.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULTS = {
|
|
19
|
+
'consent.personalData': {
|
|
20
|
+
ru: 'Я даю <0>согласие на обработку персональных данных</0>',
|
|
21
|
+
en: 'I give my <0>consent to the processing of personal data</0>',
|
|
22
|
+
},
|
|
23
|
+
'consent.personalDataRequired': {
|
|
24
|
+
ru: 'Подтвердите согласие на обработку персональных данных',
|
|
25
|
+
en: 'Please confirm your consent to the processing of personal data',
|
|
26
|
+
},
|
|
27
|
+
'consent.privacyNotice': {
|
|
28
|
+
ru: 'Данные обрабатываются в соответствии с <0>политикой конфиденциальности</0>',
|
|
29
|
+
en: 'Your data is processed in accordance with the <0>privacy policy</0>',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const usePersonalDataConsentT = () => {
|
|
33
|
+
var _a;
|
|
34
|
+
const { t, i18n } = (0, react_i18next_1.useTranslation)('common');
|
|
35
|
+
const lang = ((_a = i18n.language) === null || _a === void 0 ? void 0 : _a.startsWith('ru')) ? 'ru' : 'en';
|
|
36
|
+
return {
|
|
37
|
+
t,
|
|
38
|
+
defaults: (key) => DEFAULTS[key][lang],
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
exports.usePersonalDataConsentT = usePersonalDataConsentT;
|
|
42
|
+
const DocLink = ({ href, children }) => (react_1.default.createElement(ui_1.Link, { size: 's', target: '_blank', rel: 'noreferrer', className: index_module_css_1.default.link, href: href }, children));
|
|
43
|
+
const TransWithLink = ({ i18nKey, href }) => {
|
|
44
|
+
const { t, defaults } = (0, exports.usePersonalDataConsentT)();
|
|
45
|
+
return (react_1.default.createElement(react_i18next_1.Trans, { t: t, ns: 'common', i18nKey: i18nKey, defaults: defaults(i18nKey), components: [react_1.default.createElement(DocLink, { key: 'doc', href: href })] }));
|
|
46
|
+
};
|
|
47
|
+
/** Текст галочки согласия со ссылкой на сам документ согласия. */
|
|
48
|
+
const PersonalDataConsentLabel = ({ consentUrl }) => (react_1.default.createElement(TransWithLink, { i18nKey: 'consent.personalData', href: consentUrl }));
|
|
49
|
+
exports.PersonalDataConsentLabel = PersonalDataConsentLabel;
|
|
50
|
+
const PersonalDataConsent = ({ required, consentUrl, policyUrl, checked, onChange, showError, }) => {
|
|
51
|
+
const { t, defaults } = (0, exports.usePersonalDataConsentT)();
|
|
52
|
+
const invalid = required && !!showError && !checked;
|
|
53
|
+
const notice = (react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'secondary' },
|
|
54
|
+
react_1.default.createElement(TransWithLink, { i18nKey: 'consent.privacyNotice', href: policyUrl })));
|
|
55
|
+
if (!required)
|
|
56
|
+
return react_1.default.createElement("div", { className: index_module_css_1.default.wrapper }, notice);
|
|
57
|
+
return (react_1.default.createElement("div", { className: [index_module_css_1.default.wrapper, invalid ? index_module_css_1.default.shake : ''].join(' ').trim() },
|
|
58
|
+
react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 6 },
|
|
59
|
+
react_1.default.createElement(ui_1.Checkbox, { size: 's', align: 'start', block: true, checked: checked, onChange: (_e, payload) => onChange(!!(payload === null || payload === void 0 ? void 0 : payload.checked)), label: react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: invalid ? 'negative' : 'secondary' },
|
|
60
|
+
react_1.default.createElement(exports.PersonalDataConsentLabel, { consentUrl: consentUrl })) }),
|
|
61
|
+
notice,
|
|
62
|
+
invalid && (react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'negative' }, t('consent.personalDataRequired', {
|
|
63
|
+
defaultValue: defaults('consent.personalDataRequired'),
|
|
64
|
+
}))))));
|
|
65
|
+
};
|
|
66
|
+
exports.PersonalDataConsent = PersonalDataConsent;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/hooks",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.23",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@escapenavigator/services": "^2.1.
|
|
18
|
-
"@escapenavigator/types": "^2.1.
|
|
19
|
-
"@escapenavigator/utils": "^2.1.
|
|
17
|
+
"@escapenavigator/services": "^2.1.23",
|
|
18
|
+
"@escapenavigator/types": "^2.1.23",
|
|
19
|
+
"@escapenavigator/utils": "^2.1.23",
|
|
20
20
|
"libphonenumber-js": "^1.12.24",
|
|
21
21
|
"react": "19.2.6",
|
|
22
22
|
"react-phone-input-2": "^2.15.1"
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"ts-jest": "^29.1.1",
|
|
33
33
|
"typescript": "^5.6"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "533e344e432372b89b80d81c0ce35bc27463b677"
|
|
36
36
|
}
|