add data to auth and session
This commit is contained in:
parent
d24795a4a6
commit
b31b42a57d
354
docs/REGISTRATION_FIELD_KEY.md
Normal file
354
docs/REGISTRATION_FIELD_KEY.md
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
# Registration Field Key для List Single Selection
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
Функциональность `registrationFieldKey` позволяет автоматически передавать выбранные значения из list single selection экранов в payload регистрации пользователя при авторизации через email экран.
|
||||||
|
|
||||||
|
## Как это работает
|
||||||
|
|
||||||
|
### 1. Настройка в админке
|
||||||
|
|
||||||
|
Для list экранов с `selectionType: "single"` в админке появляется дополнительное поле **"Ключ поля для регистрации"**.
|
||||||
|
|
||||||
|
В это поле можно указать путь к полю в объекте регистрации, используя точечную нотацию для вложенных объектов.
|
||||||
|
|
||||||
|
**Примеры:**
|
||||||
|
- `profile.gender` → `{ profile: { gender: "selected-id" } }`
|
||||||
|
- `profile.relationship_status` → `{ profile: { relationship_status: "selected-id" } }`
|
||||||
|
- `partner.gender` → `{ partner: { gender: "selected-id" } }`
|
||||||
|
|
||||||
|
### 2. Пример JSON конфигурации
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "gender-screen",
|
||||||
|
"template": "list",
|
||||||
|
"title": {
|
||||||
|
"text": "What is your gender?"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"selectionType": "single",
|
||||||
|
"registrationFieldKey": "profile.gender",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"id": "male",
|
||||||
|
"label": "Male",
|
||||||
|
"emoji": "👨"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "female",
|
||||||
|
"label": "Female",
|
||||||
|
"emoji": "👩"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "other",
|
||||||
|
"label": "Other",
|
||||||
|
"emoji": "🧑"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Как данные попадают в регистрацию и сессию
|
||||||
|
|
||||||
|
#### **Передача в сессию (на каждом экране):**
|
||||||
|
|
||||||
|
1. **Пользователь выбирает вариант** на экране (например, "Male" с id "male")
|
||||||
|
2. **Ответ сохраняется** в `FunnelAnswers` под ключом экрана: `{ "gender-screen": ["male"] }`
|
||||||
|
3. **При переходе вперед** (нажатие Continue или автопереход):
|
||||||
|
- Вызывается `buildSessionDataFromScreen()` для текущего экрана
|
||||||
|
- Создается объект с вложенной структурой: `{ profile: { gender: "male" } }`
|
||||||
|
- Вызывается `updateSession()` с данными:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
answers: { "gender-screen": ["male"] }, // Старая логика
|
||||||
|
profile: { gender: "male" } // Новая логика с registrationFieldKey
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **Данные отправляются в API** и сохраняются в сессии пользователя
|
||||||
|
|
||||||
|
#### **Передача в регистрацию (при авторизации):**
|
||||||
|
|
||||||
|
1. **При переходе на email экран** вызывается функция `buildRegistrationDataFromAnswers()`
|
||||||
|
2. **Функция обрабатывает** все list single selection экраны с `registrationFieldKey`
|
||||||
|
3. **Создается объект** с вложенной структурой из всех экранов: `{ profile: { gender: "male", relationship_status: "single" } }`
|
||||||
|
4. **При авторизации** этот объект объединяется с базовым payload
|
||||||
|
5. **Отправляется на сервер** в составе `ICreateAuthorizeRequest`
|
||||||
|
|
||||||
|
### 4. Структура payload регистрации
|
||||||
|
|
||||||
|
**Базовый payload (без registrationFieldKey):**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
email: "user@example.com",
|
||||||
|
timezone: "Europe/Moscow",
|
||||||
|
locale: "en",
|
||||||
|
source: "funnel-id",
|
||||||
|
sign: true,
|
||||||
|
signDate: "2024-01-01T00:00:00.000Z",
|
||||||
|
feature: "stripe"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**С registrationFieldKey (profile.gender = "male"):**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
email: "user@example.com",
|
||||||
|
timezone: "Europe/Moscow",
|
||||||
|
locale: "en",
|
||||||
|
source: "funnel-id",
|
||||||
|
sign: true,
|
||||||
|
signDate: "2024-01-01T00:00:00.000Z",
|
||||||
|
feature: "stripe",
|
||||||
|
profile: {
|
||||||
|
gender: "male"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**С несколькими registrationFieldKey:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
email: "user@example.com",
|
||||||
|
timezone: "Europe/Moscow",
|
||||||
|
locale: "en",
|
||||||
|
source: "funnel-id",
|
||||||
|
sign: true,
|
||||||
|
signDate: "2024-01-01T00:00:00.000Z",
|
||||||
|
feature: "stripe",
|
||||||
|
profile: {
|
||||||
|
gender: "male",
|
||||||
|
relationship_status: "single"
|
||||||
|
},
|
||||||
|
partner: {
|
||||||
|
gender: "female"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Полный пример воронки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"id": "dating-funnel",
|
||||||
|
"title": "Dating Profile",
|
||||||
|
"firstScreenId": "gender"
|
||||||
|
},
|
||||||
|
"screens": [
|
||||||
|
{
|
||||||
|
"id": "gender",
|
||||||
|
"template": "list",
|
||||||
|
"title": { "text": "What is your gender?" },
|
||||||
|
"list": {
|
||||||
|
"selectionType": "single",
|
||||||
|
"registrationFieldKey": "profile.gender",
|
||||||
|
"options": [
|
||||||
|
{ "id": "male", "label": "Male", "emoji": "👨" },
|
||||||
|
{ "id": "female", "label": "Female", "emoji": "👩" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"defaultNextScreenId": "relationship-status"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "relationship-status",
|
||||||
|
"template": "list",
|
||||||
|
"title": { "text": "What is your relationship status?" },
|
||||||
|
"list": {
|
||||||
|
"selectionType": "single",
|
||||||
|
"registrationFieldKey": "profile.relationship_status",
|
||||||
|
"options": [
|
||||||
|
{ "id": "single", "label": "Single" },
|
||||||
|
{ "id": "relationship", "label": "In a relationship" },
|
||||||
|
{ "id": "married", "label": "Married" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"defaultNextScreenId": "partner-gender"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "partner-gender",
|
||||||
|
"template": "list",
|
||||||
|
"title": { "text": "What is your partner's gender?" },
|
||||||
|
"list": {
|
||||||
|
"selectionType": "single",
|
||||||
|
"registrationFieldKey": "partner.gender",
|
||||||
|
"options": [
|
||||||
|
{ "id": "male", "label": "Male", "emoji": "👨" },
|
||||||
|
{ "id": "female", "label": "Female", "emoji": "👩" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"defaultNextScreenId": "email"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "email",
|
||||||
|
"template": "email",
|
||||||
|
"title": { "text": "Enter your email" },
|
||||||
|
"emailInput": {
|
||||||
|
"label": "Email",
|
||||||
|
"placeholder": "your@email.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат после прохождения воронки:**
|
||||||
|
|
||||||
|
Если пользователь выбрал:
|
||||||
|
- Gender: Male
|
||||||
|
- Relationship Status: Single
|
||||||
|
- Partner Gender: Female
|
||||||
|
- Email: user@example.com
|
||||||
|
|
||||||
|
Payload регистрации будет:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
email: "user@example.com",
|
||||||
|
timezone: "Europe/Moscow",
|
||||||
|
locale: "en",
|
||||||
|
source: "dating-funnel",
|
||||||
|
sign: true,
|
||||||
|
signDate: "2024-01-01T00:00:00.000Z",
|
||||||
|
feature: "stripe",
|
||||||
|
profile: {
|
||||||
|
gender: "male",
|
||||||
|
relationship_status: "single"
|
||||||
|
},
|
||||||
|
partner: {
|
||||||
|
gender: "female"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
1. **Только для single selection** - работает только с `selectionType: "single"`
|
||||||
|
2. **Только ID опции** - передается именно `id` выбранной опции, а не `label` или `value`
|
||||||
|
3. **Перезапись значений** - если несколько экранов используют один и тот же ключ, последний перезапишет предыдущий
|
||||||
|
4. **Обязательный email экран** - данные передаются только при авторизации через email экран
|
||||||
|
|
||||||
|
## Техническая реализация
|
||||||
|
|
||||||
|
### Файлы
|
||||||
|
|
||||||
|
- **types.ts** - добавлено поле `registrationFieldKey` в `ListScreenDefinition`
|
||||||
|
- **ListScreenConfig.tsx** - UI для настройки ключа в админке
|
||||||
|
- **registrationHelpers.ts** - утилиты `buildRegistrationDataFromAnswers()` и `buildSessionDataFromScreen()`
|
||||||
|
- **FunnelRuntime.tsx** - вызывает `buildSessionDataFromScreen()` при переходе вперед и передает в `updateSession()`
|
||||||
|
- **useAuth.ts** - принимает `registrationData` и объединяет с базовым payload
|
||||||
|
- **EmailTemplate.tsx** - вызывает `buildRegistrationDataFromAnswers()` и передает в `useAuth`
|
||||||
|
- **screenRenderer.tsx** - передает `answers` в `EmailTemplate`
|
||||||
|
|
||||||
|
### Функция buildRegistrationDataFromAnswers
|
||||||
|
|
||||||
|
Используется при авторизации для сбора данных со всех экранов воронки:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function buildRegistrationDataFromAnswers(
|
||||||
|
funnel: FunnelDefinition,
|
||||||
|
answers: FunnelAnswers
|
||||||
|
): RegistrationDataObject {
|
||||||
|
const registrationData: RegistrationDataObject = {};
|
||||||
|
|
||||||
|
for (const screen of funnel.screens) {
|
||||||
|
if (screen.template === "list") {
|
||||||
|
const listScreen = screen as ListScreenDefinition;
|
||||||
|
|
||||||
|
if (
|
||||||
|
listScreen.list.selectionType === "single" &&
|
||||||
|
listScreen.list.registrationFieldKey &&
|
||||||
|
answers[screen.id] &&
|
||||||
|
answers[screen.id].length > 0
|
||||||
|
) {
|
||||||
|
const selectedId = answers[screen.id][0];
|
||||||
|
const fieldKey = listScreen.list.registrationFieldKey;
|
||||||
|
|
||||||
|
// Устанавливаем значение по многоуровневому ключу
|
||||||
|
setNestedValue(registrationData, fieldKey, selectedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return registrationData;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Функция buildSessionDataFromScreen
|
||||||
|
|
||||||
|
Используется при переходе вперед для сбора данных с текущего экрана:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function buildSessionDataFromScreen(
|
||||||
|
screen: { template: string; id: string; list?: { selectionType?: string; registrationFieldKey?: string } },
|
||||||
|
selectedIds: string[]
|
||||||
|
): RegistrationDataObject {
|
||||||
|
const sessionData: RegistrationDataObject = {};
|
||||||
|
|
||||||
|
if (screen.template === "list" && screen.list) {
|
||||||
|
const { selectionType, registrationFieldKey } = screen.list;
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectionType === "single" &&
|
||||||
|
registrationFieldKey &&
|
||||||
|
selectedIds.length > 0
|
||||||
|
) {
|
||||||
|
const selectedId = selectedIds[0];
|
||||||
|
setNestedValue(sessionData, registrationFieldKey, selectedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionData;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Используйте понятные ID** - ID опций должны соответствовать ожидаемым значениям на сервере
|
||||||
|
2. **Документируйте ключи** - ведите список используемых `registrationFieldKey` для избежания конфликтов
|
||||||
|
3. **Проверяйте типы** - убедитесь что ID опций соответствуют типам полей в `ICreateAuthorizeRequest`
|
||||||
|
4. **Тестируйте payload** - проверяйте что данные корректно попадают в регистрацию
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### Простой профиль
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"list": {
|
||||||
|
"selectionType": "single",
|
||||||
|
"registrationFieldKey": "profile.gender",
|
||||||
|
"options": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вложенная структура
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"list": {
|
||||||
|
"selectionType": "single",
|
||||||
|
"registrationFieldKey": "partner.birthplace.country",
|
||||||
|
"options": [
|
||||||
|
{ "id": "US", "label": "United States" },
|
||||||
|
{ "id": "UK", "label": "United Kingdom" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Без регистрации (обычный list)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"list": {
|
||||||
|
"selectionType": "single",
|
||||||
|
// registrationFieldKey не указан - данные не попадут в регистрацию
|
||||||
|
"options": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
2685
public/funnels/soulmate_prod.json
Normal file
2685
public/funnels/soulmate_prod.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -174,6 +174,23 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-semibold text-foreground">Регистрация пользователя</h4>
|
||||||
|
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||||||
|
Ключ поля для регистрации (необязательно)
|
||||||
|
<TextInput
|
||||||
|
placeholder="Например: profile.birthdate"
|
||||||
|
value={dateScreen.dateInput?.registrationFieldKey ?? ""}
|
||||||
|
onChange={(event) => handleDateInputChange("registrationFieldKey", event.target.value || undefined)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="rounded-lg border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
|
||||||
|
<p><strong>Использование:</strong> Выбранная дата будет передана в регистрацию и сессию по указанному ключу.</p>
|
||||||
|
<p className="mt-1"><strong>Формат даты:</strong> YYYY-MM-DD HH:mm (например: <code className="bg-muted px-1 rounded">2000-07-03 12:00</code>)</p>
|
||||||
|
<p className="mt-1"><strong>Пример:</strong> <code className="bg-muted px-1 rounded">profile.birthdate</code> → <code className="bg-muted px-1 rounded">{`{ profile: { birthdate: "2000-07-03 12:00" } }`}</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,6 +48,15 @@ export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRegistrationFieldKeyChange = (value: string) => {
|
||||||
|
onUpdate({
|
||||||
|
list: {
|
||||||
|
...listScreen.list,
|
||||||
|
registrationFieldKey: value || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleOptionChange = (
|
const handleOptionChange = (
|
||||||
index: number,
|
index: number,
|
||||||
@ -151,6 +160,24 @@ export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{listScreen.list.selectionType === "single" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-semibold text-foreground">Регистрация пользователя</h4>
|
||||||
|
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||||||
|
Ключ поля для регистрации (необязательно)
|
||||||
|
<TextInput
|
||||||
|
placeholder="Например: profile.gender"
|
||||||
|
value={listScreen.list.registrationFieldKey ?? ""}
|
||||||
|
onChange={(event) => handleRegistrationFieldKeyChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="rounded-lg border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
|
||||||
|
<p><strong>Использование:</strong> Выбранный ID варианта будет передан в регистрацию пользователя по указанному ключу.</p>
|
||||||
|
<p className="mt-1"><strong>Пример:</strong> <code className="bg-muted px-1 rounded">profile.gender</code> → <code className="bg-muted px-1 rounded">{`{ profile: { gender: "selected-id" } }`}</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-sm font-semibold text-foreground">Настройка вариантов</h4>
|
<h4 className="text-sm font-semibold text-foreground">Настройка вариантов</h4>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import type {
|
|||||||
} from "@/lib/funnel/types";
|
} from "@/lib/funnel/types";
|
||||||
import { getZodiacSign } from "@/lib/funnel/zodiac";
|
import { getZodiacSign } from "@/lib/funnel/zodiac";
|
||||||
import { useSession } from "@/hooks/session/useSession";
|
import { useSession } from "@/hooks/session/useSession";
|
||||||
|
import { buildSessionDataFromScreen } from "@/lib/funnel/registrationHelpers";
|
||||||
|
|
||||||
// Функция для оценки длины пути пользователя на основе текущих ответов
|
// Функция для оценки длины пути пользователя на основе текущих ответов
|
||||||
function estimatePathLength(
|
function estimatePathLength(
|
||||||
@ -128,10 +129,26 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
|
|||||||
|
|
||||||
const handleContinue = () => {
|
const handleContinue = () => {
|
||||||
if (answers[currentScreen.id] && currentScreen.template !== "email") {
|
if (answers[currentScreen.id] && currentScreen.template !== "email") {
|
||||||
|
// Собираем данные для сессии
|
||||||
|
const sessionData = buildSessionDataFromScreen(
|
||||||
|
currentScreen,
|
||||||
|
answers[currentScreen.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Для date экранов с registrationFieldKey НЕ отправляем answers
|
||||||
|
const shouldSkipAnswers =
|
||||||
|
currentScreen.template === "date" &&
|
||||||
|
"dateInput" in currentScreen &&
|
||||||
|
currentScreen.dateInput?.registrationFieldKey;
|
||||||
|
|
||||||
updateSession({
|
updateSession({
|
||||||
answers: {
|
...(shouldSkipAnswers ? {} : {
|
||||||
[currentScreen.id]: answers[currentScreen.id],
|
answers: {
|
||||||
},
|
[currentScreen.id]: answers[currentScreen.id],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Добавляем данные с registrationFieldKey
|
||||||
|
...sessionData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const nextScreenId = resolveNextScreenId(
|
const nextScreenId = resolveNextScreenId(
|
||||||
@ -217,10 +234,15 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
|
|||||||
|
|
||||||
// Auto-advance for single selection without action button
|
// Auto-advance for single selection without action button
|
||||||
if (shouldAutoAdvance) {
|
if (shouldAutoAdvance) {
|
||||||
|
// Собираем данные для сессии
|
||||||
|
const sessionData = buildSessionDataFromScreen(currentScreen, ids);
|
||||||
|
|
||||||
updateSession({
|
updateSession({
|
||||||
answers: {
|
answers: {
|
||||||
[currentScreen.id]: ids,
|
[currentScreen.id]: ids,
|
||||||
},
|
},
|
||||||
|
// Добавляем данные с registrationFieldKey если они есть
|
||||||
|
...sessionData,
|
||||||
});
|
});
|
||||||
const nextScreenId = resolveNextScreenId(
|
const nextScreenId = resolveNextScreenId(
|
||||||
currentScreen,
|
currentScreen,
|
||||||
|
|||||||
@ -23,8 +23,17 @@ const meta: Meta<typeof EmailTemplate> = {
|
|||||||
screenProgress: { current: 9, total: 10 },
|
screenProgress: { current: 9, total: 10 },
|
||||||
defaultTexts: {
|
defaultTexts: {
|
||||||
nextButton: "Next",
|
nextButton: "Next",
|
||||||
|
|
||||||
},
|
},
|
||||||
|
funnel: {
|
||||||
|
meta: {
|
||||||
|
id: "test-funnel",
|
||||||
|
title: "Test Funnel",
|
||||||
|
},
|
||||||
|
screens: [],
|
||||||
|
},
|
||||||
|
selectedEmail: "",
|
||||||
|
onEmailChange: fn(),
|
||||||
|
answers: {},
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
screen: {
|
screen: {
|
||||||
|
|||||||
@ -8,7 +8,9 @@ import type {
|
|||||||
EmailScreenDefinition,
|
EmailScreenDefinition,
|
||||||
DefaultTexts,
|
DefaultTexts,
|
||||||
FunnelDefinition,
|
FunnelDefinition,
|
||||||
|
FunnelAnswers,
|
||||||
} from "@/lib/funnel/types";
|
} from "@/lib/funnel/types";
|
||||||
|
import { buildRegistrationDataFromAnswers } from "@/lib/funnel/registrationHelpers";
|
||||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers";
|
import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@ -33,6 +35,7 @@ interface EmailTemplateProps {
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
screenProgress?: { current: number; total: number };
|
screenProgress?: { current: number; total: number };
|
||||||
defaultTexts?: DefaultTexts;
|
defaultTexts?: DefaultTexts;
|
||||||
|
answers: FunnelAnswers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmailTemplate({
|
export function EmailTemplate({
|
||||||
@ -45,9 +48,14 @@ export function EmailTemplate({
|
|||||||
onBack,
|
onBack,
|
||||||
screenProgress,
|
screenProgress,
|
||||||
defaultTexts,
|
defaultTexts,
|
||||||
|
answers,
|
||||||
}: EmailTemplateProps) {
|
}: EmailTemplateProps) {
|
||||||
|
// Собираем данные для регистрации из ответов воронки
|
||||||
|
const registrationData = buildRegistrationDataFromAnswers(funnel, answers);
|
||||||
|
|
||||||
const { authorization, isLoading, error } = useAuth({
|
const { authorization, isLoading, error } = useAuth({
|
||||||
funnelId: funnel?.meta?.id ?? "preview",
|
funnelId: funnel?.meta?.id ?? "preview",
|
||||||
|
registrationData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isTouched, setIsTouched] = useState(false);
|
const [isTouched, setIsTouched] = useState(false);
|
||||||
|
|||||||
@ -50,7 +50,9 @@ export default function SelectInput({
|
|||||||
"appearance-none",
|
"appearance-none",
|
||||||
"w-full min-w-[106px] h-fit! min-h-14",
|
"w-full min-w-[106px] h-fit! min-h-14",
|
||||||
"px-4 py-3.5",
|
"px-4 py-3.5",
|
||||||
"font-inter text-[18px]/[28px] font-medium text-placeholder-foreground",
|
"font-inter text-[18px]/[28px] font-medium",
|
||||||
|
// Цвет placeholder когда ничего не выбрано
|
||||||
|
props.value === "" || props.value === undefined ? "text-placeholder-foreground" : "text-foreground",
|
||||||
"rounded-2xl outline-2 outline-primary/30",
|
"rounded-2xl outline-2 outline-primary/30",
|
||||||
"duration-200",
|
"duration-200",
|
||||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
|
|||||||
@ -13,9 +13,15 @@ const locale = "en";
|
|||||||
|
|
||||||
interface IUseAuthProps {
|
interface IUseAuthProps {
|
||||||
funnelId: string;
|
funnelId: string;
|
||||||
|
/**
|
||||||
|
* Дополнительные данные для регистрации пользователя.
|
||||||
|
* Будут объединены с базовым payload при авторизации.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
registrationData?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuth = ({ funnelId }: IUseAuthProps) => {
|
export const useAuth = ({ funnelId, registrationData }: IUseAuthProps) => {
|
||||||
const { updateSession } = useSession({ funnelId });
|
const { updateSession } = useSession({ funnelId });
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@ -35,35 +41,24 @@ export const useAuth = ({ funnelId }: IUseAuthProps) => {
|
|||||||
const getAuthorizationPayload = useCallback(
|
const getAuthorizationPayload = useCallback(
|
||||||
(email: string): ICreateAuthorizeRequest => {
|
(email: string): ICreateAuthorizeRequest => {
|
||||||
const timezone = getClientTimezone();
|
const timezone = getClientTimezone();
|
||||||
return filterNullKeysOfObject<ICreateAuthorizeRequest>({
|
const basePayload = {
|
||||||
timezone,
|
timezone,
|
||||||
locale,
|
locale,
|
||||||
email,
|
email,
|
||||||
source: funnelId,
|
source: funnelId,
|
||||||
// source: "aura.compatibility.v2",
|
|
||||||
// profile: {
|
|
||||||
// name: username || "",
|
|
||||||
// gender: EGender[gender as keyof typeof EGender] || null,
|
|
||||||
// birthdate: formatDate(`${birthdate} ${birthtime}`),
|
|
||||||
// birthplace: {
|
|
||||||
// address: birthPlace,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// partner: {
|
|
||||||
// name: partnerName,
|
|
||||||
// gender: EGender[partnerGender as keyof typeof EGender] || null,
|
|
||||||
// birthdate: formatDate(partnerBirthdate),
|
|
||||||
// birthplace: {
|
|
||||||
// address: partnerBirthPlace,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
sign: true,
|
sign: true,
|
||||||
signDate: new Date().toISOString(),
|
signDate: new Date().toISOString(),
|
||||||
// feature: feature.includes("black") ? "ios" : feature,
|
|
||||||
feature: "stripe"
|
feature: "stripe"
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Объединяем базовый payload с данными регистрации из воронки
|
||||||
|
const mergedPayload = registrationData
|
||||||
|
? { ...basePayload, ...registrationData }
|
||||||
|
: basePayload;
|
||||||
|
|
||||||
|
return filterNullKeysOfObject<ICreateAuthorizeRequest>(mergedPayload);
|
||||||
},
|
},
|
||||||
[funnelId]
|
[funnelId, registrationData]
|
||||||
);
|
);
|
||||||
|
|
||||||
const authorization = useCallback(
|
const authorization = useCallback(
|
||||||
|
|||||||
@ -57,8 +57,7 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
|||||||
feature: "stripe",
|
feature: "stripe",
|
||||||
locale,
|
locale,
|
||||||
timezone,
|
timezone,
|
||||||
// source: funnelId,
|
source: funnelId,
|
||||||
source: "aura.compatibility.v2",
|
|
||||||
sign: false,
|
sign: false,
|
||||||
utm,
|
utm,
|
||||||
domain: window.location.hostname,
|
domain: window.location.hostname,
|
||||||
@ -90,8 +89,7 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
|||||||
sessionId: "",
|
sessionId: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [sessionId, timezone, setSessionId]);
|
}, [sessionId, timezone, setSessionId, funnelId]);
|
||||||
// localStorageKey, sessionId, timezone, utm
|
|
||||||
|
|
||||||
const updateSession = useCallback(
|
const updateSession = useCallback(
|
||||||
async (data: IUpdateSessionRequest["data"]) => {
|
async (data: IUpdateSessionRequest["data"]) => {
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
import type { FunnelDefinition } from "./types";
|
import type { FunnelDefinition } from "./types";
|
||||||
|
|
||||||
export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
||||||
"soulmate": {
|
"soulmate_prod": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"id": "soulmate",
|
"id": "soulmate_prod",
|
||||||
"title": "Soulmate V1",
|
"title": "Soulmate V1",
|
||||||
"description": "Soulmate",
|
"description": "Soulmate",
|
||||||
"firstScreenId": "onboarding"
|
"firstScreenId": "onboarding"
|
||||||
@ -2196,9 +2196,9 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
|||||||
"showPrivacyTermsConsent": false
|
"showPrivacyTermsConsent": false
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
|
"rules": [],
|
||||||
"defaultNextScreenId": "payment",
|
"defaultNextScreenId": "payment",
|
||||||
"isEndScreen": false,
|
"isEndScreen": false
|
||||||
"rules": []
|
|
||||||
},
|
},
|
||||||
"coupon": {
|
"coupon": {
|
||||||
"title": {
|
"title": {
|
||||||
@ -2690,5 +2690,305 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"soulmate": {
|
||||||
|
"meta": {
|
||||||
|
"id": "soulmate",
|
||||||
|
"title": "Новая воронка",
|
||||||
|
"description": "Описание новой воронки",
|
||||||
|
"firstScreenId": "onboarding"
|
||||||
|
},
|
||||||
|
"defaultTexts": {
|
||||||
|
"nextButton": "Continue"
|
||||||
|
},
|
||||||
|
"screens": [
|
||||||
|
{
|
||||||
|
"id": "onboarding",
|
||||||
|
"template": "info",
|
||||||
|
"title": {
|
||||||
|
"text": "Добро пожаловать!",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "bold",
|
||||||
|
"size": "md",
|
||||||
|
"align": "center",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"text": "Это ваша новая воронка. Начните редактирование.",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "regular",
|
||||||
|
"size": "md",
|
||||||
|
"align": "center",
|
||||||
|
"color": "muted"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"rules": [],
|
||||||
|
"defaultNextScreenId": "gender",
|
||||||
|
"isEndScreen": false
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"type": "emoji",
|
||||||
|
"value": "🎯",
|
||||||
|
"size": "lg"
|
||||||
|
},
|
||||||
|
"variables": [],
|
||||||
|
"variants": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gender",
|
||||||
|
"template": "list",
|
||||||
|
"header": {
|
||||||
|
"showBackButton": true,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"text": "Новый экран",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "bold",
|
||||||
|
"size": "2xl",
|
||||||
|
"align": "left",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"text": "Добавьте детали справа",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "medium",
|
||||||
|
"size": "lg",
|
||||||
|
"align": "left",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"bottomActionButton": {
|
||||||
|
"show": false,
|
||||||
|
"cornerRadius": "3xl",
|
||||||
|
"showPrivacyTermsConsent": false
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"rules": [],
|
||||||
|
"defaultNextScreenId": "example",
|
||||||
|
"isEndScreen": false
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"selectionType": "single",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"id": "male",
|
||||||
|
"label": "male",
|
||||||
|
"disabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "female",
|
||||||
|
"label": "female",
|
||||||
|
"disabled": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"registrationFieldKey": "profile.gender"
|
||||||
|
},
|
||||||
|
"variants": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "example",
|
||||||
|
"template": "list",
|
||||||
|
"header": {
|
||||||
|
"showBackButton": true,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"text": "Новый экран",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "bold",
|
||||||
|
"size": "2xl",
|
||||||
|
"align": "left",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"text": "Добавьте детали справа",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "medium",
|
||||||
|
"size": "lg",
|
||||||
|
"align": "left",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"bottomActionButton": {
|
||||||
|
"show": true,
|
||||||
|
"cornerRadius": "3xl",
|
||||||
|
"showPrivacyTermsConsent": false
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"rules": [],
|
||||||
|
"defaultNextScreenId": "birthdate",
|
||||||
|
"isEndScreen": false
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"selectionType": "single",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"id": "option-1",
|
||||||
|
"label": "Вариант 1",
|
||||||
|
"disabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "option-2",
|
||||||
|
"label": "Вариант 2",
|
||||||
|
"disabled": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variants": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "birthdate",
|
||||||
|
"template": "date",
|
||||||
|
"header": {
|
||||||
|
"showBackButton": true,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"text": "Новый экран",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "bold",
|
||||||
|
"size": "2xl",
|
||||||
|
"align": "left",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"text": "Добавьте детали справа",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "medium",
|
||||||
|
"size": "lg",
|
||||||
|
"align": "left",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"bottomActionButton": {
|
||||||
|
"show": true,
|
||||||
|
"cornerRadius": "3xl",
|
||||||
|
"showPrivacyTermsConsent": false
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"rules": [],
|
||||||
|
"defaultNextScreenId": "email",
|
||||||
|
"isEndScreen": false
|
||||||
|
},
|
||||||
|
"dateInput": {
|
||||||
|
"monthLabel": "Месяц",
|
||||||
|
"dayLabel": "День",
|
||||||
|
"yearLabel": "Год",
|
||||||
|
"monthPlaceholder": "ММ",
|
||||||
|
"dayPlaceholder": "ДД",
|
||||||
|
"yearPlaceholder": "ГГГГ",
|
||||||
|
"showSelectedDate": true,
|
||||||
|
"selectedDateFormat": "dd MMMM yyyy",
|
||||||
|
"selectedDateLabel": "Выбранная дата:",
|
||||||
|
"zodiac": {
|
||||||
|
"enabled": true,
|
||||||
|
"storageKey": "userZodiac"
|
||||||
|
},
|
||||||
|
"registrationFieldKey": "profile.birthdate"
|
||||||
|
},
|
||||||
|
"variants": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "email",
|
||||||
|
"template": "email",
|
||||||
|
"header": {
|
||||||
|
"showBackButton": true,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"text": "Портрет твоей второй половинки готов! Куда нам его отправить?",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "bold",
|
||||||
|
"size": "2xl",
|
||||||
|
"align": "center",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"bottomActionButton": {
|
||||||
|
"show": true,
|
||||||
|
"cornerRadius": "3xl",
|
||||||
|
"showPrivacyTermsConsent": true
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"rules": [],
|
||||||
|
"defaultNextScreenId": "final",
|
||||||
|
"isEndScreen": false
|
||||||
|
},
|
||||||
|
"emailInput": {
|
||||||
|
"label": "Email адрес",
|
||||||
|
"placeholder": "example@email.com"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"src": "/female-portrait.jpg"
|
||||||
|
},
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"screenId": "gender",
|
||||||
|
"operator": "includesAny",
|
||||||
|
"optionIds": [
|
||||||
|
"male"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"overrides": {
|
||||||
|
"image": {
|
||||||
|
"src": "/male-portrait.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "final",
|
||||||
|
"template": "info",
|
||||||
|
"header": {
|
||||||
|
"showBackButton": true,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"text": "Спасибо за регистрацию",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "bold",
|
||||||
|
"size": "2xl",
|
||||||
|
"align": "center",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"text": "Добавьте подзаголовок для информационного экрана",
|
||||||
|
"show": true,
|
||||||
|
"font": "manrope",
|
||||||
|
"weight": "medium",
|
||||||
|
"size": "lg",
|
||||||
|
"align": "center",
|
||||||
|
"color": "default"
|
||||||
|
},
|
||||||
|
"bottomActionButton": {
|
||||||
|
"show": false,
|
||||||
|
"cornerRadius": "3xl",
|
||||||
|
"showPrivacyTermsConsent": false
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"rules": [],
|
||||||
|
"isEndScreen": true
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"type": "emoji",
|
||||||
|
"value": "ℹ️",
|
||||||
|
"size": "xl"
|
||||||
|
},
|
||||||
|
"variables": [],
|
||||||
|
"variants": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
141
src/lib/funnel/registrationHelpers.ts
Normal file
141
src/lib/funnel/registrationHelpers.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import type { FunnelDefinition, FunnelAnswers, ListScreenDefinition, DateScreenDefinition } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тип для вложенных объектов регистрации
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type RegistrationDataObject = Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает значение в объекте по многоуровневому ключу через точку.
|
||||||
|
* Например: setNestedValue({}, "profile.gender", "male") → { profile: { gender: "male" } }
|
||||||
|
*/
|
||||||
|
function setNestedValue(obj: RegistrationDataObject, path: string, value: string): void {
|
||||||
|
const keys = path.split(".");
|
||||||
|
let current = obj;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
if (!current[key] || typeof current[key] !== "object") {
|
||||||
|
current[key] = {};
|
||||||
|
}
|
||||||
|
current = current[key] as RegistrationDataObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastKey = keys[keys.length - 1];
|
||||||
|
current[lastKey] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Собирает данные для регистрации пользователя из ответов воронки.
|
||||||
|
* Обрабатывает list single selection и date экраны с registrationFieldKey.
|
||||||
|
*
|
||||||
|
* @param funnel - Определение воронки
|
||||||
|
* @param answers - Ответы пользователя
|
||||||
|
* @returns Объект с данными для регистрации
|
||||||
|
*/
|
||||||
|
export function buildRegistrationDataFromAnswers(
|
||||||
|
funnel: FunnelDefinition,
|
||||||
|
answers: FunnelAnswers
|
||||||
|
): RegistrationDataObject {
|
||||||
|
const registrationData: RegistrationDataObject = {};
|
||||||
|
|
||||||
|
// Проходим по всем экранам воронки
|
||||||
|
for (const screen of funnel.screens) {
|
||||||
|
// Обрабатываем list single selection экраны с registrationFieldKey
|
||||||
|
if (screen.template === "list") {
|
||||||
|
const listScreen = screen as ListScreenDefinition;
|
||||||
|
|
||||||
|
if (
|
||||||
|
listScreen.list.selectionType === "single" &&
|
||||||
|
listScreen.list.registrationFieldKey &&
|
||||||
|
answers[screen.id] &&
|
||||||
|
answers[screen.id].length > 0
|
||||||
|
) {
|
||||||
|
const selectedId = answers[screen.id][0];
|
||||||
|
const fieldKey = listScreen.list.registrationFieldKey;
|
||||||
|
setNestedValue(registrationData, fieldKey, selectedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем date экраны с registrationFieldKey
|
||||||
|
if (screen.template === "date") {
|
||||||
|
const dateScreen = screen as DateScreenDefinition;
|
||||||
|
|
||||||
|
if (
|
||||||
|
dateScreen.dateInput?.registrationFieldKey &&
|
||||||
|
answers[screen.id] &&
|
||||||
|
answers[screen.id].length === 3
|
||||||
|
) {
|
||||||
|
const [month, day, year] = answers[screen.id];
|
||||||
|
const monthNum = parseInt(month);
|
||||||
|
const dayNum = parseInt(day);
|
||||||
|
const yearNum = parseInt(year);
|
||||||
|
|
||||||
|
// Проверяем что дата валидна
|
||||||
|
if (monthNum >= 1 && monthNum <= 12 && dayNum >= 1 && dayNum <= 31 && yearNum > 1900) {
|
||||||
|
// Форматируем в YYYY-MM-DD HH:mm с временем 12:00
|
||||||
|
const formattedDate = `${yearNum}-${monthNum.toString().padStart(2, '0')}-${dayNum.toString().padStart(2, '0')} 12:00`;
|
||||||
|
const fieldKey = dateScreen.dateInput.registrationFieldKey;
|
||||||
|
setNestedValue(registrationData, fieldKey, formattedDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return registrationData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Собирает данные для сессии из одного экрана с registrationFieldKey.
|
||||||
|
* Используется при отправке данных в updateSession для конкретного экрана.
|
||||||
|
*
|
||||||
|
* @param screen - Определение экрана
|
||||||
|
* @param selectedIds - Выбранные значения пользователя на этом экране
|
||||||
|
* @returns Объект с данными для сессии (может быть пустым если нет registrationFieldKey)
|
||||||
|
*/
|
||||||
|
export function buildSessionDataFromScreen(
|
||||||
|
screen: {
|
||||||
|
template: string;
|
||||||
|
id: string;
|
||||||
|
list?: { selectionType?: string; registrationFieldKey?: string };
|
||||||
|
dateInput?: { registrationFieldKey?: string };
|
||||||
|
},
|
||||||
|
selectedIds: string[]
|
||||||
|
): RegistrationDataObject {
|
||||||
|
const sessionData: RegistrationDataObject = {};
|
||||||
|
|
||||||
|
// Обрабатываем list single selection экраны с registrationFieldKey
|
||||||
|
if (screen.template === "list" && screen.list) {
|
||||||
|
const { selectionType, registrationFieldKey } = screen.list;
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectionType === "single" &&
|
||||||
|
registrationFieldKey &&
|
||||||
|
selectedIds.length > 0
|
||||||
|
) {
|
||||||
|
const selectedId = selectedIds[0];
|
||||||
|
setNestedValue(sessionData, registrationFieldKey, selectedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем date экраны с registrationFieldKey
|
||||||
|
if (screen.template === "date" && screen.dateInput?.registrationFieldKey) {
|
||||||
|
// Для date экранов selectedIds = [month, day, year]
|
||||||
|
if (selectedIds.length === 3) {
|
||||||
|
const [month, day, year] = selectedIds;
|
||||||
|
const monthNum = parseInt(month);
|
||||||
|
const dayNum = parseInt(day);
|
||||||
|
const yearNum = parseInt(year);
|
||||||
|
|
||||||
|
// Проверяем что дата валидна
|
||||||
|
if (monthNum >= 1 && monthNum <= 12 && dayNum >= 1 && dayNum <= 31 && yearNum > 1900) {
|
||||||
|
// Форматируем в YYYY-MM-DD HH:mm с временем 12:00
|
||||||
|
const formattedDate = `${yearNum}-${monthNum.toString().padStart(2, '0')}-${dayNum.toString().padStart(2, '0')} 12:00`;
|
||||||
|
setNestedValue(sessionData, screen.dateInput.registrationFieldKey, formattedDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionData;
|
||||||
|
}
|
||||||
@ -233,6 +233,7 @@ const TEMPLATE_REGISTRY: Record<
|
|||||||
screenProgress,
|
screenProgress,
|
||||||
defaultTexts,
|
defaultTexts,
|
||||||
funnel,
|
funnel,
|
||||||
|
answers,
|
||||||
}) => {
|
}) => {
|
||||||
const emailScreen = screen as EmailScreenDefinition;
|
const emailScreen = screen as EmailScreenDefinition;
|
||||||
|
|
||||||
@ -254,6 +255,7 @@ const TEMPLATE_REGISTRY: Record<
|
|||||||
screenProgress={screenProgress}
|
screenProgress={screenProgress}
|
||||||
defaultTexts={defaultTexts}
|
defaultTexts={defaultTexts}
|
||||||
funnel={funnel}
|
funnel={funnel}
|
||||||
|
answers={answers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -190,6 +190,13 @@ export interface DateInputDefinition {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
storageKey?: string;
|
storageKey?: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Ключ поля для регистрации пользователя и сессии.
|
||||||
|
* Поддерживает многоуровневую вложенность через точку.
|
||||||
|
* Дата передается в формате: "YYYY-MM-DD HH:mm" (время всегда 12:00)
|
||||||
|
* Например: "profile.birthdate" → { profile: { birthdate: "2000-07-03 12:00" } }
|
||||||
|
*/
|
||||||
|
registrationFieldKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -266,6 +273,12 @@ export interface ListScreenDefinition {
|
|||||||
list: {
|
list: {
|
||||||
selectionType: SelectionType;
|
selectionType: SelectionType;
|
||||||
options: ListOptionDefinition[];
|
options: ListOptionDefinition[];
|
||||||
|
/**
|
||||||
|
* Ключ поля для регистрации пользователя (только для single selection).
|
||||||
|
* Поддерживает многоуровневую вложенность через точку.
|
||||||
|
* Например: "profile.gender" → { profile: { gender: "selected-id" } }
|
||||||
|
*/
|
||||||
|
registrationFieldKey?: string;
|
||||||
};
|
};
|
||||||
bottomActionButton?: BottomActionButtonDefinition;
|
bottomActionButton?: BottomActionButtonDefinition;
|
||||||
navigation?: NavigationDefinition;
|
navigation?: NavigationDefinition;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user