diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index a009243..97a2e57 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -2,6 +2,12 @@ import type { Preview } from "@storybook/nextjs-vite"; import { Geist, Geist_Mono, Inter, Manrope, Poppins } from "next/font/google"; import "../src/app/globals.css"; import React from "react"; +import { + PaymentPlacementProvider, + TrialVariantSelectionProvider, +} from "../src/entities/session/payment"; +import type { IFunnelPaymentPlacement } from "../src/entities/session/funnel/types"; +import { Currency } from "../src/shared/types"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -31,6 +37,24 @@ const poppins = Poppins({ weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], }); +// Storybook mock placement to avoid network calls +const storybookPlacement: IFunnelPaymentPlacement = { + currency: Currency.USD, + billingPeriod: "WEEK", + billingInterval: 1, + trialPeriod: "DAY", + trialInterval: 7, + placementId: "plc_story", + paywallId: "pw_story", + paymentUrl: "https://example.com/pay", + variants: [ + { id: "v1", key: "basic", type: "subscription", price: 1499, trialPrice: 100, title: "Basic" }, + { id: "v2", key: "standard", type: "subscription", price: 1499, trialPrice: 499, title: "Standard" }, + { id: "v3", key: "popular", type: "subscription", price: 1499, trialPrice: 899, title: "Popular", accent: true }, + { id: "v4", key: "premium", type: "subscription", price: 1499, trialPrice: 1367, title: "Premium" }, + ], +}; + const preview: Preview = { parameters: { controls: { @@ -58,11 +82,19 @@ const preview: Preview = { }, decorators: [ (Story) => ( -
- -
+ +
+ +
+
+ ), ], }; diff --git a/AB_TESTING_GUIDE.md b/docs/AB_TESTING_GUIDE.md similarity index 100% rename from AB_TESTING_GUIDE.md rename to docs/AB_TESTING_GUIDE.md diff --git a/AB_TESTING_IMPLEMENTATION.md b/docs/AB_TESTING_IMPLEMENTATION.md similarity index 100% rename from AB_TESTING_IMPLEMENTATION.md rename to docs/AB_TESTING_IMPLEMENTATION.md diff --git a/AB_TESTING_UPDATES.md b/docs/AB_TESTING_UPDATES.md similarity index 100% rename from AB_TESTING_UPDATES.md rename to docs/AB_TESTING_UPDATES.md diff --git a/MONGODB_SCHEMA_UPDATE.md b/docs/MONGODB_SCHEMA_UPDATE.md similarity index 100% rename from MONGODB_SCHEMA_UPDATE.md rename to docs/MONGODB_SCHEMA_UPDATE.md diff --git a/NAVIGATION_RULES_GUIDE.md b/docs/NAVIGATION_RULES_GUIDE.md similarity index 100% rename from NAVIGATION_RULES_GUIDE.md rename to docs/NAVIGATION_RULES_GUIDE.md diff --git a/docs/PAYMENT_TEMPLATE_VARIABLES.md b/docs/PAYMENT_TEMPLATE_VARIABLES.md new file mode 100644 index 0000000..cbf851f --- /dev/null +++ b/docs/PAYMENT_TEMPLATE_VARIABLES.md @@ -0,0 +1,188 @@ +# Payment Template Variables + +## Доступные переменные для подстановки + +В текстах экранов **TrialPayment** и **SpecialOffer** можно использовать следующие переменные через синтаксис `{{variableName}}`. + +### Основные переменные + +| Переменная | Описание | Пример значения | +|------------|----------|-----------------| +| `{{trialPrice}}` | Форматированная цена триала | `$1.00`, `€5.00` | +| `{{billingPrice}}` | Форматированная цена подписки | `$14.99`, `€49.99` | +| `{{trialPeriod}}` | Период триала с интервалом | `7 days`, `1 week`, `2 weeks` | +| `{{billingPeriod}}` | Период списания с интервалом | `1 week`, `1 month`, `3 months` | +| `{{trialPeriodHyphen}}` | Период триала через дефис | `7-day`, `1-week` | + +### Дополнительные переменные + +| Переменная | Описание | Пример значения | Шаблон | +|------------|----------|-----------------|---------| +| `{{oldPrice}}` | Старая цена (для скидки) | `$14.99` | TrialPayment | +| `{{discountPercent}}` | Процент скидки | `94` | TrialPayment, SpecialOffer | +| `{{oldTrialPrice}}` | Старая цена триала | `$14.99` | SpecialOffer | +| `{{oldTrialPeriod}}` | Старый период триала | `7 days` | SpecialOffer | + +## Где использовать + +### TrialPayment экран + +Переменные работают во **ВСЕХ текстовых полях** экрана, включая: + +#### `tryForDays.title` +```json +{ + "text": "Try it for {{trialPeriod}}!" +} +``` + +#### `tryForDays.textList.items` +```json +{ + "items": [ + { "text": "Start your {{trialPeriodHyphen}} trial for just {{trialPrice}}." }, + { "text": "Then only {{billingPrice}}/{{billingPeriod}} for full access." } + ] +} +``` + +#### `totalPrice.priceContainer.price` +```json +{ + "text": "{{trialPrice}}" +} +``` + +#### `totalPrice.priceContainer.oldPrice` +```json +{ + "text": "{{oldPrice}}" +} +``` + +#### `totalPrice.priceContainer.discount` +```json +{ + "text": "{{discountPercent}}% discount applied" +} +``` + +### SpecialOffer экран + +Переменные работают во всех текстовых полях: + +#### `text.title` +```json +{ + "text": "SPECIAL {{discountPercent}}% DISCOUNT" +} +``` + +#### `text.subtitle` +```json +{ + "text": "Only {{trialPrice}} for {{trialPeriod}}" +} +``` + +#### `text.description` +```json +{ + "trialPrice": { "text": "{{trialPrice}}" }, + "text": { "text": "for {{trialPeriod}}" }, + "oldTrialPrice": { "text": "{{oldTrialPrice}}" } +} +``` + +## Policy текст (хардкоженный) + +Policy текст автоматически подставляет значения **без** использования `{{}}`: + +```tsx +You also acknowledge that your {trialPeriodHyphen} introductory plan to Wit Lab LLC, +billed at {formattedTrialPrice}, will automatically renew at {formattedBillingPrice} +every {billingPeriodText} unless canceled before the end of the trial period. +``` + +**Этот текст НЕ редактируется через админку** - значения подставляются автоматически из выбранного variant. + +## Примеры использования + +### Пример 1: Try For Days секция +```json +{ + "tryForDays": { + "title": { + "text": "Try it for {{trialPeriod}}!" + }, + "textList": { + "items": [ + { "text": "Receive a hand-drawn sketch of your soulmate." }, + { "text": "Reveal the path with the guide." }, + { "text": "Talk to live experts and get guidance." }, + { "text": "Start your {{trialPeriodHyphen}} trial for just {{trialPrice}}." }, + { "text": "Then {{billingPrice}} every {{billingPeriod}} for full access." }, + { "text": "Cancel anytime—just 24 hours before renewal." } + ] + } + } +} +``` + +**Результат** (для variant с trialPrice=100, price=1499, trialInterval=7, billingInterval=1): +- "Try it for 7 days!" +- "Start your 7-day trial for just $1.00." +- "Then $14.99 every 1 week for full access." + +### Пример 2: Total Price секция +```json +{ + "totalPrice": { + "priceContainer": { + "price": { "text": "{{trialPrice}}" }, + "oldPrice": { "text": "{{oldPrice}}" }, + "discount": { "text": "{{discountPercent}}% discount applied" } + } + } +} +``` + +**Результат**: +- Price: "$1.00" +- Old Price: "$14.99" +- Discount: "94% discount applied" + +## Форматирование цен + +Цены автоматически форматируются с учетом валюты: +- USD: `$1.00` +- EUR: `€1.00` +- GBP: `£1.00` + +## Форматирование периодов + +Периоды форматируются на английском: +- `trialPeriod="DAY"`, `interval=1` → `"1 day"` +- `trialPeriod="DAY"`, `interval=7` → `"7 days"` +- `trialPeriod="WEEK"`, `interval=1` → `"1 week"` +- `trialPeriod="MONTH"`, `interval=3` → `"3 months"` + +С дефисом (`trialPeriodHyphen`): +- `"1-day"`, `"7-day"`, `"1-week"`, `"3-month"` + +## Важные замечания + +1. **Регистр имеет значение**: используйте точное написание `{{trialPrice}}`, а не `{{TrialPrice}}` +2. **Пробелы не важны**: `{{ trialPrice }}` тоже сработает +3. **Несуществующие переменные**: если переменная не найдена, она остается как есть в тексте +4. **Пустые значения**: если значение не определено (например, `discountPercent` для variant без скидки), подставляется пустая строка + +## Откуда берутся значения + +Все значения загружаются из **Payment Placement API**: +- `GET /api/session/funnel/:funnelId/payment/:paymentId` + +И зависят от **выбранного variant**: +- В **Trial Choice** пользователь выбирает variant → его ID сохраняется +- В **Trial Payment** используется выбранный variant (или первый по умолчанию) +- В **Special Offer** всегда используется первый variant из `main_secret_discount` placement diff --git a/docs/TRIAL_CHOICE_PAYMENT_INTEGRATION.md b/docs/TRIAL_CHOICE_PAYMENT_INTEGRATION.md new file mode 100644 index 0000000..e69de29 diff --git a/UNLEASH_ANALYTICS_FIX.md b/docs/UNLEASH_ANALYTICS_FIX.md similarity index 100% rename from UNLEASH_ANALYTICS_FIX.md rename to docs/UNLEASH_ANALYTICS_FIX.md diff --git a/UNLEASH_ANALYTICS_FLOW.md b/docs/UNLEASH_ANALYTICS_FLOW.md similarity index 100% rename from UNLEASH_ANALYTICS_FLOW.md rename to docs/UNLEASH_ANALYTICS_FLOW.md diff --git a/UNLEASH_LAZY_IMPRESSION_IMPLEMENTATION.md b/docs/UNLEASH_LAZY_IMPRESSION_IMPLEMENTATION.md similarity index 100% rename from UNLEASH_LAZY_IMPRESSION_IMPLEMENTATION.md rename to docs/UNLEASH_LAZY_IMPRESSION_IMPLEMENTATION.md diff --git a/UNLEASH_SETUP.md b/docs/UNLEASH_SETUP.md similarity index 100% rename from UNLEASH_SETUP.md rename to docs/UNLEASH_SETUP.md diff --git a/package-lock.json b/package-lock.json index 7fada9f..c53c0a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,11 +38,11 @@ "devDependencies": { "@chromatic-com/storybook": "^4.1.1", "@eslint/eslintrc": "^3", - "@storybook/addon-a11y": "^9.1.6", - "@storybook/addon-docs": "^9.1.6", + "@storybook/addon-a11y": "^9.1.13", + "@storybook/addon-docs": "^9.1.13", "@storybook/addon-styling-webpack": "^2.0.0", - "@storybook/addon-vitest": "^9.1.6", - "@storybook/nextjs-vite": "^9.1.6", + "@storybook/addon-vitest": "^9.1.13", + "@storybook/nextjs-vite": "^9.1.13", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", @@ -51,9 +51,9 @@ "@vitest/coverage-v8": "^3.2.4", "eslint": "^9", "eslint-config-next": "15.5.3", - "eslint-plugin-storybook": "^9.1.6", + "eslint-plugin-storybook": "^9.1.13", "playwright": "^1.55.0", - "storybook": "^9.1.6", + "storybook": "^9.1.13", "tailwindcss": "^4", "tw-animate-css": "^1.3.8", "typescript": "^5", @@ -1605,53 +1605,6 @@ } } }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3109,9 +3062,9 @@ "license": "MIT" }, "node_modules/@storybook/addon-a11y": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-9.1.6.tgz", - "integrity": "sha512-jpuzbZlT8G1hx4N6nhhmxy6Lu+Xnz1oeGb2/pm+rKx2fZ4oy7yGRliRNOvpTy8MbkpnfMoLLrcqc66s/kfdf3A==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-9.1.13.tgz", + "integrity": "sha512-4enIl1h2XSZnFKUQJJoZbp1X40lzdj7f5JE15ZhU1al4z6hHWp7i2zD7ySyDpEbMypBCz1xnLvyiyw79m1fp7w==", "dev": true, "license": "MIT", "dependencies": { @@ -3123,20 +3076,20 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.1.6" + "storybook": "^9.1.13" } }, "node_modules/@storybook/addon-docs": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.6.tgz", - "integrity": "sha512-4ZE/T2Ayw77/v2ersAk/VM7vlvqV2zCNFwt0uvOzUR1VZ9VqZCHhsfy/IyBPeKt6Otax3EpfE1LkH4slfceB0g==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.13.tgz", + "integrity": "sha512-V1nCo7bfC3kQ5VNVq0VDcHsIhQf507m+BxMA5SIYiwdJHljH2BXpW2fL3FFn9gv9Wp57AEEzhm+wh4zANaJgkg==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "9.1.6", + "@storybook/csf-plugin": "9.1.13", "@storybook/icons": "^1.4.0", - "@storybook/react-dom-shim": "9.1.6", + "@storybook/react-dom-shim": "9.1.13", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -3146,7 +3099,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.1.6" + "storybook": "^9.1.13" } }, "node_modules/@storybook/addon-styling-webpack": { @@ -3161,9 +3114,9 @@ } }, "node_modules/@storybook/addon-vitest": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-9.1.6.tgz", - "integrity": "sha512-I5kev4ZfJFP4ScTV7cfA1PMqSantcfNBio5xzO0edoMXBWugPrD22M2Z2kF+odieHGYXsQy8bc56F4KdAUiu4w==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-9.1.13.tgz", + "integrity": "sha512-g/wkQ8i1GGlsoHEe6bjWic+ESokWhuMBxAa9FDLW9KDf0L1DMyQqFFJFnGoo99zCNRVJcSXgzZTFp6SCt3FKog==", "dev": true, "license": "MIT", "dependencies": { @@ -3179,7 +3132,7 @@ "peerDependencies": { "@vitest/browser": "^3.0.0", "@vitest/runner": "^3.0.0", - "storybook": "^9.1.6", + "storybook": "^9.1.13", "vitest": "^3.0.0" }, "peerDependenciesMeta": { @@ -3195,13 +3148,13 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.6.tgz", - "integrity": "sha512-AUoSjXr4MvtkFQkfFfZSXrqVM0z80DX0sebm80nODu/qFhsJIU5trNP+XDYY8ClODERXd5QSZJyOyH9nOz60SA==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.13.tgz", + "integrity": "sha512-pmtIjU02ASJOZKdL8DoxWXJgZnpTDgD5WmMnjKJh9FaWmc2YiCW2Y6VRxPox96OM655jYHQe5+UIbk3Cwtwb4A==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "9.1.6", + "@storybook/csf-plugin": "9.1.13", "ts-dedent": "^2.0.0" }, "funding": { @@ -3209,14 +3162,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.1.6", + "storybook": "^9.1.13", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.6.tgz", - "integrity": "sha512-cz4Y+OYCtuovFNwoLkIKk0T62clrRTYf26Bbo1gdIGuX/W3JPP/LnN97sP2/0nfF6heZqCdEwb47k7RubkxXZg==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.13.tgz", + "integrity": "sha512-EMpzYuyt9FDcxxfBChWzfId50y8QMpdenviEQ8m+pa6c+ANx3pC5J6t7y0khD8TQu815sTy+nc6cc8PC45dPUA==", "dev": true, "license": "MIT", "dependencies": { @@ -3227,7 +3180,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.1.6" + "storybook": "^9.1.13" } }, "node_modules/@storybook/global": { @@ -3252,17 +3205,17 @@ } }, "node_modules/@storybook/nextjs-vite": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@storybook/nextjs-vite/-/nextjs-vite-9.1.6.tgz", - "integrity": "sha512-BdzgPiuOKTl5q+NgCJjpwuFYL/me0u69zpZ7w4EhVLs4pxyJtUO7JRW8OW5VyBp3/M2ryoqWU0dPAfQ4WzrHAA==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/nextjs-vite/-/nextjs-vite-9.1.13.tgz", + "integrity": "sha512-iUQbfAndUag5ehPPldvVCM8T4GylOEZqf13EEHvjgh8F33vOiANq7WTGbvO+aBpBNug3x+1pG1hmKXRKVmA6xQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-vite": "9.1.6", - "@storybook/react": "9.1.6", - "@storybook/react-vite": "9.1.6", + "@storybook/builder-vite": "9.1.13", + "@storybook/react": "9.1.13", + "@storybook/react-vite": "9.1.13", "styled-jsx": "5.1.6", - "vite-plugin-storybook-nextjs": "^2.0.5" + "vite-plugin-storybook-nextjs": "^2.0.7" }, "engines": { "node": ">=20.0.0" @@ -3275,7 +3228,7 @@ "next": "^14.1.0 || ^15.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.6", + "storybook": "^9.1.13", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "peerDependenciesMeta": { @@ -3285,14 +3238,14 @@ } }, "node_modules/@storybook/react": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.6.tgz", - "integrity": "sha512-BGf3MQaXj6LmYnYpSwHUoWH0RP6kaqBoPc2u5opSU2ajw34enIL5w2sFaXzL+k2ap0aHnCYYlyBINBBvtD6NIA==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.13.tgz", + "integrity": "sha512-B0UpYikKf29t8QGcdmumWojSQQ0phSDy/Ne2HYdrpNIxnUvHHUVOlGpq4lFcIDt52Ip5YG5GuAwJg3+eR4LCRg==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "9.1.6" + "@storybook/react-dom-shim": "9.1.13" }, "engines": { "node": ">=20.0.0" @@ -3304,7 +3257,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.6", + "storybook": "^9.1.13", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -3314,9 +3267,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.6.tgz", - "integrity": "sha512-Px4duzPMTPqI3kes6eUyYjWpEeJ0AOCCeSDCBDm9rzlf4a+eXlxfhkcVWft3viCDiIkc0vtYagb2Yu7bcSIypg==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.13.tgz", + "integrity": "sha512-/tMr9TmV3+98GEQO0S03k4gtKHGCpv9+k9Dmnv+TJK3TBz7QsaFEzMwe3gCgoTaebLACyVveDiZkWnCYAWB6NA==", "dev": true, "license": "MIT", "funding": { @@ -3326,20 +3279,20 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.6" + "storybook": "^9.1.13" } }, "node_modules/@storybook/react-vite": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.6.tgz", - "integrity": "sha512-YNKQZcz5Vtv8OdHUJ65Wx4PbfZMrPPbtL+OYAR0We+EEoTDofi3VogXyOUw99Jppp1HIq5IiDF5qyZPEpC5k0A==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.13.tgz", + "integrity": "sha512-mV1bZ1bpkNQygnuDo1xMGAS5ZXuoXFF0WGmr/BzNDGmRhZ1K1HQh42kC0w3PklckFBUwCFxmP58ZwTFzf+/dJA==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "9.1.6", - "@storybook/react": "9.1.6", + "@storybook/builder-vite": "9.1.13", + "@storybook/react": "9.1.13", "find-up": "^7.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", @@ -3356,23 +3309,10 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.6", + "storybook": "^9.1.13", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@storybook/react-vite/node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@storybook/react-vite/node_modules/find-up": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", @@ -3462,28 +3402,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/@storybook/react-vite/node_modules/react-docgen": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.1.tgz", - "integrity": "sha512-kQKsqPLplY3Hx4jGnM3jpQcG3FQDt7ySz32uTHt3C9HAe45kNXG+3o16Eqn3Fw1GtMfHoN3b4J/z2e6cZJCmqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.2", - "@types/babel__core": "^7.20.5", - "@types/babel__traverse": "^7.20.7", - "@types/doctrine": "^0.0.9", - "@types/resolve": "^1.20.2", - "doctrine": "^3.0.0", - "resolve": "^1.22.1", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": "^20.9.0 || >=22" - } - }, "node_modules/@storybook/react-vite/node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -6856,9 +6774,9 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.6.tgz", - "integrity": "sha512-4NLf8lOT7Nl+m9aipVHJczyt/Dp6BzHzyNq4nhaEUjoZFGKMhPa52vSbuLyQYX7IrcrYPlM37X8dFGo/EIE9JA==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.13.tgz", + "integrity": "sha512-kPuhbtGDiJLB5OLZuwFZAxgzWakNDw64sJtXUPN8g0+VAeXfHyZEmsE28qIIETHxtal71lPKVm8QNnERaJHPJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6869,7 +6787,7 @@ }, "peerDependencies": { "eslint": ">=8", - "storybook": "^9.1.6" + "storybook": "^9.1.13" } }, "node_modules/eslint-scope": { @@ -7357,6 +7275,27 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7378,6 +7317,32 @@ "license": "BSD-2-Clause", "peer": true }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -9696,6 +9661,28 @@ "react": ">=0.14.0" } }, + "node_modules/react-docgen": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.2.tgz", + "integrity": "sha512-+NRMYs2DyTP4/tqWz371Oo50JqmWltR1h2gcdgUMAWZJIAvrd0/SqlCfx7tpzpl/s36rzw6qH2MjoNrxtRNYhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@types/babel__core": "^7.20.5", + "@types/babel__traverse": "^7.20.7", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.9.0 || >=22" + } + }, "node_modules/react-docgen-typescript": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", @@ -9706,6 +9693,19 @@ "typescript": ">= 4.3.x" } }, + "node_modules/react-docgen/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -10598,9 +10598,9 @@ } }, "node_modules/storybook": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.6.tgz", - "integrity": "sha512-iIcMaDKkjR5nN+JYBy9hhoxZhjX4TXhyJgUBed+toJOlfrl+QvxpBjImAi7qKyLR3hng3uoigEP0P8+vYtXpOg==", + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.13.tgz", + "integrity": "sha512-G3KZ36EVzXyHds72B/qtWiJnhUpM0xOUeYlDcO9DSHL1bDTv15cW4+upBl+mcBZrDvU838cn7Bv4GpF+O5MCfw==", "dev": true, "license": "MIT", "dependencies": { @@ -10854,9 +10854,9 @@ } }, "node_modules/strip-indent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.0.tgz", - "integrity": "sha512-OA95x+JPmL7kc7zCu+e+TeYxEiaIyndRx0OrBcK2QPPH09oAndr2ALvymxWA+Lx1PYYvFUm4O63pRkdJAaW96w==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", "dev": true, "license": "MIT", "engines": { @@ -11086,27 +11086,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/test-exclude/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", diff --git a/package.json b/package.json index 9dd3064..899d30e 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,11 @@ "devDependencies": { "@chromatic-com/storybook": "^4.1.1", "@eslint/eslintrc": "^3", - "@storybook/addon-a11y": "^9.1.6", - "@storybook/addon-docs": "^9.1.6", + "@storybook/addon-a11y": "^9.1.13", + "@storybook/addon-docs": "^9.1.13", "@storybook/addon-styling-webpack": "^2.0.0", - "@storybook/addon-vitest": "^9.1.6", - "@storybook/nextjs-vite": "^9.1.6", + "@storybook/addon-vitest": "^9.1.13", + "@storybook/nextjs-vite": "^9.1.13", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", @@ -63,9 +63,9 @@ "@vitest/coverage-v8": "^3.2.4", "eslint": "^9", "eslint-config-next": "15.5.3", - "eslint-plugin-storybook": "^9.1.6", + "eslint-plugin-storybook": "^9.1.13", "playwright": "^1.55.0", - "storybook": "^9.1.6", + "storybook": "^9.1.13", "tailwindcss": "^4", "tw-animate-css": "^1.3.8", "typescript": "^5", diff --git a/public/funnels/soulmate-small.json b/public/funnels/soulmate-small.json new file mode 100644 index 0000000..49782d1 --- /dev/null +++ b/public/funnels/soulmate-small.json @@ -0,0 +1,841 @@ +{ + "meta": { + "id": "soulmate-small", + "title": "Soulmate V1 (копия)", + "description": "Soulmate", + "firstScreenId": "onboarding", + "googleAnalyticsId": "", + "yandexMetrikaId": "" + }, + "defaultTexts": { + "nextButton": "Next", + "privacyBanner": "We don’t share personal information — it stays safe and under your control." + }, + "screens": [ + { + "id": "onboarding", + "template": "soulmate", + "header": { + "showBackButton": false, + "showProgress": true, + "show": false + }, + "title": { + "text": "Soulmate Portrait", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "Continue", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": true + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "gender", + "isEndScreen": false + }, + "description": { + "text": "Ready to see who your true soulmate is?", + "font": "manrope", + "weight": "regular", + "size": "md", + "align": "center", + "color": "default" + }, + "variants": [], + "soulmatePortraitsDelivered": { + "image": "/soulmate-portrait-delivered-male.jpg", + "text": { + "text": "soulmate portraits delivered today", + "font": "inter", + "weight": "medium", + "size": "sm", + "color": "primary" + }, + "avatars": [ + { + "src": "/avatars/male-1.jpg", + "alt": "Male 1" + }, + { + "src": "/avatars/male-2.jpg", + "alt": "Male 2" + }, + { + "src": "/avatars/male-3.jpg", + "alt": "Male 3" + }, + { + "src": "", + "fallbackText": "900+" + } + ] + }, + "textList": { + "items": [ + { + "text": "Just 2 minutes — and the Portrait will reveal the one who’s destined to be with you." + }, + { + "text": "Astonishing 99% accuracy." + }, + { + "text": "An unexpected revelation awaits you." + }, + { + "text": "All that’s left is to dare to look." + } + ] + } + }, + { + "id": "gender", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "What’s your gender?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "It all starts with you! Choose your gender.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "partner-gender", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "female", + "label": "FEMALE", + "emoji": "🩷", + "disabled": false + }, + { + "id": "male", + "label": "MALE", + "emoji": "💙", + "disabled": false + } + ], + "registrationFieldKey": "profile.gender" + }, + "variants": [] + }, + { + "id": "partner-gender", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Who are you interested in?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "email", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "male", + "label": "Male", + "disabled": false + }, + { + "id": "female", + "label": "Female", + "disabled": false + } + ], + "registrationFieldKey": "partner.gender" + }, + "variants": [] + }, + { + "id": "email", + "template": "email", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Your soulmate’s portrait is ready! Where should we send it?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "Continue", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": true + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "trial-choice", + "isEndScreen": false + }, + "emailInput": { + "label": "Email", + "placeholder": "example@email.com" + }, + "image": { + "src": "/female-portrait.jpg" + }, + "variants": [ + { + "conditions": [ + { + "screenId": "partner-gender", + "operator": "includesAny", + "optionIds": [ + "male" + ] + } + ], + "overrides": { + "image": { + "src": "/male-portrait.jpg" + } + } + } + ] + }, + { + "id": "trial-choice", + "template": "trialChoice", + "header": { + "showBackButton": true, + "showProgress": false, + "show": true + }, + "title": { + "text": "Trial Choice", + "show": false, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "Next", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "payment", + "isEndScreen": false + }, + "variants": [] + }, + { + "id": "payment", + "template": "trialPayment", + "header": { + "showBackButton": true, + "showProgress": true, + "show": false + }, + "title": { + "text": "Payment", + "show": false, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "specialoffer", + "isEndScreen": true, + "onBackScreenId": "screen-7" + }, + "variants": [ + { + "conditions": [ + { + "screenId": "partner-gender", + "operator": "includesAny", + "optionIds": [ + "male" + ] + } + ], + "overrides": { + "unlockYourSketch": { + "image": { + "src": "/trial-payment/portrait-male.jpg" + } + } + } + } + ], + "headerBlock": { + "text": { + "text": "⚠️ Your sketch expires soon!" + }, + "timer": { + "text": "" + }, + "timerSeconds": 600 + }, + "unlockYourSketch": { + "title": { + "text": "Unlock Your Sketch" + }, + "subtitle": { + "text": "Just One Click to Reveal Your Match!" + }, + "image": { + "src": "/trial-payment/portrait-female.jpg" + }, + "blur": { + "text": { + "text": "Unlock to reveal your personalized portrait" + }, + "icon": "lock" + }, + "buttonText": "Get My Soulmate Sketch" + }, + "joinedToday": { + "count": { + "text": "954" + }, + "text": { + "text": "Joined today" + } + }, + "trustedByOver": { + "text": { + "text": "Trusted by over **355,000** people." + } + }, + "findingOneGuide": { + "header": { + "emoji": { + "text": "❤️" + }, + "title": { + "text": "Finding the One Guide" + } + }, + "text": { + "text": "You’re special — you have imagination and energy that not everyone can understand. You deserve more than to be “half-understood” — you deserve to meet someone who sees the world as deeply and vividly as you do. This guide will help you recognize the signs of destiny, trust your path, and find the one whose heart is already connected to yours.\nThey may already be closer than you think — drawn to your light, searching for the same rare connection. Stay open, follow your intuition, and notice the quiet moments that feel like fate. Every step you take brings you nearer to the soul that was always meant to walk beside you." + }, + "blur": { + "text": { + "text": "Full access is required to unlock the complete report." + }, + "icon": "lock" + } + }, + "tryForDays": { + "title": { + "text": "Try it for {{trialPeriod}}!" + }, + "textList": { + "items": [ + { + "text": "Receive a hand-drawn sketch of your soulmate." + }, + { + "text": "Reveal the path with the guide." + }, + { + "text": "Talk to live experts and get guidance." + }, + { + "text": "Start your {{trialPeriod}} trial for just {{trialPrice}}." + }, + { + "text": "Cancel anytime—just 24 hours before renewal." + } + ] + } + }, + "totalPrice": { + "couponContainer": { + "title": { + "text": "Coupon\nCode" + }, + "buttonText": "SOULMATE94" + }, + "priceContainer": { + "title": { + "text": "Total" + }, + "price": { + "text": "{{trialPrice}}" + }, + "oldPrice": { + "text": "{{oldPrice}}" + }, + "discount": { + "text": "{{discountPercent}}% discount applied" + } + } + }, + "paymentButtons": { + "buttons": [ + { + "text": "Pay", + "icon": "pay" + }, + { + "text": "Pay", + "icon": "google" + }, + { + "text": "Credit or debit card", + "icon": "card", + "primary": true + } + ] + }, + "moneyBackGuarantee": { + "title": { + "text": "30-DAY MONEY-BACK GUARANTEE" + }, + "text": { + "text": "If you don't receive your soulmate sketch, we'll refund your money!" + } + }, + "policy": { + "text": { + "text": "By clicking Continue, you agree to our Terms of Use & Service and Privacy Policy. You also acknowledge that your 1 week introductory plan to Respontika, billed at $1.00, will automatically renew at $14.50 every 1 week unless canceled before the end of the trial period." + } + }, + "usersPortraits": { + "title": { + "text": "Our Users' Soulmate Portraits" + }, + "images": [ + { + "src": "/trial-payment/users-portraits/1.jpg" + }, + { + "src": "/trial-payment/users-portraits/2.jpg" + }, + { + "src": "/trial-payment/users-portraits/3.jpg" + }, + { + "src": "/trial-payment/users-portraits/4.jpg" + }, + { + "src": "/trial-payment/users-portraits/5.jpg" + }, + { + "src": "/trial-payment/users-portraits/6.jpg" + } + ], + "buttonText": "Get My Soulmate Sketch" + }, + "joinedTodayWithAvatars": { + "count": { + "text": "954" + }, + "text": { + "text": "people joined today" + }, + "avatars": { + "images": [ + { + "src": "/trial-payment/avatars/1.jpg" + }, + { + "src": "/trial-payment/avatars/2.jpg" + }, + { + "src": "/trial-payment/avatars/3.jpg" + }, + { + "src": "/trial-payment/avatars/4.jpg" + }, + { + "src": "/trial-payment/avatars/5.jpg" + } + ] + } + }, + "progressToSeeSoulmate": { + "title": { + "text": "See Your Soulmate – Just One Step Away" + }, + "progress": { + "value": 92 + }, + "leftText": { + "text": "Step 2 of 5" + }, + "rightText": { + "text": "99% Complete" + } + }, + "stepsToSeeSoulmate": { + "steps": [ + { + "title": { + "text": "Questions Answered" + }, + "description": { + "text": "You've provided all the necessary information." + }, + "icon": "questions", + "isActive": true + }, + { + "title": { + "text": "Profile Analysis" + }, + "description": { + "text": "Creating your perfect soulmate profile." + }, + "icon": "profile", + "isActive": true + }, + { + "title": { + "text": "Sketch Creation" + }, + "description": { + "text": "Your personalized soulmate sketch will be created." + }, + "icon": "sketch" + }, + { + "title": { + "text": "Astrological Insights" + }, + "description": { + "text": "Unique astrology-based recommendations." + }, + "icon": "astro" + }, + { + "title": { + "text": "Personalized chat with an expert" + }, + "description": { + "text": "Individual guidance." + }, + "icon": "chat" + } + ], + "buttonText": "Show Me My Soulmate" + }, + "reviews": { + "title": { + "text": "Loved and Trusted Worldwide" + }, + "items": [ + { + "name": { + "text": "Jennifer Wilson 🇺🇸" + }, + "text": { + "text": "**“I saw my mistakes… and found my husband.”**\nThe portrait instantly struck me — I had this feeling like I’d seen him somewhere before. But the real turning point came after the guide: I finally understood why I kept choosing the “wrong” people. And the most amazing part — soon after, I met a man who turned out to be the exact image from that portrait. He’s my husband now, and when we compared the drawing to his photo, the resemblance was just wow." + }, + "avatar": { + "src": "/trial-payment/reviews/avatars/1.jpg" + }, + "portrait": { + "src": "/trial-payment/reviews/portraits/1.jpg" + }, + "photo": { + "src": "/trial-payment/reviews/photos/1.jpg" + }, + "rating": 5, + "date": { + "text": "1 day ago" + } + }, + { + "name": { + "text": "Amanda Davis 🇨🇦" + }, + "text": { + "text": "**“I understood my partner better in one evening than in several years.”**\nI took the test just for fun — the portrait surprised us. But the real breakthrough came when I read the guide about my other half. It had spot-on insights about how we can support each other. The price was nothing, but the value was huge — now we have fewer misunderstandings and so much more warmth." + }, + "avatar": { + "src": "/trial-payment/reviews/avatars/2.jpg" + }, + "portrait": { + "src": "/trial-payment/reviews/portraits/2.jpg" + }, + "photo": { + "src": "/trial-payment/reviews/photos/2.jpg" + }, + "rating": 5, + "date": { + "text": "4 days ago" + } + }, + { + "name": { + "text": "Michael Johnson 🇬🇧" + }, + "text": { + "text": "**“I saw her face — and got goosebumps.”**\nWhen I got my test results and saw the portrait, I literally froze. It was the exact girl I’d started dating a couple of weeks earlier. And the guide described perfectly why we’re drawn to each other. Honestly, I didn’t expect such a match." + }, + "avatar": { + "src": "/trial-payment/reviews/avatars/3.jpg" + }, + "portrait": { + "src": "/trial-payment/reviews/portraits/3.jpg" + }, + "photo": { + "src": "/trial-payment/reviews/photos/3.jpg" + }, + "rating": 5, + "date": { + "text": "1 week ago" + } + } + ] + }, + "stillHaveQuestions": { + "title": { + "text": "Still have questions? We're here to help!" + }, + "actionButtonText": "Get My Soulmate Sketch", + "contactButtonText": "Contact Support" + }, + "commonQuestions": { + "title": { + "text": "Common Questions" + }, + "items": [ + { + "question": "When will I receive my sketch?", + "answer": "Your personalized soulmate sketch will be delivered within 24-48 hours after completing your order. You'll receive an email notification when it's ready for viewing in your account." + }, + { + "question": "How do I cancel my subscription?", + "answer": "You can cancel anytime from your account settings. Make sure to cancel at least 24 hours before the renewal date to avoid being charged." + }, + { + "question": "How accurate are the readings?", + "answer": "Our readings are based on a combination of your answers and advanced pattern analysis. While they provide valuable insights, they are intended for guidance and entertainment purposes." + }, + { + "question": "Is my data secure and private?", + "answer": "Yes. We follow strict data protection standards. Your data is encrypted and never shared with third parties without your consent." + } + ] + }, + "footer": { + "title": { + "text": "WIT LAB ©" + }, + "contacts": { + "title": { + "text": "CONTACTS" + }, + "email": { + "href": "info@witlab.us", + "text": "info@witlab.us" + }, + "address": { + "text": "Wit Lab 2108 N ST STE N SACRAMENTO, CA95816, US" + } + }, + "legal": { + "title": { + "text": "LEGAL" + }, + "links": [ + { + "href": "https://witlab.com/terms", + "text": "Terms of Service" + }, + { + "href": "https://witlab.com/privacy", + "text": "Privacy Policy" + }, + { + "href": "https://witlab.com/refund", + "text": "Refund Policy" + } + ], + "copyright": { + "text": "Copyright © 2025 Wit Lab™. All rights reserved. All trademarks referenced herein are the properties of their respective owners." + } + }, + "paymentMethods": { + "title": { + "text": "PAYMENT METHODS" + }, + "methods": [ + { + "src": "/trial-payment/payment-methods/visa.svg", + "alt": "visa" + }, + { + "src": "/trial-payment/payment-methods/mastercard.svg", + "alt": "mastercard" + }, + { + "src": "/trial-payment/payment-methods/discover.svg", + "alt": "discover" + }, + { + "src": "/trial-payment/payment-methods/apple.svg", + "alt": "apple" + }, + { + "src": "/trial-payment/payment-methods/google.svg", + "alt": "google" + }, + { + "src": "/trial-payment/payment-methods/paypal.svg", + "alt": "paypal" + } + ] + } + } + }, + { + "id": "screen-7", + "template": "specialOffer", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Special Offer", + "show": false, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "Добавьте детали справа", + "show": false, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "GET {{trialPeriod}} TRIAL", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "isEndScreen": true + }, + "variants": [], + "text": { + "title": { + "text": "Special Offer" + }, + "subtitle": { + "text": "SAVE {{discountPercent}}% OFF!" + }, + "description": { + "trialPrice": { + "text": "{{trialPrice}}" + }, + "text": { + "text": " instead of " + }, + "oldTrialPrice": { + "text": "~~{{oldTrialPrice}}~~" + } + } + }, + "advantages": { + "items": [ + { + "icon": { + "text": "🔥" + }, + "text": { + "text": " **{{trialPeriodHyphen}}** trial instead of ~~{{oldTrialPeriod}}~~" + } + }, + { + "icon": { + "text": "💝" + }, + "text": { + "text": " Get **{{discountPercent}}%** off your Soulmate Sketch" + } + }, + { + "icon": { + "text": "💌" + }, + "text": { + "text": " Includes the **“Finding the One”** Guide" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/public/funnels/soulmate.json b/public/funnels/soulmate.json index 3c9507d..7849da2 100644 --- a/public/funnels/soulmate.json +++ b/public/funnels/soulmate.json @@ -17,6 +17,7 @@ "template": "soulmate", "header": { "showBackButton": false, + "showProgress": true, "show": false }, "title": { @@ -153,6 +154,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -208,6 +210,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -252,6 +255,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -314,6 +318,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -376,6 +381,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -511,6 +517,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -568,6 +575,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -641,6 +649,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -709,6 +718,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -766,6 +776,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -824,6 +835,7 @@ "template": "info", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -944,6 +956,7 @@ "template": "date", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -998,6 +1011,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1055,6 +1069,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1112,6 +1127,7 @@ "template": "info", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1395,6 +1411,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1516,6 +1533,7 @@ "template": "info", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1693,6 +1711,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1751,6 +1770,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1794,6 +1814,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1857,6 +1878,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1915,6 +1937,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -1978,6 +2001,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -2036,6 +2060,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -2094,6 +2119,7 @@ "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -2176,6 +2202,7 @@ "template": "loaders", "header": { "showBackButton": false, + "showProgress": true, "show": false }, "title": { @@ -2228,6 +2255,7 @@ "template": "email", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -2246,7 +2274,40 @@ "showPrivacyTermsConsent": true }, "navigation": { - "rules": [], + "rules": [ + { + "conditions": [ + { + "screenId": "email", + "conditionType": "unleash", + "operator": "includesAny", + "optionIds": [], + "values": [], + "unleashFlag": "soulmate-trial-grid", + "unleashVariants": [ + "coupon" + ] + } + ], + "nextScreenId": "coupon" + }, + { + "conditions": [ + { + "screenId": "email", + "conditionType": "unleash", + "operator": "includesAny", + "optionIds": [], + "values": [], + "unleashFlag": "soulmate-trial-grid", + "unleashVariants": [ + "grid" + ] + } + ], + "nextScreenId": "trial-choice" + } + ], "defaultNextScreenId": "coupon", "isEndScreen": false }, @@ -2281,6 +2342,7 @@ "template": "coupon", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -2358,15 +2420,46 @@ "copiedMessage": "Promo code copied!", "variants": [] }, + { + "id": "trial-choice", + "template": "trialChoice", + "header": { + "showBackButton": true, + "showProgress": false, + "show": true + }, + "title": { + "text": "Trial Choice", + "show": false, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "Next", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "payment", + "isEndScreen": false + }, + "variants": [] + }, { "id": "payment", "template": "trialPayment", "header": { "showBackButton": true, + "showProgress": true, "show": false }, "title": { - "text": "Title", + "text": "Payment", "show": false, "font": "manrope", "weight": "bold", @@ -2382,7 +2475,8 @@ "navigation": { "rules": [], "defaultNextScreenId": "specialoffer", - "isEndScreen": false + "isEndScreen": true, + "onBackScreenId": "specialoffer" }, "variants": [ { @@ -2465,7 +2559,7 @@ }, "tryForDays": { "title": { - "text": "Try it for 7 days!" + "text": "Try it for {{trialPeriod}}!" }, "textList": { "items": [ @@ -2479,7 +2573,7 @@ "text": "Talk to live experts and get guidance." }, { - "text": "Start your 7-day trial for just $1.00." + "text": "Start your {{trialPeriodHyphen}} trial for just {{trialPrice}}." }, { "text": "Cancel anytime—just 24 hours before renewal." @@ -2499,13 +2593,13 @@ "text": "Total" }, "price": { - "text": "$1.00" + "text": "{{trialPrice}}" }, "oldPrice": { - "text": "$14.99" + "text": "{{billingPrice}}" }, "discount": { - "text": "94% discount applied" + "text": "{{discountPercent}}% discount applied" } } }, @@ -2536,7 +2630,7 @@ }, "policy": { "text": { - "text": "By clicking Continue, you agree to our Terms of Use & Service and Privacy Policy. You also acknowledge that your 1 week introductory plan to Respontika, billed at $1.00, will automatically renew at $14.50 every 1 week unless canceled before the end of the trial period." + "text": "By clicking Continue, you agree to our Terms of Use & Service and Privacy Policy. You also acknowledge that your 1 week introductory plan to WitLab, billed at $1.00, will automatically renew at $14.50 every 1 week unless canceled before the end of the trial period." } }, "usersPortraits": { @@ -2834,6 +2928,7 @@ "template": "specialOffer", "header": { "showBackButton": false, + "showProgress": true, "show": true }, "title": { @@ -2851,6 +2946,10 @@ "cornerRadius": "3xl", "showPrivacyTermsConsent": false }, + "navigation": { + "rules": [], + "isEndScreen": true + }, "variants": [], "text": { "title": { diff --git a/src/app/[funnelId]/layout.tsx b/src/app/[funnelId]/layout.tsx index ccf9f20..e01bf34 100644 --- a/src/app/[funnelId]/layout.tsx +++ b/src/app/[funnelId]/layout.tsx @@ -5,6 +5,10 @@ import { UnleashProvider } from "@/lib/funnel/unleash"; import type { FunnelDefinition } from "@/lib/funnel/types"; import { BAKED_FUNNELS } from "@/lib/funnel/bakedFunnels"; import { IS_FULL_SYSTEM_BUILD } from "@/lib/runtime/buildVariant"; +import { + PaymentPlacementProvider, + TrialVariantSelectionProvider, +} from "@/entities/session/payment"; // Функция для загрузки воронки из базы данных async function loadFunnelFromDatabase( @@ -73,7 +77,11 @@ export default async function FunnelLayout({ googleAnalyticsId={funnel.meta.googleAnalyticsId} yandexMetrikaId={funnel.meta.yandexMetrikaId} > - {children} + + + {children} + + ); diff --git a/src/components/admin/builder/Canvas/BuilderCanvas.tsx b/src/components/admin/builder/Canvas/BuilderCanvas.tsx index 4a43670..40c59b6 100644 --- a/src/components/admin/builder/Canvas/BuilderCanvas.tsx +++ b/src/components/admin/builder/Canvas/BuilderCanvas.tsx @@ -11,6 +11,7 @@ import type { } from "@/lib/funnel/types"; import { cn } from "@/lib/utils"; import { DropIndicator } from "./DropIndicator"; +import { InsertScreenButton } from "./InsertScreenButton"; import { TransitionRow } from "./TransitionRow"; import { TemplateSummary } from "./TemplateSummary"; import { VariantSummary } from "./VariantSummary"; @@ -24,6 +25,8 @@ export function BuilderCanvas() { const dragStateRef = useRef<{ screenId: string; dragStartIndex: number } | null>(null); const [dropIndex, setDropIndex] = useState(null); const [addScreenDialogOpen, setAddScreenDialogOpen] = useState(false); + const [insertScreenDialogOpen, setInsertScreenDialogOpen] = useState(false); + const [insertAtIndex, setInsertAtIndex] = useState(null); const handleDragStart = useCallback((event: React.DragEvent, screenId: string, index: number) => { event.dataTransfer.effectAllowed = "move"; @@ -115,6 +118,17 @@ export function BuilderCanvas() { dispatch({ type: "add-screen", payload: { template } }); }, [dispatch]); + const handleInsertScreen = useCallback((atIndex: number) => { + setInsertAtIndex(atIndex); + setInsertScreenDialogOpen(true); + }, []); + + const handleInsertScreenWithTemplate = useCallback((template: ScreenDefinition["template"]) => { + if (insertAtIndex !== null) { + dispatch({ type: "insert-screen", payload: { template, atIndex: insertAtIndex } }); + } + }, [dispatch, insertAtIndex]); + const screenTitleMap = useMemo(() => { return screens.reduce>((accumulator, screen) => { accumulator[screen.id] = screen.title?.text || screen.id; @@ -159,10 +173,15 @@ export function BuilderCanvas() { const defaultTargetIndex = defaultNext ? screens.findIndex((candidate) => candidate.id === defaultNext) : null; + const onBackScreenId = screen.navigation?.onBackScreenId; + const backTargetIndex = onBackScreenId + ? screens.findIndex((candidate) => candidate.id === onBackScreenId) + : null; return (
{isDropBefore && } +
+ {onBackScreenId && ( + + )} +
+ + {/* Insert button after each screen */} + handleInsertScreen(index + 1)} /> + {isDropAfter && }
); @@ -301,6 +333,12 @@ export function BuilderCanvas() { onOpenChange={setAddScreenDialogOpen} onAddScreen={handleAddScreenWithTemplate} /> + + ); } diff --git a/src/components/admin/builder/Canvas/InsertScreenButton.tsx b/src/components/admin/builder/Canvas/InsertScreenButton.tsx new file mode 100644 index 0000000..0c29b0f --- /dev/null +++ b/src/components/admin/builder/Canvas/InsertScreenButton.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { Plus } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useState } from "react"; + +interface InsertScreenButtonProps { + onInsert: () => void; +} + +export function InsertScreenButton({ onInsert }: InsertScreenButtonProps) { + const [isHovered, setIsHovered] = useState(false); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* Hover area - wider for easier interaction */} +
+ + {/* Divider line */} +
+ + {/* Insert button */} + +
+ ); +} diff --git a/src/components/admin/builder/Canvas/TransitionRow.tsx b/src/components/admin/builder/Canvas/TransitionRow.tsx index 98c43fb..13a7d14 100644 --- a/src/components/admin/builder/Canvas/TransitionRow.tsx +++ b/src/components/admin/builder/Canvas/TransitionRow.tsx @@ -1,8 +1,8 @@ -import { ArrowDown, ArrowRight, CircleSlash2, GitBranch } from "lucide-react"; +import { ArrowDown, ArrowRight, ArrowLeft, CircleSlash2, GitBranch } from "lucide-react"; import { cn } from "@/lib/utils"; export interface TransitionRowProps { - type: "default" | "branch" | "end"; + type: "default" | "branch" | "end" | "back"; label: string; targetLabel?: string; targetIndex?: number | null; @@ -18,7 +18,7 @@ export function TransitionRow({ optionSummaries = [], operator, }: TransitionRowProps) { - const Icon = type === "branch" ? GitBranch : type === "end" ? CircleSlash2 : ArrowDown; + const Icon = type === "branch" ? GitBranch : type === "end" ? CircleSlash2 : type === "back" ? ArrowLeft : ArrowDown; return (
@@ -42,7 +48,11 @@ export function TransitionRow({ {label} diff --git a/src/components/admin/builder/Canvas/constants.ts b/src/components/admin/builder/Canvas/constants.ts index 3b68fae..59134a7 100644 --- a/src/components/admin/builder/Canvas/constants.ts +++ b/src/components/admin/builder/Canvas/constants.ts @@ -13,6 +13,7 @@ export const TEMPLATE_TITLES: Record = { loaders: "Загрузка", soulmate: "Портрет партнера", trialPayment: "Trial Payment", + trialChoice: "Trial Choice", specialOffer: "Special Offer", }; diff --git a/src/components/admin/builder/Canvas/index.ts b/src/components/admin/builder/Canvas/index.ts index 8f409a3..fb39e74 100644 --- a/src/components/admin/builder/Canvas/index.ts +++ b/src/components/admin/builder/Canvas/index.ts @@ -3,6 +3,7 @@ export { BuilderCanvas } from "./BuilderCanvas"; // Sub-components export { DropIndicator } from "./DropIndicator"; +export { InsertScreenButton } from "./InsertScreenButton"; export { TransitionRow } from "./TransitionRow"; export { TemplateSummary } from "./TemplateSummary"; export { VariantSummary } from "./VariantSummary"; diff --git a/src/components/admin/builder/dialogs/AddScreenDialog.tsx b/src/components/admin/builder/dialogs/AddScreenDialog.tsx index d63f5bd..59c2778 100644 --- a/src/components/admin/builder/dialogs/AddScreenDialog.tsx +++ b/src/components/admin/builder/dialogs/AddScreenDialog.tsx @@ -12,6 +12,7 @@ import { Mail, CreditCard, Gift, + Layers, } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -99,6 +100,14 @@ const TEMPLATE_OPTIONS = [ color: "bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400", }, + { + template: "trialChoice" as const, + title: "Trial Choice", + description: "Экран выбора вариантов пробного периода", + icon: Layers, + color: + "bg-lime-50 text-lime-700 dark:bg-lime-900/20 dark:text-lime-400", + }, { template: "specialOffer" as const, title: "Special Offer", diff --git a/src/components/admin/builder/layout/BuilderPreview.tsx b/src/components/admin/builder/layout/BuilderPreview.tsx index a5fbb7e..6ed2e9e 100644 --- a/src/components/admin/builder/layout/BuilderPreview.tsx +++ b/src/components/admin/builder/layout/BuilderPreview.tsx @@ -10,6 +10,10 @@ import { renderScreen } from "@/lib/funnel/screenRenderer"; import { mergeScreenWithOverrides } from "@/lib/admin/builder/variants"; import { PreviewErrorBoundary } from "@/components/admin/ErrorBoundary"; import { PREVIEW_DIMENSIONS } from "@/lib/constants"; +import { + PaymentPlacementProvider, + TrialVariantSelectionProvider, +} from "@/entities/session/payment"; // ✅ Мемоизированные моки - создаются один раз const MOCK_CALLBACKS = { @@ -196,9 +200,13 @@ export function BuilderPreview() { > {/* Screen Content with scroll - wrapped in Error Boundary */} -
- {renderScreenPreview()} -
+ + +
+ {renderScreenPreview()} +
+
+
diff --git a/src/components/admin/builder/templates/TemplateConfig.tsx b/src/components/admin/builder/templates/TemplateConfig.tsx index ce762ad..4ccd54f 100644 --- a/src/components/admin/builder/templates/TemplateConfig.tsx +++ b/src/components/admin/builder/templates/TemplateConfig.tsx @@ -294,18 +294,16 @@ interface HeaderControlsProps { } function HeaderControls({ header, onChange }: HeaderControlsProps) { - const activeHeader = header ?? { show: true, showBackButton: true }; - - const handleToggle = (field: "show" | "showBackButton", checked: boolean) => { - if (field === "show" && !checked) { - onChange({ - ...activeHeader, - show: false, - showBackButton: false, - }); - return; - } + const activeHeader = header ?? { + show: true, + showBackButton: true, + showProgress: true + }; + const handleToggle = ( + field: "show" | "showBackButton" | "showProgress", + checked: boolean + ) => { onChange({ ...activeHeader, [field]: checked, @@ -320,11 +318,22 @@ function HeaderControls({ header, onChange }: HeaderControlsProps) { checked={activeHeader.show !== false} onChange={(event) => handleToggle("show", event.target.checked)} /> - Показывать шапку с прогрессом + Показывать шапку экрана {activeHeader.show !== false && ( -
+
+ +
)} @@ -631,15 +723,21 @@ export function TrialPaymentTemplate({ {screen.usersPortraits && ( ({ src: img.src, alt: "user portrait", @@ -669,14 +767,26 @@ export function TrialPaymentTemplate({ } : undefined } - count={buildTypographyProps(screen.joinedTodayWithAvatars.count, { - as: "span", - defaults: { font: "inter", weight: "bold", size: "sm" }, - })} - text={buildTypographyProps(screen.joinedTodayWithAvatars.text, { - as: "p", - defaults: { font: "inter", weight: "semiBold", size: "sm" }, - })} + count={buildTypographyProps( + { + ...screen.joinedTodayWithAvatars.count, + text: replacePlaceholders(screen.joinedTodayWithAvatars.count?.text), + }, + { + as: "span", + defaults: { font: "inter", weight: "bold", size: "sm" }, + } + )} + text={buildTypographyProps( + { + ...screen.joinedTodayWithAvatars.text, + text: replacePlaceholders(screen.joinedTodayWithAvatars.text?.text), + }, + { + as: "p", + defaults: { font: "inter", weight: "semiBold", size: "sm" }, + } + )} /> )} @@ -684,24 +794,36 @@ export function TrialPaymentTemplate({ ({ - title: buildTypographyProps(s.title, { - as: "h4", - defaults: { font: "inter", weight: "semiBold", size: "sm" }, - })!, - description: buildTypographyProps(s.description, { - as: "p", - defaults: { font: "inter", size: "xs" }, - })!, + title: buildTypographyProps( + { + ...s.title, + text: replacePlaceholders(s.title?.text), + }, + { + as: "h4", + defaults: { font: "inter", weight: "semiBold", size: "sm" }, + } + )!, + description: buildTypographyProps( + { + ...s.description, + text: replacePlaceholders(s.description?.text), + }, + { + as: "p", + defaults: { font: "inter", size: "xs" }, + } + )!, icon: s.icon === "questions" ? ( ({ - name: buildTypographyProps(r.name, { - as: "span", - defaults: { font: "inter", weight: "semiBold", size: "sm" }, - }), - text: buildTypographyProps(r.text, { - as: "p", - defaults: { font: "inter", size: "sm" }, - }), - date: buildTypographyProps(r.date, { - as: "span", - defaults: { font: "inter", size: "xs" }, - }), + name: buildTypographyProps( + { + ...r.name, + text: replacePlaceholders(r.name?.text), + }, + { + as: "span", + defaults: { font: "inter", weight: "semiBold", size: "sm" }, + } + ), + text: buildTypographyProps( + { + ...r.text, + text: replacePlaceholders(r.text?.text), + }, + { + as: "p", + defaults: { font: "inter", size: "sm" }, + } + ), + date: buildTypographyProps( + { + ...r.date, + text: replacePlaceholders(r.date?.text), + }, + { + as: "span", + defaults: { font: "inter", size: "xs" }, + } + ), avatar: r.avatar ? { imageProps: { src: r.avatar.src, alt: "avatar" }, @@ -840,19 +998,25 @@ export function TrialPaymentTemplate({ {screen.commonQuestions && ( ({ value: `q-${index}`, - trigger: { children: q.question }, - content: { children: q.answer }, + trigger: { children: replacePlaceholders(q.question) }, + content: { children: replacePlaceholders(q.answer) }, }))} accordionProps={{ defaultValue: "q-0", type: "single" }} /> @@ -861,10 +1025,16 @@ export function TrialPaymentTemplate({ {screen.stillHaveQuestions && ( ({ href: l.href, - children: l.text, + children: replacePlaceholders(l.text), })) || [], copyright: buildTypographyProps( - screen.footer.legal.copyright, + { + ...screen.footer.legal.copyright, + text: replacePlaceholders(screen.footer.legal.copyright?.text), + }, { as: "p", defaults: { font: "inter", size: "xs" } } ), } @@ -952,7 +1146,10 @@ export function TrialPaymentTemplate({ screen.footer.paymentMethods ? { title: buildTypographyProps( - screen.footer.paymentMethods.title, + { + ...screen.footer.paymentMethods.title, + text: replacePlaceholders(screen.footer.paymentMethods.title?.text), + }, { as: "h3", defaults: { font: "inter", weight: "bold" } } ), methods: diff --git a/src/components/funnel/templates/index.ts b/src/components/funnel/templates/index.ts index 712cc30..061237a 100644 --- a/src/components/funnel/templates/index.ts +++ b/src/components/funnel/templates/index.ts @@ -8,6 +8,7 @@ export { CouponTemplate } from "./CouponTemplate"; export { LoadersTemplate } from "./LoadersTemplate"; export { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate"; export { TrialPaymentTemplate } from "./TrialPaymentTemplate/index"; +export { TrialChoiceTemplate } from "./TrialChoiceTemplate"; export { SpecialOfferTemplate } from "./SpecialOffer/index"; // Layout Templates diff --git a/src/components/ui/TrialOption/TrialOption.stories.tsx b/src/components/ui/TrialOption/TrialOption.stories.tsx new file mode 100644 index 0000000..8c31cb4 --- /dev/null +++ b/src/components/ui/TrialOption/TrialOption.stories.tsx @@ -0,0 +1,54 @@ +import { Meta, StoryObj } from "@storybook/nextjs-vite"; +import { fn } from "storybook/test"; +import { TrialOption } from "./TrialOption"; +import type { TrialOptionProps } from "./TrialOption"; + +/** Reusable TrialOption component matching Figma states */ +const meta: Meta = { + title: "UI/TrialOption", + component: TrialOption, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + args: { + value: "$1", + title: "Basic", + state: "default", + error: false, + onClick: fn(), + } satisfies TrialOptionProps, + argTypes: { + state: { + control: { type: "select" }, + options: ["default", "selected", "accent"], + }, + error: { control: { type: "boolean" } }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Selected: Story = { + args: { state: "selected" }, +}; + +export const Accent: Story = { + args: { state: "accent" }, +}; + +export const ErrorDefault: Story = { + args: { state: "default", error: true }, +}; + +export const ErrorAccent: Story = { + args: { state: "accent", error: true }, +}; + +export const Playground: Story = { + args: {}, +}; diff --git a/src/components/ui/TrialOption/TrialOption.tsx b/src/components/ui/TrialOption/TrialOption.tsx new file mode 100644 index 0000000..c2adae7 --- /dev/null +++ b/src/components/ui/TrialOption/TrialOption.tsx @@ -0,0 +1,89 @@ +"use client"; + +import React from "react"; +import { cn } from "@/lib/utils"; + +export type TrialOptionState = "default" | "selected" | "accent"; + +export interface TrialOptionProps extends React.ComponentProps<"button"> { + value?: string; + title?: string; + state?: TrialOptionState; + error?: boolean; +} + +export function TrialOption({ + className, + value = "$1", + title = "Basic", + state = "default", + error = false, + ...props +}: TrialOptionProps) { + const isSelected = state === "selected"; + const isAccent = state === "accent"; + // Error must apply only to default and accent (not to selected) + const isError = error && !isSelected; + + return ( +
+

black, Selected -> #E5E7EB, Error -> #FF0D11 + isSelected ? "text-[#E5E7EB]" : isError ? "text-[#FF0D11]" : "text-[#000000]" + )} + > + {value} +

+

#6B7280, Selected -> #E5E7EB, Error -> #FF0D11 + isSelected ? "text-[#E5E7EB]" : isError ? "text-[#FF0D11]" : "text-[#6B7280]" + )} + > + {title} +

+
+ + ); +} diff --git a/src/components/ui/TrialOption/index.ts b/src/components/ui/TrialOption/index.ts new file mode 100644 index 0000000..a142213 --- /dev/null +++ b/src/components/ui/TrialOption/index.ts @@ -0,0 +1,2 @@ +export { TrialOption } from "./TrialOption"; +export type { TrialOptionProps, TrialOptionState } from "./TrialOption"; diff --git a/src/components/widgets/BottomActionButton/BottomActionButton.tsx b/src/components/widgets/BottomActionButton/BottomActionButton.tsx index b055df3..8aced14 100644 --- a/src/components/widgets/BottomActionButton/BottomActionButton.tsx +++ b/src/components/widgets/BottomActionButton/BottomActionButton.tsx @@ -23,6 +23,8 @@ export interface BottomActionButtonProps extends React.ComponentProps<"div"> { syncCssVar?: boolean; gradientBlurProps?: React.ComponentProps; + /** Вызывается при клике на отключенную кнопку действия */ + onDisabledClick?: () => void; } const BottomActionButton = forwardRef( @@ -35,6 +37,7 @@ const BottomActionButton = forwardRef( className, syncCssVar = true, gradientBlurProps, + onDisabledClick, ...props }, ref @@ -95,7 +98,30 @@ const BottomActionButton = forwardRef( {childrenAboveButton}
)} - {hasButton ? : null} + {hasButton ? ( +
+ {/* Invisible overlay to capture clicks when button is disabled */} + {actionButtonProps?.disabled && onDisabledClick ? ( +
{ + e.preventDefault(); + onDisabledClick(); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onDisabledClick(); + } + }} + tabIndex={0} + /> + ) : null} + +
+ ) : null} {childrenUnderButton}
diff --git a/src/components/widgets/ProfileCreated/ProfileCreated.stories.tsx b/src/components/widgets/ProfileCreated/ProfileCreated.stories.tsx new file mode 100644 index 0000000..f46894f --- /dev/null +++ b/src/components/widgets/ProfileCreated/ProfileCreated.stories.tsx @@ -0,0 +1,75 @@ +/* eslint-disable storybook/no-renderer-packages */ +import type { Meta, StoryObj } from '@storybook/react'; +import { ProfileCreated } from './ProfileCreated'; + +const meta = { + title: 'Widgets/ProfileCreated', + component: ProfileCreated, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + email: { + control: 'text', + description: 'Email address to display', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * Default state with a sample email + */ +export const Default: Story = { + args: { + email: 'logolgo@gmail.com', + }, +}; + +/** + * Example with a different email + */ +export const AnotherEmail: Story = { + args: { + email: 'john.doe@example.com', + }, +}; + +/** + * Example with a single letter email + */ +export const SingleLetter: Story = { + args: { + email: 'a@test.com', + }, +}; + +/** + * Example with a long email + */ +export const LongEmail: Story = { + args: { + email: 'very.long.email.address@example.com', + }, +}; + +/** + * Example with uppercase email (should still show uppercase letter) + */ +export const UppercaseEmail: Story = { + args: { + email: 'ADMIN@COMPANY.COM', + }, +}; + +/** + * Example with number starting email + */ +export const NumberStartEmail: Story = { + args: { + email: '123user@example.com', + }, +}; diff --git a/src/components/widgets/ProfileCreated/ProfileCreated.test.tsx b/src/components/widgets/ProfileCreated/ProfileCreated.test.tsx new file mode 100644 index 0000000..0bcdf25 --- /dev/null +++ b/src/components/widgets/ProfileCreated/ProfileCreated.test.tsx @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ProfileCreated } from './ProfileCreated'; + +describe('ProfileCreated', () => { + it('renders email correctly', () => { + render(); + expect(screen.getByText('test@example.com')).toBeInTheDocument(); + }); + + it('renders success message', () => { + render(); + expect(screen.getByText('Profile created successfully')).toBeInTheDocument(); + }); + + it('displays first letter of email in uppercase', () => { + render(); + expect(screen.getByText('J')).toBeInTheDocument(); + }); + + it('handles lowercase email correctly', () => { + render(); + expect(screen.getByText('A')).toBeInTheDocument(); + }); + + it('handles uppercase email correctly', () => { + render(); + expect(screen.getByText('B')).toBeInTheDocument(); + }); + + it('handles email starting with number', () => { + render(); + expect(screen.getByText('1')).toBeInTheDocument(); + }); + + it('handles single character email', () => { + render(); + expect(screen.getByText('X')).toBeInTheDocument(); + }); + + it('renders checkmark SVG', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg).toBeInTheDocument(); + expect(svg?.getAttribute('width')).toBe('16'); + expect(svg?.getAttribute('height')).toBe('16'); + }); + + it('has correct structure with all main elements', () => { + const { container } = render(); + + // Check for avatar container + const avatar = container.querySelector('.rounded-full.bg-gradient-to-br'); + expect(avatar).toBeInTheDocument(); + + // Check for email text + expect(screen.getByText('test@example.com')).toBeInTheDocument(); + + // Check for success text + expect(screen.getByText('Profile created successfully')).toBeInTheDocument(); + }); +}); diff --git a/src/components/widgets/ProfileCreated/ProfileCreated.tsx b/src/components/widgets/ProfileCreated/ProfileCreated.tsx new file mode 100644 index 0000000..b976bf0 --- /dev/null +++ b/src/components/widgets/ProfileCreated/ProfileCreated.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +interface ProfileCreatedProps { + email: string; +} + +/** + * ProfileCreated widget displays a success message when a user profile is created. + * Shows the email with an avatar containing the first letter of the email. + */ +export function ProfileCreated({ email }: ProfileCreatedProps) { + // Extract first letter of email in uppercase + const avatarLetter = email.charAt(0).toUpperCase(); + + return ( +
+ {/* Profile section with avatar and email */} +
+ {/* Avatar with first letter */} +
+ + {avatarLetter} + +
+ + {/* Email and success message */} +
+

+ {email} +

+

+ Profile created successfully +

+
+
+ + {/* Success checkmark icon */} +
+
+ + + + +
+
+
+ ); +} diff --git a/src/components/widgets/ProfileCreated/README.md b/src/components/widgets/ProfileCreated/README.md new file mode 100644 index 0000000..1561f11 --- /dev/null +++ b/src/components/widgets/ProfileCreated/README.md @@ -0,0 +1,73 @@ +# ProfileCreated + +Виджет для отображения успешного создания профиля пользователя. + +## Описание + +`ProfileCreated` - это компонент, который показывает email пользователя с аватаром (первая буква email) и сообщением об успешном создании профиля. Компонент включает зеленую галочку для визуального подтверждения успеха. + +## Использование + +```tsx +import { ProfileCreated } from '@/components/widgets/ProfileCreated'; + +function MyComponent() { + return ; +} +``` + +## Props + +| Prop | Type | Required | Description | +|-------|--------|----------|--------------------------------------| +| email | string | Да | Email адрес для отображения | + +## Особенности + +- **Аватар с первой буквой**: Автоматически извлекает первую букву email и преобразует в верхний регистр +- **Градиентный фон**: Использует градиент от голубого до индиго +- **Градиентная иконка**: Аватар имеет красивый градиент от синего к темно-синему +- **Иконка успеха**: Зеленая галочка справа для подтверждения действия +- **Адаптивный**: Использует `inline-flex` для гибкого размещения + +## Дизайн + +Компонент точно следует спецификации из Figma: +- Padding: 18px +- Border: 2px solid #BFDBFE +- Border radius: 16px +- Avatar size: 48px +- Gap между элементами: 20px (основной), 12px (профиль), 3px (внутри профиля) + +## Стили шрифтов + +- **Email**: Inter, 18px, font-weight: 600, color: #333 +- **Текст успеха**: Inter, 14px, font-weight: 500, color: #2563EB (blue-600) +- **Буква в аватаре**: Inter, 20px, font-weight: 700, color: white + +## Storybook + +Для просмотра всех вариантов компонента запустите Storybook: + +```bash +npm run storybook +``` + +Доступные stories: +- **Default**: Стандартный вид с примером email +- **AnotherEmail**: Пример с другим email +- **SingleLetter**: Email начинающийся с одной буквы +- **LongEmail**: Длинный email адрес +- **UppercaseEmail**: Email в верхнем регистре +- **NumberStartEmail**: Email начинающийся с цифры + +## Figma + +Дизайн компонента: [Figma Link](https://www.figma.com/design/kx7k6sswURrLwBeavffF9D/%D0%92%D0%BE%D1%80%D0%BE%D0%BD%D0%BA%D0%B0-%D0%9F%D0%BE%D1%80%D1%82%D1%80%D0%B5%D1%82?node-id=566-2752) + +## Примечания + +- Компонент не имеет внутренних состояний +- Не требует интеграции с другими системами +- Является чисто презентационным компонентом +- Использует только Tailwind CSS для стилизации diff --git a/src/components/widgets/ProfileCreated/index.ts b/src/components/widgets/ProfileCreated/index.ts new file mode 100644 index 0000000..6b9b159 --- /dev/null +++ b/src/components/widgets/ProfileCreated/index.ts @@ -0,0 +1 @@ +export { ProfileCreated } from './ProfileCreated'; diff --git a/src/components/widgets/TrialOptionsGrid/TrialOptionsGrid.stories.tsx b/src/components/widgets/TrialOptionsGrid/TrialOptionsGrid.stories.tsx new file mode 100644 index 0000000..0341af1 --- /dev/null +++ b/src/components/widgets/TrialOptionsGrid/TrialOptionsGrid.stories.tsx @@ -0,0 +1,73 @@ +import { Meta, StoryObj } from "@storybook/nextjs-vite"; +import { fn } from "storybook/test"; +import { TrialOptionsGrid } from "./TrialOptionsGrid"; +import type { TrialOptionsItem } from "./TrialOptionsGrid"; + +const buildItems = (n: number): TrialOptionsItem[] => + Array.from({ length: n }).map((_, i) => ({ + id: `opt-${i + 1}`, + value: `$${i + 1}`, + title: i % 2 === 0 ? "Basic" : "Pro", + state: i === 1 ? "selected" : i === 2 ? "accent" : "default", + error: false, + onClick: fn(), + })); + +const meta: Meta = { + title: "Widgets/TrialOptionsGrid", + component: TrialOptionsGrid, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + args: { + items: buildItems(2), + onItemClick: fn(), + className: "w-[360px]", + }, +}; + +export default meta; + +export type Story = StoryObj; + +export const One: Story = { + args: { + items: buildItems(1), + }, +}; + +export const Two: Story = { + args: { + items: buildItems(2), + }, +}; + +export const Three: Story = { + args: { + items: buildItems(3), + }, +}; + +export const Four: Story = { + args: { + items: buildItems(4), + }, +}; + +export const Five: Story = { + args: { + items: buildItems(5), + }, +}; + +export const WithErrors: Story = { + args: { + items: [ + { id: "a", value: "$1", title: "Basic", state: "default", error: true }, + { id: "b", value: "$2", title: "Pro", state: "selected", error: false }, + { id: "c", value: "$3", title: "Plus", state: "accent", error: true }, + { id: "d", value: "$4", title: "Max", state: "default", error: false }, + ], + }, +}; diff --git a/src/components/widgets/TrialOptionsGrid/TrialOptionsGrid.tsx b/src/components/widgets/TrialOptionsGrid/TrialOptionsGrid.tsx new file mode 100644 index 0000000..ff9b4ae --- /dev/null +++ b/src/components/widgets/TrialOptionsGrid/TrialOptionsGrid.tsx @@ -0,0 +1,67 @@ +"use client"; + +import React from "react"; +import { cn } from "@/lib/utils"; +import { TrialOption } from "@/components/ui/TrialOption"; +import type { TrialOptionProps } from "@/components/ui/TrialOption"; + +export type TrialOptionsItem = (Omit & { id: string }); + +export interface TrialOptionsGridProps extends React.ComponentProps<"div"> { + items: TrialOptionsItem[]; + /** Fallback click handler if item doesn't provide onClick */ + onItemClick?: (id: string) => void; + /** Extra class applied to each option wrapper */ + itemClassName?: string; +} + +/** + * TrialOptionsGrid — lays out TrialOption components in two columns with 12px gap. + * - 1 item → full width + * - 2 items → two columns + * - Odd count (>=3) → last item spans full width + * - Even count → pairs in two columns + */ +export function TrialOptionsGrid({ + className, + items, + onItemClick, + itemClassName, + ...props +}: TrialOptionsGridProps) { + const count = items.length; + + return ( +
+ {items.map((item, index) => { + const isLast = index === count - 1; + const shouldSpanFull = + count === 1 || (count % 2 !== 0 && isLast); + + return ( +
+ { + item.onClick?.(e); + if (!item.onClick && onItemClick) onItemClick(item.id); + }} + className={cn("w-full", item.className)} + /> +
+ ); + })} +
+ ); +} diff --git a/src/components/widgets/TrialOptionsGrid/index.ts b/src/components/widgets/TrialOptionsGrid/index.ts new file mode 100644 index 0000000..851de5e --- /dev/null +++ b/src/components/widgets/TrialOptionsGrid/index.ts @@ -0,0 +1,2 @@ +export { TrialOptionsGrid } from "./TrialOptionsGrid"; +export type { TrialOptionsGridProps, TrialOptionsItem } from "./TrialOptionsGrid"; diff --git a/src/entities/session/funnel/types.ts b/src/entities/session/funnel/types.ts index 067b4af..6a3e42c 100644 --- a/src/entities/session/funnel/types.ts +++ b/src/entities/session/funnel/types.ts @@ -21,6 +21,8 @@ export const FunnelPaymentVariantSchema = z.object({ price: z.number(), oldPrice: z.number().optional(), trialPrice: z.number().optional(), + title: z.string().optional(), + accent: z.boolean().optional(), }); export const FunnelPaymentPlacementSchema = z.object({ diff --git a/src/entities/session/payment/PaymentPlacementProvider.tsx b/src/entities/session/payment/PaymentPlacementProvider.tsx new file mode 100644 index 0000000..05dab89 --- /dev/null +++ b/src/entities/session/payment/PaymentPlacementProvider.tsx @@ -0,0 +1,122 @@ +"use client"; + +import React, { createContext, useContext, useState, useCallback } from "react"; +import type { IFunnelPaymentPlacement } from "../funnel/types"; +import { loadFunnelPaymentById } from "../funnel/loaders"; + +interface PlacementCacheEntry { + placement: IFunnelPaymentPlacement | null; + isLoading: boolean; + error: string | null; +} + +interface PaymentPlacementContextValue { + getPlacement: ( + funnelKey: string, + paymentId: string + ) => PlacementCacheEntry; + loadPlacement: (funnelKey: string, paymentId: string) => Promise; +} + +const PaymentPlacementContext = createContext( + null +); + +type PreloadedPlacement = { + funnelKey: string; + paymentId: string; + placement: IFunnelPaymentPlacement; +}; + +export function PaymentPlacementProvider({ + children, + initialCache, +}: { + children: React.ReactNode; + initialCache?: PreloadedPlacement[]; +}) { + // Cache: Map<"funnelKey:paymentId", PlacementCacheEntry> + const [cache, setCache] = useState>(() => { + const map = new Map(); + initialCache?.forEach(({ funnelKey, paymentId, placement }) => { + const key = `${funnelKey}:${paymentId}`; + map.set(key, { placement, isLoading: false, error: null }); + }); + return map; + }); + + const getCacheKey = (funnelKey: string, paymentId: string) => + `${funnelKey}:${paymentId}`; + + const getPlacement = useCallback( + (funnelKey: string, paymentId: string): PlacementCacheEntry => { + const key = getCacheKey(funnelKey, paymentId); + return ( + cache.get(key) || { placement: null, isLoading: false, error: null } + ); + }, + [cache] + ); + + const loadPlacement = useCallback( + async (funnelKey: string, paymentId: string) => { + const key = getCacheKey(funnelKey, paymentId); + + // Если уже загружается или загружено, не делаем повторный запрос + const existing = cache.get(key); + if (existing?.isLoading || existing?.placement) { + return; + } + + // Отмечаем как загружающийся + setCache((prev) => { + const next = new Map(prev); + next.set(key, { placement: null, isLoading: true, error: null }); + return next; + }); + + try { + const data = await loadFunnelPaymentById( + { funnel: funnelKey }, + paymentId + ); + + // Normalize union: record value can be IFunnelPaymentPlacement or IFunnelPaymentPlacement[] or null + const normalized: IFunnelPaymentPlacement | null = Array.isArray(data) + ? data[0] ?? null + : data ?? null; + + setCache((prev) => { + const next = new Map(prev); + next.set(key, { placement: normalized, isLoading: false, error: null }); + return next; + }); + } catch (e) { + const message = + e instanceof Error ? e.message : "Failed to load payment placement"; + setCache((prev) => { + const next = new Map(prev); + next.set(key, { placement: null, isLoading: false, error: message }); + return next; + }); + } + }, + [cache] + ); + + return ( + + {children} + + ); +} + +export function usePaymentPlacementContext() { + const context = useContext(PaymentPlacementContext); + if (!context) { + throw new Error( + "usePaymentPlacementContext must be used within PaymentPlacementProvider" + ); + } + return context; +} diff --git a/src/entities/session/payment/TrialVariantSelectionContext.tsx b/src/entities/session/payment/TrialVariantSelectionContext.tsx new file mode 100644 index 0000000..07253c6 --- /dev/null +++ b/src/entities/session/payment/TrialVariantSelectionContext.tsx @@ -0,0 +1,57 @@ +"use client"; + +import React, { createContext, useContext, useState } from "react"; + +interface TrialVariantSelectionContextValue { + selectedVariantId: string | null; + setSelectedVariantId: (variantId: string | null) => void; +} + +const TrialVariantSelectionContext = + createContext(null); + +const STORAGE_KEY = 'trial_variant_selection'; + +export function TrialVariantSelectionProvider({ + children, +}: { + children: React.ReactNode; +}) { + // Initialize from sessionStorage if available + const [selectedVariantId, setSelectedVariantId] = useState(() => { + if (typeof window === 'undefined') return null; + const stored = sessionStorage.getItem(STORAGE_KEY); + return stored || null; + }); + + const wrappedSetSelectedVariantId = (id: string | null) => { + setSelectedVariantId(id); + + // Persist to sessionStorage + if (typeof window !== 'undefined') { + if (id) { + sessionStorage.setItem(STORAGE_KEY, id); + } else { + sessionStorage.removeItem(STORAGE_KEY); + } + } + }; + + return ( + + {children} + + ); +} + +export function useTrialVariantSelection() { + const context = useContext(TrialVariantSelectionContext); + if (!context) { + throw new Error( + "useTrialVariantSelection must be used within TrialVariantSelectionProvider" + ); + } + return context; +} diff --git a/src/entities/session/payment/index.ts b/src/entities/session/payment/index.ts new file mode 100644 index 0000000..19d2eba --- /dev/null +++ b/src/entities/session/payment/index.ts @@ -0,0 +1,9 @@ +export { + PaymentPlacementProvider, + usePaymentPlacementContext, +} from "./PaymentPlacementProvider"; + +export { + TrialVariantSelectionProvider, + useTrialVariantSelection, +} from "./TrialVariantSelectionContext"; diff --git a/src/hooks/auth/useClientToken.ts b/src/hooks/auth/useClientToken.ts index 19533c0..1a7eed3 100644 --- a/src/hooks/auth/useClientToken.ts +++ b/src/hooks/auth/useClientToken.ts @@ -8,8 +8,25 @@ export const useClientToken = () => { useEffect(() => { (async () => { - const token = await getAuthTokenFromCookie(); - setToken(token); + try { + const token = await getAuthTokenFromCookie(); + // If server returned undefined, fall back to a stable mock in Storybook + if (!token && typeof window !== "undefined") { + const w = window as Window & { __STORYBOOK_ADDONS?: unknown }; + const isStorybook = Boolean(w.__STORYBOOK_ADDONS) || + /:\/\/.*(:6006|storybook)/i.test(w.location.href); + setToken(isStorybook ? "storybook-token" : undefined); + } else { + setToken(token); + } + } catch { + // In Storybook or non-Next runtime, server action may fail; use mock token + if (typeof window !== "undefined") { + setToken("storybook-token"); + } else { + setToken(undefined); + } + } })(); }, []); diff --git a/src/hooks/payment/usePaymentPlacement.ts b/src/hooks/payment/usePaymentPlacement.ts index eec6d58..c5d7fd0 100644 --- a/src/hooks/payment/usePaymentPlacement.ts +++ b/src/hooks/payment/usePaymentPlacement.ts @@ -1,9 +1,9 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo } from "react"; import type { FunnelDefinition } from "@/lib/funnel/types"; -import { loadFunnelPaymentById } from "@/entities/session/funnel/loaders"; import type { IFunnelPaymentPlacement } from "@/entities/session/funnel/types"; +import { usePaymentPlacementContext } from "@/entities/session/payment/PaymentPlacementProvider"; interface UsePaymentPlacementArgs { funnel: FunnelDefinition; @@ -20,49 +20,20 @@ export function usePaymentPlacement({ funnel, paymentId, }: UsePaymentPlacementArgs): UsePaymentPlacementResult { - const [placement, setPlacement] = useState( - null - ); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); + const { getPlacement, loadPlacement } = usePaymentPlacementContext(); const funnelKey = useMemo(() => funnel?.meta?.id ?? "", [funnel]); useEffect(() => { - let isMounted = true; if (!funnelKey || !paymentId) return; + loadPlacement(funnelKey, paymentId); + }, [funnelKey, paymentId, loadPlacement]); - (async () => { - try { - setIsLoading(true); - setError(null); + const cached = getPlacement(funnelKey, paymentId); - const data = await loadFunnelPaymentById( - { funnel: funnelKey }, - paymentId - ); - - // Normalize union: record value can be IFunnelPaymentPlacement or IFunnelPaymentPlacement[] or null - const normalized: IFunnelPaymentPlacement | null = Array.isArray(data) - ? data[0] ?? null - : data ?? null; - - if (!isMounted) return; - setPlacement(normalized); - } catch (e) { - if (!isMounted) return; - const message = - e instanceof Error ? e.message : "Failed to load payment placement"; - setError(message); - } finally { - if (isMounted) setIsLoading(false); - } - })(); - - return () => { - isMounted = false; - }; - }, [funnelKey, paymentId]); - - return { placement, isLoading, error }; + return { + placement: cached.placement, + isLoading: cached.isLoading, + error: cached.error, + }; } diff --git a/src/lib/admin/builder/state/defaults/index.ts b/src/lib/admin/builder/state/defaults/index.ts index 95b5523..74dd117 100644 --- a/src/lib/admin/builder/state/defaults/index.ts +++ b/src/lib/admin/builder/state/defaults/index.ts @@ -31,3 +31,4 @@ export { buildLoadersDefaults } from "./loaders"; export { buildSoulmateDefaults } from "./soulmate"; export { buildTrialPaymentDefaults } from "./trialPayment"; export { buildSpecialOfferDefaults } from "./specialOffer"; +export { buildTrialChoiceDefaults } from "./trialChoice"; diff --git a/src/lib/admin/builder/state/defaults/trialChoice.ts b/src/lib/admin/builder/state/defaults/trialChoice.ts new file mode 100644 index 0000000..6a8c39c --- /dev/null +++ b/src/lib/admin/builder/state/defaults/trialChoice.ts @@ -0,0 +1,30 @@ +import type { BuilderScreen } from "@/lib/admin/builder/types"; +import { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultSubtitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, +} from "./blocks"; + +export function buildTrialChoiceDefaults(id: string): BuilderScreen { + return { + id, + template: "trialChoice", + header: buildDefaultHeader({ + showProgress: false, + }), + title: buildDefaultTitle({ + show: false, + text: "Trial Choice", + }), + subtitle: buildDefaultSubtitle({ + show: false, + text: undefined, + }), + bottomActionButton: buildDefaultBottomActionButton({ + text: "Next", + }), + navigation: buildDefaultNavigation(), + } as BuilderScreen; +} diff --git a/src/lib/admin/builder/state/reducer.ts b/src/lib/admin/builder/state/reducer.ts index a9c876e..1f051af 100644 --- a/src/lib/admin/builder/state/reducer.ts +++ b/src/lib/admin/builder/state/reducer.ts @@ -129,6 +129,61 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil }, }); } + case "insert-screen": { + const { atIndex, template = "list" } = action.payload; + const nextId = generateScreenId(state.screens.map((s) => s.id)); + const newScreen = createScreenByTemplate(template, nextId); + + // Вставляем экран на указанную позицию + const updatedScreens = [...state.screens]; + updatedScreens.splice(atIndex, 0, newScreen); + + // 🎯 АВТОМАТИЧЕСКОЕ СВЯЗЫВАНИЕ + // Обновляем навигацию предыдущего экрана (если есть) + if (atIndex > 0) { + const prevScreen = updatedScreens[atIndex - 1]; + const nextScreen = updatedScreens[atIndex + 1]; + + // Если предыдущий экран указывал на следующий, перенаправляем через новый + if (nextScreen && prevScreen.navigation?.defaultNextScreenId === nextScreen.id) { + updatedScreens[atIndex - 1] = { + ...prevScreen, + navigation: { + ...prevScreen.navigation, + defaultNextScreenId: nextId, + }, + }; + + // Новый экран указывает на следующий + updatedScreens[atIndex] = { + ...newScreen, + navigation: { + ...newScreen.navigation, + defaultNextScreenId: nextScreen.id, + }, + }; + } else if (!prevScreen.navigation?.defaultNextScreenId) { + // Если у предыдущего нет перехода, связываем с новым + updatedScreens[atIndex - 1] = { + ...prevScreen, + navigation: { + ...prevScreen.navigation, + defaultNextScreenId: nextId, + }, + }; + } + } + + return withDirty(state, { + ...state, + screens: updatedScreens, + selectedScreenId: newScreen.id, + meta: { + ...state.meta, + firstScreenId: atIndex === 0 ? newScreen.id : state.meta.firstScreenId ?? newScreen.id, + }, + }); + } case "remove-screen": { const filtered = state.screens.filter((screen) => screen.id !== action.payload.screenId); const selectedScreenId = diff --git a/src/lib/admin/builder/state/types.ts b/src/lib/admin/builder/state/types.ts index 29da528..ba68bfc 100644 --- a/src/lib/admin/builder/state/types.ts +++ b/src/lib/admin/builder/state/types.ts @@ -13,6 +13,7 @@ export type BuilderAction = | { type: "set-meta"; payload: Partial } | { type: "set-default-texts"; payload: Partial } | { type: "add-screen"; payload?: { template?: ScreenDefinition["template"] } & Partial } + | { type: "insert-screen"; payload: { template?: ScreenDefinition["template"]; atIndex: number } & Partial } | { type: "remove-screen"; payload: { screenId: string } } | { type: "update-screen"; payload: { screenId: string; screen: Partial } } | { type: "reorder-screens"; payload: { fromIndex: number; toIndex: number } } diff --git a/src/lib/admin/builder/state/utils.ts b/src/lib/admin/builder/state/utils.ts index 939fb94..1ecb7e5 100644 --- a/src/lib/admin/builder/state/utils.ts +++ b/src/lib/admin/builder/state/utils.ts @@ -11,6 +11,7 @@ import { buildLoadersDefaults } from "./defaults/loaders"; import { buildSoulmateDefaults } from "./defaults/soulmate"; import { buildTrialPaymentDefaults } from "./defaults/trialPayment"; import { buildSpecialOfferDefaults } from "./defaults/specialOffer"; +import { buildTrialChoiceDefaults } from "./defaults/trialChoice"; /** * Marks the state as dirty if it has changed @@ -66,6 +67,8 @@ export function createScreenByTemplate( return buildTrialPaymentDefaults(id); case "specialOffer": return buildSpecialOfferDefaults(id); + case "trialChoice": + return buildTrialChoiceDefaults(id); default: throw new Error(`Unknown template: ${template}`); } diff --git a/src/lib/funnel/bakedFunnels.ts b/src/lib/funnel/bakedFunnels.ts index bc2bf19..665b679 100644 --- a/src/lib/funnel/bakedFunnels.ts +++ b/src/lib/funnel/bakedFunnels.ts @@ -6,14 +6,14 @@ import type { FunnelDefinition } from "./types"; export const BAKED_FUNNELS: Record = { - "soulmate": { + "soulmate-small": { "meta": { - "id": "soulmate", - "title": "Soulmate V1", + "id": "soulmate-small", + "title": "Soulmate V1 (копия)", "description": "Soulmate", "firstScreenId": "onboarding", - "googleAnalyticsId": "G-4N17LL3BB5", - "yandexMetrikaId": "104471567" + "googleAnalyticsId": "", + "yandexMetrikaId": "" }, "defaultTexts": { "nextButton": "Next", @@ -25,6 +25,7 @@ export const BAKED_FUNNELS: Record = { "template": "soulmate", "header": { "showBackButton": false, + "showProgress": true, "show": false }, "title": { @@ -55,62 +56,7 @@ export const BAKED_FUNNELS: Record = { "align": "center", "color": "default" }, - "variants": [ - { - "conditions": [ - { - "screenId": "onboarding", - "operator": "includesAny", - "conditionType": "unleash", - "unleashFlag": "soulmate-onboarding-image", - "unleashVariants": [ - "v0" - ] - } - ], - "overrides": {} - }, - { - "conditions": [ - { - "screenId": "onboarding", - "operator": "includesAny", - "conditionType": "unleash", - "unleashFlag": "soulmate-onboarding-image", - "unleashVariants": [ - "v1" - ] - } - ], - "overrides": { - "soulmatePortraitsDelivered": { - "image": null, - "mediaUrl": "/images/90b8c77f-c0cd-475d-a4de-bcabb3708c59.png", - "mediaType": "image" - } - } - }, - { - "conditions": [ - { - "screenId": "onboarding", - "operator": "includesAny", - "conditionType": "unleash", - "unleashFlag": "soulmate-onboarding-image", - "unleashVariants": [ - "v2" - ] - } - ], - "overrides": { - "soulmatePortraitsDelivered": { - "image": null, - "mediaUrl": "/images/275472b0-30e0-47d7-a1ab-8090bc9fb236.mp4", - "mediaType": "video" - } - } - } - ], + "variants": [], "soulmatePortraitsDelivered": { "image": "/soulmate-portrait-delivered-male.jpg", "text": { @@ -161,6 +107,7 @@ export const BAKED_FUNNELS: Record = { "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -216,6 +163,7 @@ export const BAKED_FUNNELS: Record = { "template": "list", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -234,7 +182,7 @@ export const BAKED_FUNNELS: Record = { }, "navigation": { "rules": [], - "defaultNextScreenId": "analysis-target", + "defaultNextScreenId": "email", "isEndScreen": false }, "list": { @@ -255,1987 +203,12 @@ export const BAKED_FUNNELS: Record = { }, "variants": [] }, - { - "id": "relationship-status", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "You are currently?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "subtitle": { - "text": "This helps make the portrait and insights more accurate.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "analysis-target", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "in_relationship", - "label": "In a relationship", - "disabled": false - }, - { - "id": "single", - "label": "Single", - "disabled": false - }, - { - "id": "after_breakup", - "label": "Just went through a breakup", - "disabled": false - }, - { - "id": "its_complicated", - "label": "It’s complicated", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "analysis-target", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Who are we analyzing?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "subtitle": { - "text": "This helps make the portrait and insights more accurate.", - "show": true, - "font": "manrope", - "weight": "regular", - "size": "md", - "align": "center", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "partner-age", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "current_partner", - "label": "Current partner", - "disabled": false - }, - { - "id": "crush", - "label": "Crush", - "disabled": false - }, - { - "id": "ex_partner", - "label": "Ex", - "disabled": false - }, - { - "id": "future_date", - "label": "Future connection", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "partner-age", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Current partner’s age", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [ - { - "conditions": [ - { - "screenId": "partner-age", - "conditionType": "options", - "operator": "includesAny", - "optionIds": [ - "under_29" - ], - "values": [], - "unleashVariants": [] - } - ], - "nextScreenId": "partner-age-detail" - } - ], - "defaultNextScreenId": "partner-ethnicity", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "under_29", - "label": "Under 29", - "disabled": false - }, - { - "id": "from_30_to_39", - "label": "30–39", - "disabled": false - }, - { - "id": "from_40_to_49", - "label": "40–49", - "disabled": false - }, - { - "id": "from_50_to_59", - "label": "50–59", - "disabled": false - }, - { - "id": "over_60", - "label": "60+", - "disabled": false - } - ] - }, - "variants": [ - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": [ - "current_partner" - ] - } - ], - "overrides": {} - }, - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": [ - "crush" - ] - } - ], - "overrides": { - "title": { - "text": "Age of the person you like" - } - } - }, - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": [ - "ex_partner" - ] - } - ], - "overrides": { - "title": { - "text": "Ex’s age" - } - } - }, - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": [ - "future_date" - ] - } - ], - "overrides": { - "title": { - "text": "Future partner’s age" - } - } - } - ] - }, - { - "id": "partner-age-detail", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Please specify a bit more", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "subtitle": { - "text": "So the portrait can be as accurate as possible.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "partner-ethnicity", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "from_18_to_21", - "label": "18–21", - "disabled": false - }, - { - "id": "from_22_to_25", - "label": "22–25", - "disabled": false - }, - { - "id": "from_26_to_29", - "label": "26–29", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "partner-ethnicity", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Your partner’s ethnicity?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "partner-eye-color", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "white", - "label": "White", - "disabled": false - }, - { - "id": "hispanic_latino", - "label": "Hispanic / Latino", - "disabled": false - }, - { - "id": "african_african_american", - "label": "African / African-American", - "disabled": false - }, - { - "id": "asian", - "label": "Asian", - "disabled": false - }, - { - "id": "indian_south_asian", - "label": "Indian / South Asian", - "disabled": false - }, - { - "id": "middle_eastern_arab", - "label": "Middle Eastern / Arab", - "disabled": false - }, - { - "id": "native_american_indigenous", - "label": "Native American / Indigenous", - "disabled": false - }, - { - "id": "no_preference", - "label": "No preference", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "partner-eye-color", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Your partner’s eye color?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "partner-hair-length", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "brown", - "label": "Brown", - "disabled": false - }, - { - "id": "blue", - "label": "Blue", - "disabled": false - }, - { - "id": "hazel", - "label": "Hazel", - "disabled": false - }, - { - "id": "green", - "label": "Green", - "disabled": false - }, - { - "id": "amber", - "label": "Amber", - "disabled": false - }, - { - "id": "gray", - "label": "Gray", - "disabled": false - }, - { - "id": "unknown", - "label": "I don’t know", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "partner-hair-length", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Choose the hair length", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "subtitle": { - "text": "It affects the portrait’s shape and mood.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "burnout-support", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "short", - "label": "Short", - "disabled": false - }, - { - "id": "medium", - "label": "Medium", - "disabled": false - }, - { - "id": "long", - "label": "Long", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "burnout-support", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "When you’re burned out, you need your partner to", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "burnout-result", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "acknowledged_and_calmed", - "label": "Acknowledge your frustration and comfort you", - "disabled": false - }, - { - "id": "gave_emotional_support", - "label": "Give emotional support and a safe space", - "disabled": false - }, - { - "id": "took_over_tasks", - "label": "Take over daily tasks so you can recover", - "disabled": false - }, - { - "id": "inspired_with_plan", - "label": "Inspire you with a goal and a short action plan", - "disabled": false - }, - { - "id": "shifted_to_positive", - "label": "Shift your focus to something positive — a walk, a movie, funny stories", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "burnout-result", - "template": "info", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "burnout-result", - "show": false, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "center", - "color": "default" - }, - "subtitle": { - "text": "This kind of partner **knows how to listen and support**, and you’re a **deep soul** who values honesty and the power of genuine emotions.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "center", - "color": "default" - }, - "bottomActionButton": { - "show": true, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "birthdate", - "isEndScreen": false - }, - "icon": { - "type": "image", - "value": "/images/fb6eb360-db1e-4433-9c8a-33eedfad1b12.svg", - "size": "lg" - }, - "variables": [], - "variants": [ - { - "conditions": [ - { - "screenId": "burnout-support", - "operator": "includesAny", - "optionIds": [ - "acknowledged_and_calmed" - ] - } - ], - "overrides": {} - }, - { - "conditions": [ - { - "screenId": "burnout-support", - "operator": "includesAny", - "optionIds": [ - "gave_emotional_support" - ] - } - ], - "overrides": { - "subtitle": { - "text": "This kind of person creates a **sense of security**, and you have the wisdom and emotional maturity to choose closeness and trust." - } - } - }, - { - "conditions": [ - { - "screenId": "burnout-support", - "operator": "includesAny", - "optionIds": [ - "took_over_tasks" - ] - } - ], - "overrides": { - "subtitle": { - "text": "This kind of partner is ready to **lend a shoulder** when it’s needed, and your strength lies in your ability to **trust** and **accept support** — that’s your natural wisdom." - } - } - }, - { - "conditions": [ - { - "screenId": "burnout-support", - "operator": "includesAny", - "optionIds": [ - "inspired_with_plan" - ] - } - ], - "overrides": { - "subtitle": { - "text": "This kind of person **brings clarity** and **motivates**, while you stand out for your **willpower** and **drive for growth** — you’re not afraid to move forward." - } - } - }, - { - "conditions": [ - { - "screenId": "burnout-support", - "operator": "includesAny", - "optionIds": [ - "shifted_to_positive" - ] - } - ], - "overrides": { - "subtitle": { - "text": "This kind of partner knows how to **bring back joy**, and you show your strength through your ability to **stay lighthearted** and **keep a bright outlook** on life." - } - } - } - ] - }, - { - "id": "birthdate", - "template": "date", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "When were you born?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "subtitle": { - "text": "The moment you were born holds deep underlying patterns.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "nature-archetype", - "isEndScreen": false - }, - "dateInput": { - "monthLabel": "Month", - "dayLabel": "Month", - "yearLabel": "Month", - "monthPlaceholder": "MM", - "dayPlaceholder": "DD", - "yearPlaceholder": "YYYY", - "showSelectedDate": true, - "selectedDateFormat": "dd MMMM yyyy", - "selectedDateLabel": "Selected date:", - "zodiac": { - "enabled": true, - "storageKey": "userZodiac" - }, - "registrationFieldKey": "profile.birthdate", - "validationMessage": "Please enter a valid date" - }, - "variants": [] - }, - { - "id": "nature-archetype", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Which natural symbol best matches your personality?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "love-priority", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "flower", - "label": "Flower — tenderness, care, charm", - "emoji": "🌹", - "disabled": false - }, - { - "id": "sea", - "label": "Sea — depth, mystery, emotion", - "emoji": "🌊", - "disabled": false - }, - { - "id": "sun", - "label": "Sun — energy, strength, brightness", - "emoji": "🌞️", - "disabled": false - }, - { - "id": "moon", - "label": "Moon — intuition, sensitivity", - "emoji": "🌙", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "love-priority", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "When it comes to love, what matters more to you: heart or mind?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "love-priority-result", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "follow_heart", - "label": "I follow my heart", - "emoji": "🧡", - "disabled": false - }, - { - "id": "follow_mind", - "label": "I rely on my mind", - "emoji": "🧠", - "disabled": false - }, - { - "id": "balance_heart_mind", - "label": "A balance of heart and mind", - "emoji": "🎯", - "disabled": false - }, - { - "id": "depends_on_situation", - "label": "Depends on the situation", - "emoji": "⚖️", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "love-priority-result", - "template": "info", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Title", - "show": false, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "center", - "color": "default" - }, - "subtitle": { - "text": "According to our statistics, **51% of {{gender}} {{zodiac}}** trust their emotions. But sensitivity alone isn’t enough. We’ll show which qualities in your partner will bring warmth and confidence — and create their portrait.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "center", - "color": "default" - }, - "bottomActionButton": { - "show": true, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "relationship-block", - "isEndScreen": false - }, - "icon": { - "type": "image", - "value": "/images/528c3574-2121-46cd-b5e5-b1fda5ae9315.svg", - "size": "xl" - }, - "variables": [ - { - "name": "gender", - "mappings": [ - { - "conditions": [ - { - "screenId": "gender", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "male" - ] - } - ], - "value": "men" - } - ], - "fallback": "women" - }, - { - "name": "zodiac", - "mappings": [ - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "aries" - ] - } - ], - "value": "Aries" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "taurus" - ] - } - ], - "value": "Taurus" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "gemini" - ] - } - ], - "value": "Gemini" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "cancer" - ] - } - ], - "value": "Cancer" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "leo" - ] - } - ], - "value": "Leo" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "virgo" - ] - } - ], - "value": "Virgo" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "libra" - ] - } - ], - "value": "Libra" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "scorpio" - ] - } - ], - "value": "Scorpio" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "sagittarius" - ] - } - ], - "value": "Sagittarius" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "capricorn" - ] - } - ], - "value": "Capricorn" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "aquarius" - ] - } - ], - "value": "Aquarius" - }, - { - "conditions": [ - { - "screenId": "userZodiac", - "conditionType": "values", - "operator": "includesAny", - "values": [ - "pisces" - ] - } - ], - "value": "Pisces" - } - ], - "fallback": "Pisces" - } - ], - "variants": [ - { - "conditions": [ - { - "screenId": "love-priority", - "operator": "includesAny", - "optionIds": [ - "follow_mind" - ] - } - ], - "overrides": { - "subtitle": { - "text": "According to our statistics, **43% of {{gender}} {{zodiac}}** choose reason. But calculations alone aren’t enough. We’ll reveal which traits in your partner will build trust — and create their portrait." - }, - "icon": { - "value": "/images/575ab717-eaa5-462b-8aa6-0202a62c9099.svg" - } - } - }, - { - "conditions": [ - { - "screenId": "love-priority", - "operator": "includesAny", - "optionIds": [ - "balance_heart_mind" - ] - } - ], - "overrides": { - "subtitle": { - "text": "According to our statistics, **47% of {{gender}} {{zodiac}}** seek balance. But keeping it isn’t easy. We’ll show which qualities in your partner will unite passion and stability — and create their portrait." - }, - "icon": { - "value": "/images/7dd85bf0-4b92-4213-9e2a-82ba1e53d165.svg" - } - } - }, - { - "conditions": [ - { - "screenId": "love-priority", - "operator": "includesAny", - "optionIds": [ - "depends_on_situation" - ] - } - ], - "overrides": { - "subtitle": { - "text": "According to our statistics, **37% of {{gender}} {{zodiac}}** make their choice based on circumstances. But such flexibility often leads to doubt. We’ll reveal who can bring you stability and confidence — and draw your partner’s portrait." - }, - "icon": { - "value": "/images/6bd25c4d-9308-4907-a54f-b7bc10322fa8.svg" - } - } - } - ] - }, - { - "id": "relationship-block", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "What gets in the way of your relationships the most?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "relationship-block-result", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "fear_of_wrong_choice", - "label": "Fear of making the wrong choice again", - "emoji": "💔", - "disabled": false - }, - { - "id": "wasted_years", - "label": "Wasting years on the “wrong” person", - "emoji": "🕰️", - "disabled": false - }, - { - "id": "lack_of_depth", - "label": "There’s passion, but not enough depth", - "emoji": "🔥", - "disabled": false - }, - { - "id": "unclear_desires", - "label": "Not sure what I really want", - "emoji": "🗝", - "disabled": false - }, - { - "id": "stuck_in_past", - "label": "Can’t let go of a past relationship", - "emoji": "👻", - "disabled": false - }, - { - "id": "fear_of_loneliness", - "label": "Afraid of being alone", - "emoji": "🕯", - "disabled": false - } - ] - }, - "variants": [ - { - "conditions": [ - { - "screenId": "relationship-status", - "operator": "includesAny", - "optionIds": [ - "single", - "after_breakup" - ] - } - ], - "overrides": { - "list": { - "options": [ - { - "id": "fear_of_wrong_choice", - "label": "Fear of making the wrong choice again", - "emoji": "💔" - }, - { - "id": "wasted_years", - "label": "Feeling like the years are slipping away", - "emoji": "🕰️" - }, - { - "id": "wrong_people", - "label": "Meeting interesting people, but not the right one", - "emoji": "😕" - }, - { - "id": "unclear_needs", - "label": "Not sure who I really need", - "emoji": "🧩" - }, - { - "id": "stuck_in_past", - "label": "The past keeps me from moving on", - "emoji": "👻" - }, - { - "id": "fear_of_loneliness", - "label": "Afraid of being alone", - "emoji": "🕯" - } - ] - }, - "title": { - "text": "What gets in the way of finding love the most?" - } - } - } - ] - }, - { - "id": "relationship-block-result", - "template": "info", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "You’re not alone in this fear.", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "center", - "color": "default" - }, - "subtitle": { - "text": "Many people are afraid of repeating the past. We’ll help you recognize the right signs and choose the person who’s truly meant for you.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "center", - "color": "default" - }, - "bottomActionButton": { - "show": true, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "core-need", - "isEndScreen": false - }, - "icon": { - "type": "image", - "value": "/images/1d7d3979-6627-416e-8f80-c0388a6cec22.svg", - "size": "xl" - }, - "variables": [], - "variants": [ - { - "conditions": [ - { - "screenId": "relationship-block", - "operator": "includesAny", - "optionIds": [ - "wasted_years" - ] - } - ], - "overrides": { - "title": { - "text": "This pain is familiar to many." - }, - "subtitle": { - "text": "The feeling of wasted time is hard. We’ll show you how to stop getting stuck in the past and move forward." - }, - "icon": { - "value": "/images/5ae02c30-44a0-4a8c-a814-9fd2490cdc77.svg" - } - } - }, - { - "conditions": [ - { - "screenId": "relationship-block", - "operator": "includesAny", - "optionIds": [ - "lack_of_depth" - ] - } - ], - "overrides": { - "title": { - "text": "Many people face this." - }, - "subtitle": { - "text": "Bright emotions fade quickly without a foundation. We’ll help you turn a connection into true closeness." - } - } - }, - { - "conditions": [ - { - "screenId": "relationship-block", - "operator": "includesAny", - "optionIds": [ - "unclear_desires" - ] - } - ], - "overrides": { - "title": { - "text": "This is often hard to figure out." - }, - "subtitle": { - "text": "Understanding yourself is the key to making the right choice. We’ll help you see which qualities truly matter to you." - } - } - }, - { - "conditions": [ - { - "screenId": "relationship-block", - "operator": "includesAny", - "optionIds": [ - "stuck_in_past" - ] - } - ], - "overrides": { - "title": { - "text": "You’re not the only one stuck in the past." - }, - "subtitle": { - "text": "The past can hold on too tightly. We’ll show you how to let go and make room for new love." - } - } - }, - { - "conditions": [ - { - "screenId": "relationship-block", - "operator": "includesAny", - "optionIds": [ - "fear_of_loneliness" - ] - } - ], - "overrides": { - "title": { - "text": "This fear is very familiar to many." - }, - "subtitle": { - "text": "The thought of a lonely future is frightening. We’ll help you build a path where someone special walks beside you." - } - } - }, - { - "conditions": [ - { - "screenId": "relationship-block", - "operator": "includesAny", - "optionIds": [ - "wrong_people" - ] - } - ], - "overrides": { - "title": { - "text": "Many people go through this." - } - } - }, - { - "conditions": [ - { - "screenId": "relationship-block", - "operator": "includesAny", - "optionIds": [ - "unclear_needs" - ] - } - ], - "overrides": { - "title": { - "text": "It’s okay not to know right away." - }, - "subtitle": { - "text": "Figuring out what kind of partner you truly need isn’t easy. We’ll help you see which qualities really matter." - } - } - } - ] - }, - { - "id": "core-need", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "What’s your core need right now?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "partner-similarity", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "safety_and_support", - "label": "Safety and support", - "disabled": false - }, - { - "id": "passion_and_spark", - "label": "Passion and spark", - "disabled": false - }, - { - "id": "calm_and_acceptance", - "label": "Calm and acceptance", - "disabled": false - }, - { - "id": "inspiration_and_growth", - "label": "Inspiration and growth", - "disabled": false - }, - { - "id": "not_important", - "label": "Doesn’t matter", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "partner-similarity", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Is your partner similar to you?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "partner-role", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "similar", - "label": "Yes, we have things in common", - "disabled": false - }, - { - "id": "different", - "label": "We’re completely different", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "partner-role", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Preferred partner role", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "relationship-strength", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "leader", - "label": "Leader", - "disabled": false - }, - { - "id": "equal", - "label": "Equal", - "disabled": false - }, - { - "id": "supportive", - "label": "Supportive", - "disabled": false - }, - { - "id": "flexible", - "label": "Flexible", - "disabled": false - }, - { - "id": "dependent", - "label": "Dependent on me", - "disabled": false - }, - { - "id": "situational", - "label": "Depends on the situation", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "relationship-strength", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "What’s your main source of strength in a relationship?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "love-expression", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "support_and_care", - "label": "Support and care", - "disabled": false - }, - { - "id": "admiration_and_recognition", - "label": "Admiration and appreciation", - "disabled": false - }, - { - "id": "freedom_and_space", - "label": "Freedom and space", - "disabled": false - }, - { - "id": "shared_goals_and_plans", - "label": "Shared goals and plans", - "disabled": false - }, - { - "id": "joy_and_lightness", - "label": "Joy and lightness", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "love-expression", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "How do you express love?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "relationship-future", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "words", - "label": "With words", - "disabled": false - }, - { - "id": "actions", - "label": "Through actions", - "disabled": false - }, - { - "id": "quality_time", - "label": "By spending time together", - "disabled": false - }, - { - "id": "care", - "label": "With care", - "disabled": false - }, - { - "id": "passion", - "label": "With passion", - "disabled": false - }, - { - "id": "in_my_own_way", - "label": "In my own way", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "relationship-future", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "How do you see your relationship’s future?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "relationship-energy", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "home_and_family", - "label": "A shared home and family", - "disabled": false - }, - { - "id": "travel_and_discovery", - "label": "Travel and new discoveries", - "disabled": false - }, - { - "id": "shared_goals", - "label": "Joint projects and goals", - "disabled": false - }, - { - "id": "present_moment", - "label": "Just being together “here and now”", - "disabled": false - }, - { - "id": "unsure", - "label": "Hard to say for now", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "relationship-energy", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "What kind of energy do you want in a relationship?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": false, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "relationship-metaphor", - "isEndScreen": false - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "lightness_and_joy", - "label": "Lightness and joy", - "disabled": false - }, - { - "id": "strength_and_drive", - "label": "Strength and drive", - "disabled": false - }, - { - "id": "comfort_and_safety", - "label": "Comfort and stability", - "disabled": false - }, - { - "id": "depth_and_meaning", - "label": "Depth and meaning", - "disabled": false - }, - { - "id": "freedom_and_space", - "label": "Freedom and space", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "relationship-metaphor", - "template": "list", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Which image of a relationship feels closest to you?", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "left", - "color": "default" - }, - "subtitle": { - "text": "You can choose several options.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "left", - "color": "default" - }, - "bottomActionButton": { - "show": true, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "portrait-generation", - "isEndScreen": false - }, - "list": { - "selectionType": "multi", - "options": [ - { - "id": "bridge", - "label": "Bridge — connection through obstacles", - "disabled": false - }, - { - "id": "mountain_path", - "label": "Mountain path — challenges and meaning", - "disabled": false - }, - { - "id": "dance", - "label": "Dance — balance and mutual steps", - "disabled": false - }, - { - "id": "key_and_lock", - "label": "Key and lock — shared values", - "disabled": false - }, - { - "id": "harbor", - "label": "Harbor — safety and peace", - "disabled": false - }, - { - "id": "lighthouse", - "label": "Lighthouse — guidance and support", - "disabled": false - }, - { - "id": "ocean_after_storm", - "label": "Ocean after the storm — renewal and new beginnings", - "disabled": false - }, - { - "id": "garden", - "label": "Garden — care and growth", - "disabled": false - } - ] - }, - "variants": [] - }, - { - "id": "portrait-generation", - "template": "loaders", - "header": { - "showBackButton": false, - "show": false - }, - "title": { - "text": "Creating the portrait of your other half.", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "center", - "color": "default" - }, - "bottomActionButton": { - "show": true, - "text": "Continue", - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "email", - "isEndScreen": false - }, - "progressbars": { - "items": [ - { - "processingTitle": "Analyzing your answers", - "processingSubtitle": "Processing...", - "completedTitle": "Analyzing your answers", - "completedSubtitle": "Complete" - }, - { - "processingTitle": "Portrait of the Soulmate", - "processingSubtitle": "Processing...", - "completedTitle": "Portrait of the Soulmate", - "completedSubtitle": "Complete" - }, - { - "processingTitle": "Portrait of the Soulmate", - "processingSubtitle": "Processing...", - "completedTitle": "Connection Insights", - "completedSubtitle": "Complete" - } - ], - "transitionDuration": 3000 - }, - "variants": [] - }, { "id": "email", "template": "email", "header": { "showBackButton": true, + "showProgress": true, "show": true }, "title": { @@ -2255,7 +228,7 @@ export const BAKED_FUNNELS: Record = { }, "navigation": { "rules": [], - "defaultNextScreenId": "coupon", + "defaultNextScreenId": "trial-choice", "isEndScreen": false }, "emailInput": { @@ -2285,32 +258,25 @@ export const BAKED_FUNNELS: Record = { ] }, { - "id": "coupon", - "template": "coupon", + "id": "trial-choice", + "template": "trialChoice", "header": { "showBackButton": true, + "showProgress": false, "show": true }, "title": { - "text": "You’re in luck!", - "show": true, + "text": "Trial Choice", + "show": false, "font": "manrope", "weight": "bold", "size": "2xl", - "align": "center", - "color": "default" - }, - "subtitle": { - "text": "You’ve received an exclusive 94% discount.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "center", + "align": "left", "color": "default" }, "bottomActionButton": { "show": true, + "text": "Next", "cornerRadius": "3xl", "showPrivacyTermsConsent": false }, @@ -2319,51 +285,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "payment", "isEndScreen": false }, - "coupon": { - "title": { - "text": "Special Offer", - "font": "manrope", - "weight": "bold", - "align": "center", - "size": "lg", - "color": "default" - }, - "offer": { - "title": { - "text": "94% OFF", - "font": "manrope", - "weight": "bold", - "align": "center", - "size": "3xl", - "color": "primary" - }, - "description": { - "text": "One-time special offer", - "font": "inter", - "weight": "medium", - "color": "muted", - "align": "center", - "size": "md" - } - }, - "promoCode": { - "text": "SOULMATE94", - "font": "geistMono", - "weight": "bold", - "align": "center", - "size": "lg", - "color": "accent" - }, - "footer": { - "text": "Copy or click **Continue**", - "font": "inter", - "weight": "medium", - "color": "muted", - "align": "center", - "size": "sm" - } - }, - "copiedMessage": "Promo code copied!", "variants": [] }, { @@ -2371,10 +292,11 @@ export const BAKED_FUNNELS: Record = { "template": "trialPayment", "header": { "showBackButton": true, + "showProgress": true, "show": false }, "title": { - "text": "Title", + "text": "Payment", "show": false, "font": "manrope", "weight": "bold", @@ -2390,7 +312,8 @@ export const BAKED_FUNNELS: Record = { "navigation": { "rules": [], "defaultNextScreenId": "specialoffer", - "isEndScreen": false + "isEndScreen": true, + "onBackScreenId": "screen-7" }, "variants": [ { @@ -2473,7 +396,7 @@ export const BAKED_FUNNELS: Record = { }, "tryForDays": { "title": { - "text": "Try it for 7 days!" + "text": "Try it for {{trialPeriod}}!" }, "textList": { "items": [ @@ -2487,7 +410,7 @@ export const BAKED_FUNNELS: Record = { "text": "Talk to live experts and get guidance." }, { - "text": "Start your 7-day trial for just $1.00." + "text": "Start your {{trialPeriod}} trial for just {{trialPrice}}." }, { "text": "Cancel anytime—just 24 hours before renewal." @@ -2507,13 +430,13 @@ export const BAKED_FUNNELS: Record = { "text": "Total" }, "price": { - "text": "$1.00" + "text": "{{trialPrice}}" }, "oldPrice": { - "text": "$14.99" + "text": "{{oldPrice}}" }, "discount": { - "text": "94% discount applied" + "text": "{{discountPercent}}% discount applied" } } }, @@ -2837,11 +760,3025 @@ export const BAKED_FUNNELS: Record = { } } }, + { + "id": "screen-7", + "template": "specialOffer", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Special Offer", + "show": false, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "Добавьте детали справа", + "show": false, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "GET {{trialPeriod}} TRIAL", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "isEndScreen": true + }, + "variants": [], + "text": { + "title": { + "text": "Special Offer" + }, + "subtitle": { + "text": "SAVE {{discountPercent}}% OFF!" + }, + "description": { + "trialPrice": { + "text": "{{trialPrice}}" + }, + "text": { + "text": " instead of " + }, + "oldTrialPrice": { + "text": "~~{{oldTrialPrice}}~~" + } + } + }, + "advantages": { + "items": [ + { + "icon": { + "text": "🔥" + }, + "text": { + "text": " **{{trialPeriodHyphen}}** trial instead of ~~{{oldTrialPeriod}}~~" + } + }, + { + "icon": { + "text": "💝" + }, + "text": { + "text": " Get **{{discountPercent}}%** off your Soulmate Sketch" + } + }, + { + "icon": { + "text": "💌" + }, + "text": { + "text": " Includes the **“Finding the One”** Guide" + } + } + ] + } + } + ] + }, + + "soulmate": { + "meta": { + "id": "soulmate", + "title": "Soulmate V1", + "description": "Soulmate", + "firstScreenId": "onboarding", + "googleAnalyticsId": "G-4N17LL3BB5", + "yandexMetrikaId": "104471567" + }, + "defaultTexts": { + "nextButton": "Next", + "privacyBanner": "We don’t share personal information — it stays safe and under your control." + }, + "screens": [ + { + "id": "onboarding", + "template": "soulmate", + "header": { + "showBackButton": false, + "showProgress": true, + "show": false + }, + "title": { + "text": "Soulmate Portrait", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "Continue", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": true + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "gender", + "isEndScreen": false + }, + "description": { + "text": "Ready to see who your true soulmate is?", + "font": "manrope", + "weight": "regular", + "size": "md", + "align": "center", + "color": "default" + }, + "variants": [ + { + "conditions": [ + { + "screenId": "onboarding", + "operator": "includesAny", + "conditionType": "unleash", + "unleashFlag": "soulmate-onboarding-image", + "unleashVariants": [ + "v0" + ] + } + ], + "overrides": {} + }, + { + "conditions": [ + { + "screenId": "onboarding", + "operator": "includesAny", + "conditionType": "unleash", + "unleashFlag": "soulmate-onboarding-image", + "unleashVariants": [ + "v1" + ] + } + ], + "overrides": { + "soulmatePortraitsDelivered": { + "image": null, + "mediaUrl": "/images/90b8c77f-c0cd-475d-a4de-bcabb3708c59.png", + "mediaType": "image" + } + } + }, + { + "conditions": [ + { + "screenId": "onboarding", + "operator": "includesAny", + "conditionType": "unleash", + "unleashFlag": "soulmate-onboarding-image", + "unleashVariants": [ + "v2" + ] + } + ], + "overrides": { + "soulmatePortraitsDelivered": { + "image": null, + "mediaUrl": "/images/275472b0-30e0-47d7-a1ab-8090bc9fb236.mp4", + "mediaType": "video" + } + } + } + ], + "soulmatePortraitsDelivered": { + "image": "/soulmate-portrait-delivered-male.jpg", + "text": { + "text": "soulmate portraits delivered today", + "font": "inter", + "weight": "medium", + "size": "sm", + "color": "primary" + }, + "avatars": [ + { + "src": "/avatars/male-1.jpg", + "alt": "Male 1" + }, + { + "src": "/avatars/male-2.jpg", + "alt": "Male 2" + }, + { + "src": "/avatars/male-3.jpg", + "alt": "Male 3" + }, + { + "src": "", + "fallbackText": "900+" + } + ] + }, + "textList": { + "items": [ + { + "text": "Just 2 minutes — and the Portrait will reveal the one who’s destined to be with you." + }, + { + "text": "Astonishing 99% accuracy." + }, + { + "text": "An unexpected revelation awaits you." + }, + { + "text": "All that’s left is to dare to look." + } + ] + } + }, + { + "id": "gender", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "What’s your gender?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "It all starts with you! Choose your gender.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "partner-gender", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "female", + "label": "FEMALE", + "emoji": "🩷", + "disabled": false + }, + { + "id": "male", + "label": "MALE", + "emoji": "💙", + "disabled": false + } + ], + "registrationFieldKey": "profile.gender" + }, + "variants": [] + }, + { + "id": "partner-gender", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Who are you interested in?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "analysis-target", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "male", + "label": "Male", + "disabled": false + }, + { + "id": "female", + "label": "Female", + "disabled": false + } + ], + "registrationFieldKey": "partner.gender" + }, + "variants": [] + }, + { + "id": "relationship-status", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "You are currently?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "This helps make the portrait and insights more accurate.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "analysis-target", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "in_relationship", + "label": "In a relationship", + "disabled": false + }, + { + "id": "single", + "label": "Single", + "disabled": false + }, + { + "id": "after_breakup", + "label": "Just went through a breakup", + "disabled": false + }, + { + "id": "its_complicated", + "label": "It’s complicated", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "analysis-target", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Who are we analyzing?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "This helps make the portrait and insights more accurate.", + "show": true, + "font": "manrope", + "weight": "regular", + "size": "md", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "partner-age", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "current_partner", + "label": "Current partner", + "disabled": false + }, + { + "id": "crush", + "label": "Crush", + "disabled": false + }, + { + "id": "ex_partner", + "label": "Ex", + "disabled": false + }, + { + "id": "future_date", + "label": "Future connection", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "partner-age", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Current partner’s age", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [ + { + "conditions": [ + { + "screenId": "partner-age", + "conditionType": "options", + "operator": "includesAny", + "optionIds": [ + "under_29" + ], + "values": [], + "unleashVariants": [] + } + ], + "nextScreenId": "partner-age-detail" + } + ], + "defaultNextScreenId": "partner-ethnicity", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "under_29", + "label": "Under 29", + "disabled": false + }, + { + "id": "from_30_to_39", + "label": "30–39", + "disabled": false + }, + { + "id": "from_40_to_49", + "label": "40–49", + "disabled": false + }, + { + "id": "from_50_to_59", + "label": "50–59", + "disabled": false + }, + { + "id": "over_60", + "label": "60+", + "disabled": false + } + ] + }, + "variants": [ + { + "conditions": [ + { + "screenId": "analysis-target", + "operator": "includesAny", + "optionIds": [ + "current_partner" + ] + } + ], + "overrides": {} + }, + { + "conditions": [ + { + "screenId": "analysis-target", + "operator": "includesAny", + "optionIds": [ + "crush" + ] + } + ], + "overrides": { + "title": { + "text": "Age of the person you like" + } + } + }, + { + "conditions": [ + { + "screenId": "analysis-target", + "operator": "includesAny", + "optionIds": [ + "ex_partner" + ] + } + ], + "overrides": { + "title": { + "text": "Ex’s age" + } + } + }, + { + "conditions": [ + { + "screenId": "analysis-target", + "operator": "includesAny", + "optionIds": [ + "future_date" + ] + } + ], + "overrides": { + "title": { + "text": "Future partner’s age" + } + } + } + ] + }, + { + "id": "partner-age-detail", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Please specify a bit more", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "So the portrait can be as accurate as possible.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "partner-ethnicity", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "from_18_to_21", + "label": "18–21", + "disabled": false + }, + { + "id": "from_22_to_25", + "label": "22–25", + "disabled": false + }, + { + "id": "from_26_to_29", + "label": "26–29", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "partner-ethnicity", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Your partner’s ethnicity?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "partner-eye-color", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "white", + "label": "White", + "disabled": false + }, + { + "id": "hispanic_latino", + "label": "Hispanic / Latino", + "disabled": false + }, + { + "id": "african_african_american", + "label": "African / African-American", + "disabled": false + }, + { + "id": "asian", + "label": "Asian", + "disabled": false + }, + { + "id": "indian_south_asian", + "label": "Indian / South Asian", + "disabled": false + }, + { + "id": "middle_eastern_arab", + "label": "Middle Eastern / Arab", + "disabled": false + }, + { + "id": "native_american_indigenous", + "label": "Native American / Indigenous", + "disabled": false + }, + { + "id": "no_preference", + "label": "No preference", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "partner-eye-color", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Your partner’s eye color?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "partner-hair-length", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "brown", + "label": "Brown", + "disabled": false + }, + { + "id": "blue", + "label": "Blue", + "disabled": false + }, + { + "id": "hazel", + "label": "Hazel", + "disabled": false + }, + { + "id": "green", + "label": "Green", + "disabled": false + }, + { + "id": "amber", + "label": "Amber", + "disabled": false + }, + { + "id": "gray", + "label": "Gray", + "disabled": false + }, + { + "id": "unknown", + "label": "I don’t know", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "partner-hair-length", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Choose the hair length", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "It affects the portrait’s shape and mood.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "burnout-support", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "short", + "label": "Short", + "disabled": false + }, + { + "id": "medium", + "label": "Medium", + "disabled": false + }, + { + "id": "long", + "label": "Long", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "burnout-support", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "When you’re burned out, you need your partner to", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "burnout-result", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "acknowledged_and_calmed", + "label": "Acknowledge your frustration and comfort you", + "disabled": false + }, + { + "id": "gave_emotional_support", + "label": "Give emotional support and a safe space", + "disabled": false + }, + { + "id": "took_over_tasks", + "label": "Take over daily tasks so you can recover", + "disabled": false + }, + { + "id": "inspired_with_plan", + "label": "Inspire you with a goal and a short action plan", + "disabled": false + }, + { + "id": "shifted_to_positive", + "label": "Shift your focus to something positive — a walk, a movie, funny stories", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "burnout-result", + "template": "info", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "burnout-result", + "show": false, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "center", + "color": "default" + }, + "subtitle": { + "text": "This kind of partner **knows how to listen and support**, and you’re a **deep soul** who values honesty and the power of genuine emotions.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "birthdate", + "isEndScreen": false + }, + "icon": { + "type": "image", + "value": "/images/fb6eb360-db1e-4433-9c8a-33eedfad1b12.svg", + "size": "lg" + }, + "variables": [], + "variants": [ + { + "conditions": [ + { + "screenId": "burnout-support", + "operator": "includesAny", + "optionIds": [ + "acknowledged_and_calmed" + ] + } + ], + "overrides": {} + }, + { + "conditions": [ + { + "screenId": "burnout-support", + "operator": "includesAny", + "optionIds": [ + "gave_emotional_support" + ] + } + ], + "overrides": { + "subtitle": { + "text": "This kind of person creates a **sense of security**, and you have the wisdom and emotional maturity to choose closeness and trust." + } + } + }, + { + "conditions": [ + { + "screenId": "burnout-support", + "operator": "includesAny", + "optionIds": [ + "took_over_tasks" + ] + } + ], + "overrides": { + "subtitle": { + "text": "This kind of partner is ready to **lend a shoulder** when it’s needed, and your strength lies in your ability to **trust** and **accept support** — that’s your natural wisdom." + } + } + }, + { + "conditions": [ + { + "screenId": "burnout-support", + "operator": "includesAny", + "optionIds": [ + "inspired_with_plan" + ] + } + ], + "overrides": { + "subtitle": { + "text": "This kind of person **brings clarity** and **motivates**, while you stand out for your **willpower** and **drive for growth** — you’re not afraid to move forward." + } + } + }, + { + "conditions": [ + { + "screenId": "burnout-support", + "operator": "includesAny", + "optionIds": [ + "shifted_to_positive" + ] + } + ], + "overrides": { + "subtitle": { + "text": "This kind of partner knows how to **bring back joy**, and you show your strength through your ability to **stay lighthearted** and **keep a bright outlook** on life." + } + } + } + ] + }, + { + "id": "birthdate", + "template": "date", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "When were you born?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "The moment you were born holds deep underlying patterns.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "nature-archetype", + "isEndScreen": false + }, + "dateInput": { + "monthLabel": "Month", + "dayLabel": "Month", + "yearLabel": "Month", + "monthPlaceholder": "MM", + "dayPlaceholder": "DD", + "yearPlaceholder": "YYYY", + "showSelectedDate": true, + "selectedDateFormat": "dd MMMM yyyy", + "selectedDateLabel": "Selected date:", + "zodiac": { + "enabled": true, + "storageKey": "userZodiac" + }, + "registrationFieldKey": "profile.birthdate", + "validationMessage": "Please enter a valid date" + }, + "variants": [] + }, + { + "id": "nature-archetype", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Which natural symbol best matches your personality?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "love-priority", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "flower", + "label": "Flower — tenderness, care, charm", + "emoji": "🌹", + "disabled": false + }, + { + "id": "sea", + "label": "Sea — depth, mystery, emotion", + "emoji": "🌊", + "disabled": false + }, + { + "id": "sun", + "label": "Sun — energy, strength, brightness", + "emoji": "🌞️", + "disabled": false + }, + { + "id": "moon", + "label": "Moon — intuition, sensitivity", + "emoji": "🌙", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "love-priority", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "When it comes to love, what matters more to you: heart or mind?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "love-priority-result", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "follow_heart", + "label": "I follow my heart", + "emoji": "🧡", + "disabled": false + }, + { + "id": "follow_mind", + "label": "I rely on my mind", + "emoji": "🧠", + "disabled": false + }, + { + "id": "balance_heart_mind", + "label": "A balance of heart and mind", + "emoji": "🎯", + "disabled": false + }, + { + "id": "depends_on_situation", + "label": "Depends on the situation", + "emoji": "⚖️", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "love-priority-result", + "template": "info", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Title", + "show": false, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "center", + "color": "default" + }, + "subtitle": { + "text": "According to our statistics, **51% of {{gender}} {{zodiac}}** trust their emotions. But sensitivity alone isn’t enough. We’ll show which qualities in your partner will bring warmth and confidence — and create their portrait.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "relationship-block", + "isEndScreen": false + }, + "icon": { + "type": "image", + "value": "/images/528c3574-2121-46cd-b5e5-b1fda5ae9315.svg", + "size": "xl" + }, + "variables": [ + { + "name": "gender", + "mappings": [ + { + "conditions": [ + { + "screenId": "gender", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "male" + ] + } + ], + "value": "men" + } + ], + "fallback": "women" + }, + { + "name": "zodiac", + "mappings": [ + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "aries" + ] + } + ], + "value": "Aries" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "taurus" + ] + } + ], + "value": "Taurus" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "gemini" + ] + } + ], + "value": "Gemini" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "cancer" + ] + } + ], + "value": "Cancer" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "leo" + ] + } + ], + "value": "Leo" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "virgo" + ] + } + ], + "value": "Virgo" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "libra" + ] + } + ], + "value": "Libra" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "scorpio" + ] + } + ], + "value": "Scorpio" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "sagittarius" + ] + } + ], + "value": "Sagittarius" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "capricorn" + ] + } + ], + "value": "Capricorn" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "aquarius" + ] + } + ], + "value": "Aquarius" + }, + { + "conditions": [ + { + "screenId": "userZodiac", + "conditionType": "values", + "operator": "includesAny", + "values": [ + "pisces" + ] + } + ], + "value": "Pisces" + } + ], + "fallback": "Pisces" + } + ], + "variants": [ + { + "conditions": [ + { + "screenId": "love-priority", + "operator": "includesAny", + "optionIds": [ + "follow_mind" + ] + } + ], + "overrides": { + "subtitle": { + "text": "According to our statistics, **43% of {{gender}} {{zodiac}}** choose reason. But calculations alone aren’t enough. We’ll reveal which traits in your partner will build trust — and create their portrait." + }, + "icon": { + "value": "/images/575ab717-eaa5-462b-8aa6-0202a62c9099.svg" + } + } + }, + { + "conditions": [ + { + "screenId": "love-priority", + "operator": "includesAny", + "optionIds": [ + "balance_heart_mind" + ] + } + ], + "overrides": { + "subtitle": { + "text": "According to our statistics, **47% of {{gender}} {{zodiac}}** seek balance. But keeping it isn’t easy. We’ll show which qualities in your partner will unite passion and stability — and create their portrait." + }, + "icon": { + "value": "/images/7dd85bf0-4b92-4213-9e2a-82ba1e53d165.svg" + } + } + }, + { + "conditions": [ + { + "screenId": "love-priority", + "operator": "includesAny", + "optionIds": [ + "depends_on_situation" + ] + } + ], + "overrides": { + "subtitle": { + "text": "According to our statistics, **37% of {{gender}} {{zodiac}}** make their choice based on circumstances. But such flexibility often leads to doubt. We’ll reveal who can bring you stability and confidence — and draw your partner’s portrait." + }, + "icon": { + "value": "/images/6bd25c4d-9308-4907-a54f-b7bc10322fa8.svg" + } + } + } + ] + }, + { + "id": "relationship-block", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "What gets in the way of your relationships the most?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "relationship-block-result", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "fear_of_wrong_choice", + "label": "Fear of making the wrong choice again", + "emoji": "💔", + "disabled": false + }, + { + "id": "wasted_years", + "label": "Wasting years on the “wrong” person", + "emoji": "🕰️", + "disabled": false + }, + { + "id": "lack_of_depth", + "label": "There’s passion, but not enough depth", + "emoji": "🔥", + "disabled": false + }, + { + "id": "unclear_desires", + "label": "Not sure what I really want", + "emoji": "🗝", + "disabled": false + }, + { + "id": "stuck_in_past", + "label": "Can’t let go of a past relationship", + "emoji": "👻", + "disabled": false + }, + { + "id": "fear_of_loneliness", + "label": "Afraid of being alone", + "emoji": "🕯", + "disabled": false + } + ] + }, + "variants": [ + { + "conditions": [ + { + "screenId": "relationship-status", + "operator": "includesAny", + "optionIds": [ + "single", + "after_breakup" + ] + } + ], + "overrides": { + "list": { + "options": [ + { + "id": "fear_of_wrong_choice", + "label": "Fear of making the wrong choice again", + "emoji": "💔" + }, + { + "id": "wasted_years", + "label": "Feeling like the years are slipping away", + "emoji": "🕰️" + }, + { + "id": "wrong_people", + "label": "Meeting interesting people, but not the right one", + "emoji": "😕" + }, + { + "id": "unclear_needs", + "label": "Not sure who I really need", + "emoji": "🧩" + }, + { + "id": "stuck_in_past", + "label": "The past keeps me from moving on", + "emoji": "👻" + }, + { + "id": "fear_of_loneliness", + "label": "Afraid of being alone", + "emoji": "🕯" + } + ] + }, + "title": { + "text": "What gets in the way of finding love the most?" + } + } + } + ] + }, + { + "id": "relationship-block-result", + "template": "info", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "You’re not alone in this fear.", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "center", + "color": "default" + }, + "subtitle": { + "text": "Many people are afraid of repeating the past. We’ll help you recognize the right signs and choose the person who’s truly meant for you.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "core-need", + "isEndScreen": false + }, + "icon": { + "type": "image", + "value": "/images/1d7d3979-6627-416e-8f80-c0388a6cec22.svg", + "size": "xl" + }, + "variables": [], + "variants": [ + { + "conditions": [ + { + "screenId": "relationship-block", + "operator": "includesAny", + "optionIds": [ + "wasted_years" + ] + } + ], + "overrides": { + "title": { + "text": "This pain is familiar to many." + }, + "subtitle": { + "text": "The feeling of wasted time is hard. We’ll show you how to stop getting stuck in the past and move forward." + }, + "icon": { + "value": "/images/5ae02c30-44a0-4a8c-a814-9fd2490cdc77.svg" + } + } + }, + { + "conditions": [ + { + "screenId": "relationship-block", + "operator": "includesAny", + "optionIds": [ + "lack_of_depth" + ] + } + ], + "overrides": { + "title": { + "text": "Many people face this." + }, + "subtitle": { + "text": "Bright emotions fade quickly without a foundation. We’ll help you turn a connection into true closeness." + } + } + }, + { + "conditions": [ + { + "screenId": "relationship-block", + "operator": "includesAny", + "optionIds": [ + "unclear_desires" + ] + } + ], + "overrides": { + "title": { + "text": "This is often hard to figure out." + }, + "subtitle": { + "text": "Understanding yourself is the key to making the right choice. We’ll help you see which qualities truly matter to you." + } + } + }, + { + "conditions": [ + { + "screenId": "relationship-block", + "operator": "includesAny", + "optionIds": [ + "stuck_in_past" + ] + } + ], + "overrides": { + "title": { + "text": "You’re not the only one stuck in the past." + }, + "subtitle": { + "text": "The past can hold on too tightly. We’ll show you how to let go and make room for new love." + } + } + }, + { + "conditions": [ + { + "screenId": "relationship-block", + "operator": "includesAny", + "optionIds": [ + "fear_of_loneliness" + ] + } + ], + "overrides": { + "title": { + "text": "This fear is very familiar to many." + }, + "subtitle": { + "text": "The thought of a lonely future is frightening. We’ll help you build a path where someone special walks beside you." + } + } + }, + { + "conditions": [ + { + "screenId": "relationship-block", + "operator": "includesAny", + "optionIds": [ + "wrong_people" + ] + } + ], + "overrides": { + "title": { + "text": "Many people go through this." + } + } + }, + { + "conditions": [ + { + "screenId": "relationship-block", + "operator": "includesAny", + "optionIds": [ + "unclear_needs" + ] + } + ], + "overrides": { + "title": { + "text": "It’s okay not to know right away." + }, + "subtitle": { + "text": "Figuring out what kind of partner you truly need isn’t easy. We’ll help you see which qualities really matter." + } + } + } + ] + }, + { + "id": "core-need", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "What’s your core need right now?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "partner-similarity", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "safety_and_support", + "label": "Safety and support", + "disabled": false + }, + { + "id": "passion_and_spark", + "label": "Passion and spark", + "disabled": false + }, + { + "id": "calm_and_acceptance", + "label": "Calm and acceptance", + "disabled": false + }, + { + "id": "inspiration_and_growth", + "label": "Inspiration and growth", + "disabled": false + }, + { + "id": "not_important", + "label": "Doesn’t matter", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "partner-similarity", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Is your partner similar to you?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "partner-role", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "similar", + "label": "Yes, we have things in common", + "disabled": false + }, + { + "id": "different", + "label": "We’re completely different", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "partner-role", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Preferred partner role", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "relationship-strength", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "leader", + "label": "Leader", + "disabled": false + }, + { + "id": "equal", + "label": "Equal", + "disabled": false + }, + { + "id": "supportive", + "label": "Supportive", + "disabled": false + }, + { + "id": "flexible", + "label": "Flexible", + "disabled": false + }, + { + "id": "dependent", + "label": "Dependent on me", + "disabled": false + }, + { + "id": "situational", + "label": "Depends on the situation", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "relationship-strength", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "What’s your main source of strength in a relationship?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "love-expression", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "support_and_care", + "label": "Support and care", + "disabled": false + }, + { + "id": "admiration_and_recognition", + "label": "Admiration and appreciation", + "disabled": false + }, + { + "id": "freedom_and_space", + "label": "Freedom and space", + "disabled": false + }, + { + "id": "shared_goals_and_plans", + "label": "Shared goals and plans", + "disabled": false + }, + { + "id": "joy_and_lightness", + "label": "Joy and lightness", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "love-expression", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "How do you express love?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "relationship-future", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "words", + "label": "With words", + "disabled": false + }, + { + "id": "actions", + "label": "Through actions", + "disabled": false + }, + { + "id": "quality_time", + "label": "By spending time together", + "disabled": false + }, + { + "id": "care", + "label": "With care", + "disabled": false + }, + { + "id": "passion", + "label": "With passion", + "disabled": false + }, + { + "id": "in_my_own_way", + "label": "In my own way", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "relationship-future", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "How do you see your relationship’s future?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "relationship-energy", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "home_and_family", + "label": "A shared home and family", + "disabled": false + }, + { + "id": "travel_and_discovery", + "label": "Travel and new discoveries", + "disabled": false + }, + { + "id": "shared_goals", + "label": "Joint projects and goals", + "disabled": false + }, + { + "id": "present_moment", + "label": "Just being together “here and now”", + "disabled": false + }, + { + "id": "unsure", + "label": "Hard to say for now", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "relationship-energy", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "What kind of energy do you want in a relationship?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "relationship-metaphor", + "isEndScreen": false + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "lightness_and_joy", + "label": "Lightness and joy", + "disabled": false + }, + { + "id": "strength_and_drive", + "label": "Strength and drive", + "disabled": false + }, + { + "id": "comfort_and_safety", + "label": "Comfort and stability", + "disabled": false + }, + { + "id": "depth_and_meaning", + "label": "Depth and meaning", + "disabled": false + }, + { + "id": "freedom_and_space", + "label": "Freedom and space", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "relationship-metaphor", + "template": "list", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Which image of a relationship feels closest to you?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "subtitle": { + "text": "You can choose several options.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "portrait-generation", + "isEndScreen": false + }, + "list": { + "selectionType": "multi", + "options": [ + { + "id": "bridge", + "label": "Bridge — connection through obstacles", + "disabled": false + }, + { + "id": "mountain_path", + "label": "Mountain path — challenges and meaning", + "disabled": false + }, + { + "id": "dance", + "label": "Dance — balance and mutual steps", + "disabled": false + }, + { + "id": "key_and_lock", + "label": "Key and lock — shared values", + "disabled": false + }, + { + "id": "harbor", + "label": "Harbor — safety and peace", + "disabled": false + }, + { + "id": "lighthouse", + "label": "Lighthouse — guidance and support", + "disabled": false + }, + { + "id": "ocean_after_storm", + "label": "Ocean after the storm — renewal and new beginnings", + "disabled": false + }, + { + "id": "garden", + "label": "Garden — care and growth", + "disabled": false + } + ] + }, + "variants": [] + }, + { + "id": "portrait-generation", + "template": "loaders", + "header": { + "showBackButton": false, + "showProgress": true, + "show": false + }, + "title": { + "text": "Creating the portrait of your other half.", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "Continue", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "email", + "isEndScreen": false + }, + "progressbars": { + "items": [ + { + "processingTitle": "Analyzing your answers", + "processingSubtitle": "Processing...", + "completedTitle": "Analyzing your answers", + "completedSubtitle": "Complete" + }, + { + "processingTitle": "Portrait of the Soulmate", + "processingSubtitle": "Processing...", + "completedTitle": "Portrait of the Soulmate", + "completedSubtitle": "Complete" + }, + { + "processingTitle": "Portrait of the Soulmate", + "processingSubtitle": "Processing...", + "completedTitle": "Connection Insights", + "completedSubtitle": "Complete" + } + ], + "transitionDuration": 3000 + }, + "variants": [] + }, + { + "id": "email", + "template": "email", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "Your soulmate’s portrait is ready! Where should we send it?", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "Continue", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": true + }, + "navigation": { + "rules": [ + { + "conditions": [ + { + "screenId": "email", + "conditionType": "unleash", + "operator": "includesAny", + "optionIds": [], + "values": [], + "unleashFlag": "soulmate-trial-grid", + "unleashVariants": [ + "coupon" + ] + } + ], + "nextScreenId": "coupon" + }, + { + "conditions": [ + { + "screenId": "email", + "conditionType": "unleash", + "operator": "includesAny", + "optionIds": [], + "values": [], + "unleashFlag": "soulmate-trial-grid", + "unleashVariants": [ + "grid" + ] + } + ], + "nextScreenId": "trial-choice" + } + ], + "defaultNextScreenId": "coupon", + "isEndScreen": false + }, + "emailInput": { + "label": "Email", + "placeholder": "example@email.com" + }, + "image": { + "src": "/female-portrait.jpg" + }, + "variants": [ + { + "conditions": [ + { + "screenId": "partner-gender", + "operator": "includesAny", + "optionIds": [ + "male" + ] + } + ], + "overrides": { + "image": { + "src": "/male-portrait.jpg" + } + } + } + ] + }, + { + "id": "coupon", + "template": "coupon", + "header": { + "showBackButton": true, + "showProgress": true, + "show": true + }, + "title": { + "text": "You’re in luck!", + "show": true, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "center", + "color": "default" + }, + "subtitle": { + "text": "You’ve received an exclusive 94% discount.", + "show": true, + "font": "manrope", + "weight": "medium", + "size": "lg", + "align": "center", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "payment", + "isEndScreen": false + }, + "coupon": { + "title": { + "text": "Special Offer", + "font": "manrope", + "weight": "bold", + "align": "center", + "size": "lg", + "color": "default" + }, + "offer": { + "title": { + "text": "94% OFF", + "font": "manrope", + "weight": "bold", + "align": "center", + "size": "3xl", + "color": "primary" + }, + "description": { + "text": "One-time special offer", + "font": "inter", + "weight": "medium", + "color": "muted", + "align": "center", + "size": "md" + } + }, + "promoCode": { + "text": "SOULMATE94", + "font": "geistMono", + "weight": "bold", + "align": "center", + "size": "lg", + "color": "accent" + }, + "footer": { + "text": "Copy or click **Continue**", + "font": "inter", + "weight": "medium", + "color": "muted", + "align": "center", + "size": "sm" + } + }, + "copiedMessage": "Promo code copied!", + "variants": [] + }, + { + "id": "trial-choice", + "template": "trialChoice", + "header": { + "showBackButton": true, + "showProgress": false, + "show": true + }, + "title": { + "text": "Trial Choice", + "show": false, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": true, + "text": "Next", + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "payment", + "isEndScreen": false + }, + "variants": [] + }, + { + "id": "payment", + "template": "trialPayment", + "header": { + "showBackButton": true, + "showProgress": true, + "show": false + }, + "title": { + "text": "Payment", + "show": false, + "font": "manrope", + "weight": "bold", + "size": "2xl", + "align": "left", + "color": "default" + }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, + "navigation": { + "rules": [], + "defaultNextScreenId": "specialoffer", + "isEndScreen": true, + "onBackScreenId": "specialoffer" + }, + "variants": [ + { + "conditions": [ + { + "screenId": "partner-gender", + "operator": "includesAny", + "optionIds": [ + "male" + ] + } + ], + "overrides": { + "unlockYourSketch": { + "image": { + "src": "/trial-payment/portrait-male.jpg" + } + } + } + } + ], + "headerBlock": { + "text": { + "text": "⚠️ Your sketch expires soon!" + }, + "timer": { + "text": "" + }, + "timerSeconds": 600 + }, + "unlockYourSketch": { + "title": { + "text": "Unlock Your Sketch" + }, + "subtitle": { + "text": "Just One Click to Reveal Your Match!" + }, + "image": { + "src": "/trial-payment/portrait-female.jpg" + }, + "blur": { + "text": { + "text": "Unlock to reveal your personalized portrait" + }, + "icon": "lock" + }, + "buttonText": "Get My Soulmate Sketch" + }, + "joinedToday": { + "count": { + "text": "954" + }, + "text": { + "text": "Joined today" + } + }, + "trustedByOver": { + "text": { + "text": "Trusted by over **355,000** people." + } + }, + "findingOneGuide": { + "header": { + "emoji": { + "text": "❤️" + }, + "title": { + "text": "Finding the One Guide" + } + }, + "text": { + "text": "You’re special — you have imagination and energy that not everyone can understand. You deserve more than to be “half-understood” — you deserve to meet someone who sees the world as deeply and vividly as you do. This guide will help you recognize the signs of destiny, trust your path, and find the one whose heart is already connected to yours.\nThey may already be closer than you think — drawn to your light, searching for the same rare connection. Stay open, follow your intuition, and notice the quiet moments that feel like fate. Every step you take brings you nearer to the soul that was always meant to walk beside you." + }, + "blur": { + "text": { + "text": "Full access is required to unlock the complete report." + }, + "icon": "lock" + } + }, + "tryForDays": { + "title": { + "text": "Try it for {{trialPeriod}}!" + }, + "textList": { + "items": [ + { + "text": "Receive a hand-drawn sketch of your soulmate." + }, + { + "text": "Reveal the path with the guide." + }, + { + "text": "Talk to live experts and get guidance." + }, + { + "text": "Start your {{trialPeriodHyphen}} trial for just {{trialPrice}}." + }, + { + "text": "Cancel anytime—just 24 hours before renewal." + } + ] + } + }, + "totalPrice": { + "couponContainer": { + "title": { + "text": "Coupon\nCode" + }, + "buttonText": "SOULMATE94" + }, + "priceContainer": { + "title": { + "text": "Total" + }, + "price": { + "text": "{{trialPrice}}" + }, + "oldPrice": { + "text": "{{billingPrice}}" + }, + "discount": { + "text": "{{discountPercent}}% discount applied" + } + } + }, + "paymentButtons": { + "buttons": [ + { + "text": "Pay", + "icon": "pay" + }, + { + "text": "Pay", + "icon": "google" + }, + { + "text": "Credit or debit card", + "icon": "card", + "primary": true + } + ] + }, + "moneyBackGuarantee": { + "title": { + "text": "30-DAY MONEY-BACK GUARANTEE" + }, + "text": { + "text": "If you don't receive your soulmate sketch, we'll refund your money!" + } + }, + "policy": { + "text": { + "text": "By clicking Continue, you agree to our Terms of Use & Service and Privacy Policy. You also acknowledge that your 1 week introductory plan to WitLab, billed at $1.00, will automatically renew at $14.50 every 1 week unless canceled before the end of the trial period." + } + }, + "usersPortraits": { + "title": { + "text": "Our Users' Soulmate Portraits" + }, + "images": [ + { + "src": "/trial-payment/users-portraits/1.jpg" + }, + { + "src": "/trial-payment/users-portraits/2.jpg" + }, + { + "src": "/trial-payment/users-portraits/3.jpg" + }, + { + "src": "/trial-payment/users-portraits/4.jpg" + }, + { + "src": "/trial-payment/users-portraits/5.jpg" + }, + { + "src": "/trial-payment/users-portraits/6.jpg" + } + ], + "buttonText": "Get My Soulmate Sketch" + }, + "joinedTodayWithAvatars": { + "count": { + "text": "954" + }, + "text": { + "text": "people joined today" + }, + "avatars": { + "images": [ + { + "src": "/trial-payment/avatars/1.jpg" + }, + { + "src": "/trial-payment/avatars/2.jpg" + }, + { + "src": "/trial-payment/avatars/3.jpg" + }, + { + "src": "/trial-payment/avatars/4.jpg" + }, + { + "src": "/trial-payment/avatars/5.jpg" + } + ] + } + }, + "progressToSeeSoulmate": { + "title": { + "text": "See Your Soulmate – Just One Step Away" + }, + "progress": { + "value": 92 + }, + "leftText": { + "text": "Step 2 of 5" + }, + "rightText": { + "text": "99% Complete" + } + }, + "stepsToSeeSoulmate": { + "steps": [ + { + "title": { + "text": "Questions Answered" + }, + "description": { + "text": "You've provided all the necessary information." + }, + "icon": "questions", + "isActive": true + }, + { + "title": { + "text": "Profile Analysis" + }, + "description": { + "text": "Creating your perfect soulmate profile." + }, + "icon": "profile", + "isActive": true + }, + { + "title": { + "text": "Sketch Creation" + }, + "description": { + "text": "Your personalized soulmate sketch will be created." + }, + "icon": "sketch" + }, + { + "title": { + "text": "Astrological Insights" + }, + "description": { + "text": "Unique astrology-based recommendations." + }, + "icon": "astro" + }, + { + "title": { + "text": "Personalized chat with an expert" + }, + "description": { + "text": "Individual guidance." + }, + "icon": "chat" + } + ], + "buttonText": "Show Me My Soulmate" + }, + "reviews": { + "title": { + "text": "Loved and Trusted Worldwide" + }, + "items": [ + { + "name": { + "text": "Jennifer Wilson 🇺🇸" + }, + "text": { + "text": "**“I saw my mistakes… and found my husband.”**\nThe portrait instantly struck me — I had this feeling like I’d seen him somewhere before. But the real turning point came after the guide: I finally understood why I kept choosing the “wrong” people. And the most amazing part — soon after, I met a man who turned out to be the exact image from that portrait. He’s my husband now, and when we compared the drawing to his photo, the resemblance was just wow." + }, + "avatar": { + "src": "/trial-payment/reviews/avatars/1.jpg" + }, + "portrait": { + "src": "/trial-payment/reviews/portraits/1.jpg" + }, + "photo": { + "src": "/trial-payment/reviews/photos/1.jpg" + }, + "rating": 5, + "date": { + "text": "1 day ago" + } + }, + { + "name": { + "text": "Amanda Davis 🇨🇦" + }, + "text": { + "text": "**“I understood my partner better in one evening than in several years.”**\nI took the test just for fun — the portrait surprised us. But the real breakthrough came when I read the guide about my other half. It had spot-on insights about how we can support each other. The price was nothing, but the value was huge — now we have fewer misunderstandings and so much more warmth." + }, + "avatar": { + "src": "/trial-payment/reviews/avatars/2.jpg" + }, + "portrait": { + "src": "/trial-payment/reviews/portraits/2.jpg" + }, + "photo": { + "src": "/trial-payment/reviews/photos/2.jpg" + }, + "rating": 5, + "date": { + "text": "4 days ago" + } + }, + { + "name": { + "text": "Michael Johnson 🇬🇧" + }, + "text": { + "text": "**“I saw her face — and got goosebumps.”**\nWhen I got my test results and saw the portrait, I literally froze. It was the exact girl I’d started dating a couple of weeks earlier. And the guide described perfectly why we’re drawn to each other. Honestly, I didn’t expect such a match." + }, + "avatar": { + "src": "/trial-payment/reviews/avatars/3.jpg" + }, + "portrait": { + "src": "/trial-payment/reviews/portraits/3.jpg" + }, + "photo": { + "src": "/trial-payment/reviews/photos/3.jpg" + }, + "rating": 5, + "date": { + "text": "1 week ago" + } + } + ] + }, + "stillHaveQuestions": { + "title": { + "text": "Still have questions? We're here to help!" + }, + "actionButtonText": "Get My Soulmate Sketch", + "contactButtonText": "Contact Support" + }, + "commonQuestions": { + "title": { + "text": "Common Questions" + }, + "items": [ + { + "question": "When will I receive my sketch?", + "answer": "Your personalized soulmate sketch will be delivered within 24-48 hours after completing your order. You'll receive an email notification when it's ready for viewing in your account." + }, + { + "question": "How do I cancel my subscription?", + "answer": "You can cancel anytime from your account settings. Make sure to cancel at least 24 hours before the renewal date to avoid being charged." + }, + { + "question": "How accurate are the readings?", + "answer": "Our readings are based on a combination of your answers and advanced pattern analysis. While they provide valuable insights, they are intended for guidance and entertainment purposes." + }, + { + "question": "Is my data secure and private?", + "answer": "Yes. We follow strict data protection standards. Your data is encrypted and never shared with third parties without your consent." + } + ] + }, + "footer": { + "title": { + "text": "WIT LAB ©" + }, + "contacts": { + "title": { + "text": "CONTACTS" + }, + "email": { + "href": "info@witlab.us", + "text": "info@witlab.us" + }, + "address": { + "text": "Wit Lab 2108 N ST STE N SACRAMENTO, CA95816, US" + } + }, + "legal": { + "title": { + "text": "LEGAL" + }, + "links": [ + { + "href": "https://witlab.com/terms", + "text": "Terms of Service" + }, + { + "href": "https://witlab.com/privacy", + "text": "Privacy Policy" + }, + { + "href": "https://witlab.com/refund", + "text": "Refund Policy" + } + ], + "copyright": { + "text": "Copyright © 2025 Wit Lab™. All rights reserved. All trademarks referenced herein are the properties of their respective owners." + } + }, + "paymentMethods": { + "title": { + "text": "PAYMENT METHODS" + }, + "methods": [ + { + "src": "/trial-payment/payment-methods/visa.svg", + "alt": "visa" + }, + { + "src": "/trial-payment/payment-methods/mastercard.svg", + "alt": "mastercard" + }, + { + "src": "/trial-payment/payment-methods/discover.svg", + "alt": "discover" + }, + { + "src": "/trial-payment/payment-methods/apple.svg", + "alt": "apple" + }, + { + "src": "/trial-payment/payment-methods/google.svg", + "alt": "google" + }, + { + "src": "/trial-payment/payment-methods/paypal.svg", + "alt": "paypal" + } + ] + } + } + }, { "id": "specialoffer", "template": "specialOffer", "header": { "showBackButton": false, + "showProgress": true, "show": true }, "title": { @@ -2859,6 +3796,10 @@ export const BAKED_FUNNELS: Record = { "cornerRadius": "3xl", "showPrivacyTermsConsent": false }, + "navigation": { + "rules": [], + "isEndScreen": true + }, "variants": [], "text": { "title": { diff --git a/src/lib/funnel/mappers.tsx b/src/lib/funnel/mappers.tsx index e5ff26b..bcb94bc 100644 --- a/src/lib/funnel/mappers.tsx +++ b/src/lib/funnel/mappers.tsx @@ -150,6 +150,7 @@ interface BuildActionButtonOptions { defaultText?: string; disabled?: boolean; onClick: () => void; + onDisabledClick?: () => void; } export function buildActionButtonProps( @@ -195,6 +196,7 @@ export function buildBottomActionButtonProps( return { actionButtonProps, showGradientBlur: buttonDef?.showGradientBlur ?? true, // Градиент по умолчанию включен + onDisabledClick: options.onDisabledClick, }; } diff --git a/src/lib/funnel/screenRenderer.tsx b/src/lib/funnel/screenRenderer.tsx index 0962ac7..9a85806 100644 --- a/src/lib/funnel/screenRenderer.tsx +++ b/src/lib/funnel/screenRenderer.tsx @@ -13,6 +13,7 @@ import { SoulmatePortraitTemplate, TrialPaymentTemplate, SpecialOfferTemplate, + TrialChoiceTemplate, } from "@/components/funnel/templates"; import type { ListScreenDefinition, @@ -29,6 +30,7 @@ import type { FunnelDefinition, FunnelAnswers, SpecialOfferScreenDefinition, + TrialChoiceScreenDefinition, } from "@/lib/funnel/types"; export interface ScreenRenderProps { @@ -353,6 +355,31 @@ const TEMPLATE_REGISTRY: Record< /> ); }, + trialChoice: ({ + funnel, + screen, + onContinue, + canGoBack, + onBack, + screenProgress, + defaultTexts, + answers, + }) => { + const trialChoiceScreen = screen as TrialChoiceScreenDefinition; + + return ( + + ); + }, }; export function renderScreen(props: ScreenRenderProps): JSX.Element { diff --git a/src/lib/funnel/templateHelpers.ts b/src/lib/funnel/templateHelpers.ts index fe7f371..1585050 100644 --- a/src/lib/funnel/templateHelpers.ts +++ b/src/lib/funnel/templateHelpers.ts @@ -24,6 +24,7 @@ export interface ActionButtonConfig { defaultText: string; disabled: boolean; onClick: () => void; + onDisabledClick?: () => void; } /** diff --git a/src/lib/funnel/types.ts b/src/lib/funnel/types.ts index 8b97dae..18b13cb 100644 --- a/src/lib/funnel/types.ts +++ b/src/lib/funnel/types.ts @@ -557,6 +557,17 @@ export interface SpecialOfferScreenDefinition { variants?: ScreenVariantDefinition[]; } +export interface TrialChoiceScreenDefinition { + id: string; + template: "trialChoice"; + header?: HeaderDefinition; + title: TitleDefinition; + subtitle?: SubtitleDefinition; + bottomActionButton?: BottomActionButtonDefinition; + navigation?: NavigationDefinition; + variants?: ScreenVariantDefinition[]; +} + export type ScreenDefinition = | InfoScreenDefinition | DateScreenDefinition @@ -567,7 +578,8 @@ export type ScreenDefinition = | LoadersScreenDefinition | SoulmatePortraitScreenDefinition | TrialPaymentScreenDefinition - | SpecialOfferScreenDefinition; + | SpecialOfferScreenDefinition + | TrialChoiceScreenDefinition; export interface FunnelMetaDefinition { id: string; diff --git a/src/lib/models/Funnel.ts b/src/lib/models/Funnel.ts index eb2e4e1..393e252 100644 --- a/src/lib/models/Funnel.ts +++ b/src/lib/models/Funnel.ts @@ -96,6 +96,7 @@ const HeaderDefinitionSchema = new Schema( className: String, }, showBackButton: { type: Boolean, default: true }, + showProgress: { type: Boolean, default: true }, show: { type: Boolean, default: true }, }, { _id: false } @@ -184,6 +185,7 @@ const ScreenDefinitionSchema = new Schema( "soulmate", "trialPayment", "specialOffer", + "trialChoice", ], required: true, },