From fcb2492118935d2a958453fcad0329c123439ec0 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 28 Jul 2026 14:53:58 +0800 Subject: [PATCH] feat(external-entry): define Facebook business links --- docs/external-entry/README.md | 129 ++++++----- docs/external-entry/links.md | 92 ++++---- .../facebook-external-entry-links.spec.ts | 201 ++++++++++++++++++ .../__tests__/external_entry.test.ts | 35 +++ 4 files changed, 336 insertions(+), 121 deletions(-) create mode 100644 e2e/specs/mock/chat/facebook-external-entry-links.spec.ts diff --git a/docs/external-entry/README.md b/docs/external-entry/README.md index 2e42c2a7..9284d8e9 100644 --- a/docs/external-entry/README.md +++ b/docs/external-entry/README.md @@ -1,11 +1,11 @@ # 外部入口接入说明 -外部应用通过 `/external-entry` 进入本应用。 +Facebook 等外部应用统一通过 `/external-entry` 进入本应用。入口先保存外部身份和短期业务意图,再跳转到对应角色页面并清理地址栏中的入口查询参数。 | 环境 | APP HOST | | --- | --- | -| 测试环境 | `frontend-test.banlv-ai.com` | -| 正式环境 | `cozsweet.com` | +| 预发环境 `pre` | `frontend-test.banlv-ai.com` | +| 正式环境 `production` | `cozsweet.com` | 入口格式: @@ -13,85 +13,78 @@ https:///external-entry?<参数> ``` -## 可选参数 +## 当前 Facebook 业务入口 -所有参数均为可选参数。 +当前正式投放固定为 4 类入口乘以 3 个角色,共 12 条链接。Facebook 业务名称与应用页面的对应关系如下: + +| Facebook 入口 | 参数组合 | 落地行为 | +| --- | --- | --- | +| 私密空间 | `target=chat` | 进入普通聊天页,不插入任何锁定内容。 | +| 语音 | `target=chat&mode=promotion&promotion_type=voice` | 进入聊天页,并插入一条待解锁语音。 | +| 图片包 | `target=private-zone` | 进入角色的 `private-zone` 图片空间。 | +| 帮忙买咖啡 | `target=tip` | 进入角色的咖啡打赏页。 | + +每条正式链接还必须显式传入: + +- `character=elio`、`character=maya` 或 `character=nayeli`; +- `psid=`,其中 `` 必须由 Facebook 发送端替换为当前用户经过 URL 编码的 Page-scoped User ID。 + +完整的 12 条正式发送模板见 [Facebook 12 条外部入口链接清单](./links.md)。 + +## 参数说明 | 参数 | 可选值或格式 | 用途 | | --- | --- | --- | -| `target` | `chat`、`tip`、`private-zone` | 指定进入的页面;不传或值无效时进入聊天页。历史值 `private-room` 兼容进入 `private-zone`,新链接不得继续使用。 | -| `character` | `elio`、`maya`、`nayeli` | 指定角色;不传或值无效时使用 Elio。 | -| `psid` | string | 传入 Facebook Page-scoped User ID。 | +| `target` | `chat`、`tip`、`private-zone` | 指定进入的页面;不传或值无效时进入聊天页。 | +| `character` | `elio`、`maya`、`nayeli` | 指定角色;不传或值无效时使用 Elio。正式投放链接必须显式传入。 | +| `psid` | string | Facebook Page-scoped User ID。Facebook 正式投放链接必须动态传入。 | | `mode` | `promotion` | 开启聊天促销模式。 | | `promotion_type` | `voice`、`image`、`private` | 指定促销消息类型,仅与 `mode=promotion` 一起使用。 | -`mode=promotion` 只有在 `target=chat` 且同时提供有效的 `promotion_type` 时生效。 +`mode=promotion` 只有在 `target=chat` 且同时提供有效的 `promotion_type` 时生效。当前 12 条正式投放链接只使用 `promotion_type=voice`;`image` 和 `private` 保留为兼容能力,不属于本次业务清单。 -标准私密空间参数是 `target=private-zone`。前端仅为已经发布的旧链接保留 `target=private-room` 兼容;`private_zone` 等其他拼写仍按无效目标处理并回退聊天页。 +## 四类发送模板 + +发送前必须把 `` 替换为角色值,把 `` 替换为当前 Facebook 用户的真实 PSID;不得把占位符原样发送给用户。 + +私密空间(普通聊天): + +```text +https:///external-entry?target=chat&character=&psid= +``` + +语音: + +```text +https:///external-entry?target=chat&character=&mode=promotion&promotion_type=voice&psid= +``` + +图片包: + +```text +https:///external-entry?target=private-zone&character=&psid= +``` + +帮忙买咖啡: + +```text +https:///external-entry?target=tip&character=&psid= +``` ## PSID 与登录状态流程 -PSID 保存、登录状态判断和 Facebook Identity 绑定逻辑见 [PSID 与登录状态流程图](./psid-login-flow.md)。 +`psid` 可以和任意有效入口参数组合使用。应用会先保存 PSID,再执行登录状态判断和 Facebook Identity 绑定。详细流程见 [PSID 与登录状态流程图](./psid-login-flow.md)。 -## 使用示例 +所有参数值都必须进行 URL 编码。入口中禁止传递登录 Token、Page Access Token、App Secret 或其他秘密。 -普通聊天: +## 兼容说明 -```text -https:///external-entry?target=chat -``` +- 历史参数 `target=private-room` 继续兼容进入 `private-zone`,但不得用于新投放链接。 +- `private_zone` 等其他拼写仍按无效目标处理并回退聊天页。 +- `promotion_type=image` 和 `promotion_type=private` 继续由现有代码支持,但不计入当前 12 条 Facebook 业务链接。 +- 无效或缺失的 `character` 继续回退 Elio;正式投放模板始终显式传入角色,不能依赖回退。 -进入 Maya 聊天: - -```text -https:///external-entry?target=chat&character=maya -``` - -携带 PSID 进入聊天,示例 PSID 为 `27511427698460020`: - -```text -https:///external-entry?target=chat&psid=27511427698460020 -``` - -语音促销聊天: - -```text -https:///external-entry?target=chat&mode=promotion&promotion_type=voice -``` - -图片促销聊天: - -```text -https:///external-entry?target=chat&mode=promotion&promotion_type=image -``` - -私密文本促销聊天: - -```text -https:///external-entry?target=chat&mode=promotion&promotion_type=private -``` - -咖啡打赏: - -```text -https:///external-entry?target=tip -``` - -私密空间: - -```text -https:///external-entry?target=private-zone -``` - -进入 Nayeli 私密空间: - -```text -https:///external-entry?target=private-zone&character=nayeli -``` - -`psid` 可以与任意 `target` 或聊天促销参数组合使用。参数值需要进行 URL 编码,不要在入口中传递登录 Token、Page Access Token 或 App Secret。 - -完整可点击示例: +## 调试与正式清单 - [本地调试链接清单](./debug-links.md) -- [测试环境和正式环境链接清单](./links.md) +- [Facebook 12 条外部入口链接清单](./links.md) diff --git a/docs/external-entry/links.md b/docs/external-entry/links.md index 54f3f8a4..c51a51a5 100644 --- a/docs/external-entry/links.md +++ b/docs/external-entry/links.md @@ -1,66 +1,52 @@ -# 外部入口链接清单 +# Facebook 12 条外部入口链接清单 -这份清单可以独立发送和直接点击。所有新链接统一使用标准参数 `target=private-zone`;历史参数 `target=private-room` 仅用于兼容已经发出的旧链接。 +本清单是 Facebook 正式投放的唯一业务链接清单,共 4 类入口乘以 3 个角色,合计 12 条模板。 -## 使用规则 +## 发送规则 -- `target=chat`:进入聊天页。 -- `target=tip`:进入对应角色的打赏页。 -- `target=private-zone`:进入对应角色的私密空间。 -- `character` 可使用 `elio`、`maya`、`nayeli`;不传或值无效时使用 Elio。 -- `mode=promotion` 只在 `target=chat` 且 `promotion_type` 为 `voice`、`image` 或 `private` 时生效。 -- PSID 示例统一使用 `27511427698460020`;不要在入口中放登录 Token、Page Access Token 或 App Secret。 +- 发送前必须把每条模板中的 `` 替换为当前用户经过 URL 编码的 Facebook Page-scoped User ID;不得把占位符原样发送。 +- 每条链接都显式传入 `character`,不得依赖缺省角色回退。 +- Facebook 业务名称“私密空间”进入普通聊天页;业务名称“图片包”才进入应用的 `private-zone`。 +- 不要在入口中传递登录 Token、Page Access Token、App Secret 或其他秘密。 +- 以下模板包含占位符,替换真实 PSID 后才能直接发送或点击。 -## 测试环境 +## 正式投放模板 -| 入口 | 可点击链接 | 预期落地页面 | -| --- | --- | --- | -| 普通聊天(Elio) | [打开普通聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat) | `/characters/elio/chat` | -| Maya 聊天 | [打开 Maya 聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat&character=maya) | `/characters/maya/chat` | -| Nayeli 聊天 | [打开 Nayeli 聊天](https://frontend-test.banlv-ai.com/external-entry?target=chat&character=nayeli) | `/characters/nayeli/chat` | -| 携带 PSID 的 Elio 聊天 | [打开 PSID 聊天示例](https://frontend-test.banlv-ai.com/external-entry?target=chat&psid=27511427698460020) | `/characters/elio/chat` | -| 语音促销 | [打开语音促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=voice) | `/characters/elio/chat`,保存语音促销意图 | -| 图片促销 | [打开图片促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=image) | `/characters/elio/chat`,保存图片促销意图 | -| 私密文本促销 | [打开私密文本促销](https://frontend-test.banlv-ai.com/external-entry?target=chat&mode=promotion&promotion_type=private) | `/characters/elio/chat`,保存私密文本促销意图 | -| Elio 咖啡打赏 | [打开 Elio 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=elio) | `/characters/elio/tip` | -| Maya 咖啡打赏 | [打开 Maya 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=maya) | `/characters/maya/tip` | -| Nayeli 咖啡打赏 | [打开 Nayeli 咖啡打赏](https://frontend-test.banlv-ai.com/external-entry?target=tip&character=nayeli) | `/characters/nayeli/tip` | -| Elio 私密空间 | [打开 Elio 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-zone&character=elio) | `/characters/elio/private-zone` | -| Maya 私密空间 | [打开 Maya 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-zone&character=maya) | `/characters/maya/private-zone` | -| Nayeli 私密空间 | [打开 Nayeli 私密空间](https://frontend-test.banlv-ai.com/external-entry?target=private-zone&character=nayeli) | `/characters/nayeli/private-zone` | +| 编号 | Facebook 入口 | 角色 | 发送模板 | 预期落地与页面状态 | +| --- | --- | --- | --- | --- | +| 1 | 私密空间 | Elio | `https://cozsweet.com/external-entry?target=chat&character=elio&psid=` | `/characters/elio/chat`,不显示促销锁卡 | +| 2 | 私密空间 | Maya | `https://cozsweet.com/external-entry?target=chat&character=maya&psid=` | `/characters/maya/chat`,不显示促销锁卡 | +| 3 | 私密空间 | Nayeli | `https://cozsweet.com/external-entry?target=chat&character=nayeli&psid=` | `/characters/nayeli/chat`,不显示促销锁卡 | +| 4 | 语音 | Elio | `https://cozsweet.com/external-entry?target=chat&character=elio&mode=promotion&promotion_type=voice&psid=` | `/characters/elio/chat`,显示一条锁定语音 | +| 5 | 语音 | Maya | `https://cozsweet.com/external-entry?target=chat&character=maya&mode=promotion&promotion_type=voice&psid=` | `/characters/maya/chat`,显示一条锁定语音 | +| 6 | 语音 | Nayeli | `https://cozsweet.com/external-entry?target=chat&character=nayeli&mode=promotion&promotion_type=voice&psid=` | `/characters/nayeli/chat`,显示一条锁定语音 | +| 7 | 图片包 | Elio | `https://cozsweet.com/external-entry?target=private-zone&character=elio&psid=` | `/characters/elio/private-zone` | +| 8 | 图片包 | Maya | `https://cozsweet.com/external-entry?target=private-zone&character=maya&psid=` | `/characters/maya/private-zone` | +| 9 | 图片包 | Nayeli | `https://cozsweet.com/external-entry?target=private-zone&character=nayeli&psid=` | `/characters/nayeli/private-zone` | +| 10 | 帮忙买咖啡 | Elio | `https://cozsweet.com/external-entry?target=tip&character=elio&psid=` | `/characters/elio/tip` | +| 11 | 帮忙买咖啡 | Maya | `https://cozsweet.com/external-entry?target=tip&character=maya&psid=` | `/characters/maya/tip` | +| 12 | 帮忙买咖啡 | Nayeli | `https://cozsweet.com/external-entry?target=tip&character=nayeli&psid=` | `/characters/nayeli/tip` | -## 旧链接兼容验证 +## 预发验证 -以下链接仅用于验证已经发出的 `private-room` 旧链接仍能正确进入私密空间。新投放链接不要再使用这些地址。 +预发验证不维护第二套业务清单。将上述 12 条模板的主机统一替换为 `frontend-test.banlv-ai.com`,其余路径和参数保持不变: -| 旧入口 | 可点击链接 | 预期落地页面 | -| --- | --- | --- | -| Elio 旧私密空间 | [验证 Elio 旧链接](https://frontend-test.banlv-ai.com/external-entry?target=private-room&character=elio) | `/characters/elio/private-zone` | -| Maya 旧私密空间 | [验证 Maya 旧链接](https://frontend-test.banlv-ai.com/external-entry?target=private-room&character=maya) | `/characters/maya/private-zone` | -| Nayeli 旧私密空间 | [验证 Nayeli 旧链接](https://frontend-test.banlv-ai.com/external-entry?target=private-room&character=nayeli) | `/characters/nayeli/private-zone` | +```text +https://frontend-test.banlv-ai.com/external-entry?<与正式模板相同的查询参数> +``` -## 正式环境 - -正式环境仅列标准链接,不再传播 `private-room` 旧参数。 - -| 入口 | 可点击链接 | 预期落地页面 | -| --- | --- | --- | -| 普通聊天(Elio) | [打开普通聊天](https://cozsweet.com/external-entry?target=chat) | `/characters/elio/chat` | -| Maya 聊天 | [打开 Maya 聊天](https://cozsweet.com/external-entry?target=chat&character=maya) | `/characters/maya/chat` | -| Nayeli 聊天 | [打开 Nayeli 聊天](https://cozsweet.com/external-entry?target=chat&character=nayeli) | `/characters/nayeli/chat` | -| 携带 PSID 的 Elio 聊天 | [打开 PSID 聊天示例](https://cozsweet.com/external-entry?target=chat&psid=27511427698460020) | `/characters/elio/chat` | -| 语音促销 | [打开语音促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=voice) | `/characters/elio/chat`,保存语音促销意图 | -| 图片促销 | [打开图片促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=image) | `/characters/elio/chat`,保存图片促销意图 | -| 私密文本促销 | [打开私密文本促销](https://cozsweet.com/external-entry?target=chat&mode=promotion&promotion_type=private) | `/characters/elio/chat`,保存私密文本促销意图 | -| Elio 咖啡打赏 | [打开 Elio 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=elio) | `/characters/elio/tip` | -| Maya 咖啡打赏 | [打开 Maya 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=maya) | `/characters/maya/tip` | -| Nayeli 咖啡打赏 | [打开 Nayeli 咖啡打赏](https://cozsweet.com/external-entry?target=tip&character=nayeli) | `/characters/nayeli/tip` | -| Elio 私密空间 | [打开 Elio 私密空间](https://cozsweet.com/external-entry?target=private-zone&character=elio) | `/characters/elio/private-zone` | -| Maya 私密空间 | [打开 Maya 私密空间](https://cozsweet.com/external-entry?target=private-zone&character=maya) | `/characters/maya/private-zone` | -| Nayeli 私密空间 | [打开 Nayeli 私密空间](https://cozsweet.com/external-entry?target=private-zone&character=nayeli) | `/characters/nayeli/private-zone` | +预发环境是 `pre`,不是正式环境。预发验证通过不代表已经更新 `cozsweet.com` 或获得生产发布批准。 ## 验收判断 -打开入口后,浏览器地址最终应与“预期落地页面”一致。普通聊天和私密空间必须分别落在 `/chat` 与 `/private-zone`,不能再进入同一个页面。 +- 最终浏览器地址必须是表格中的预期页面,且不再保留 `/external-entry` 的查询参数。 +- `` 替换后的真实值必须由应用保存。 +- “私密空间”只进入普通聊天,不得出现锁定语音、锁定图片或私密文本促销卡。 +- “语音”进入聊天后必须只注入一条锁定语音。 +- “图片包”和“帮忙买咖啡”必须分别进入对应角色的 `private-zone` 与 `tip`,不能串到其他角色。 -如需在其他入口携带 PSID,在已有查询参数的链接末尾追加 `&psid=27511427698460020`。 +## 不属于当前 12 条清单的兼容能力 + +- 历史 `target=private-room` 继续兼容,但不得创建新的投放链接。 +- `promotion_type=image`、`promotion_type=private` 和独立 PSID 示例不属于当前正式业务清单。 +- 兼容能力的保留不改变本页“只有 12 条正式投放模板”的口径。 diff --git a/e2e/specs/mock/chat/facebook-external-entry-links.spec.ts b/e2e/specs/mock/chat/facebook-external-entry-links.spec.ts new file mode 100644 index 00000000..e2e761d6 --- /dev/null +++ b/e2e/specs/mock/chat/facebook-external-entry-links.spec.ts @@ -0,0 +1,201 @@ +import { expect, test, type Page } from "@playwright/test"; + +import { mockCoreApis } from "@e2e/fixtures/api-mocks"; +import { apiEnvelope } from "@e2e/fixtures/data/common"; +import { clearBrowserState } from "@e2e/fixtures/test-helpers"; + +const testPsid = "e2e-facebook-psid-27511427698460020"; + +const characters = [ + { + id: "elio", + slug: "elio", + displayName: "Elio Silvestri", + shortName: "Elio", + }, + { + id: "maya-tan", + slug: "maya", + displayName: "Maya Tan", + shortName: "Maya", + }, + { + id: "nayeli-cervantes", + slug: "nayeli", + displayName: "Nayeli Cervantes", + shortName: "Nayeli", + }, +] as const; + +type Character = (typeof characters)[number]; +type EntryKind = "private-space" | "voice" | "image-pack" | "coffee"; + +interface FacebookEntryCase { + character: Character; + kind: EntryKind; + target: "chat" | "private-zone" | "tip"; + expectedPath: string; + promotionType?: "voice"; +} + +const entryCases: FacebookEntryCase[] = characters.flatMap((character) => [ + { + character, + kind: "private-space", + target: "chat", + expectedPath: `/characters/${character.slug}/chat`, + }, + { + character, + kind: "voice", + target: "chat", + expectedPath: `/characters/${character.slug}/chat`, + promotionType: "voice", + }, + { + character, + kind: "image-pack", + target: "private-zone", + expectedPath: `/characters/${character.slug}/private-zone`, + }, + { + character, + kind: "coffee", + target: "tip", + expectedPath: `/characters/${character.slug}/tip`, + }, +]); + +test.beforeEach(async ({ baseURL, context, page }) => { + await clearBrowserState(context, page, baseURL); + await mockCoreApis(page); + await registerEntryPageMocks(page); +}); + +for (const entryCase of entryCases) { + test(`${entryCase.character.displayName} opens Facebook ${entryCase.kind} entry`, async ({ + page, + }) => { + await page.goto(buildEntryUrl(entryCase)); + + await expect(page).toHaveURL(entryCase.expectedPath, { timeout: 20_000 }); + await expect + .poll(() => + page.evaluate(() => localStorage.getItem("cozsweet:psid")), + ) + .toBe(testPsid); + + if (entryCase.kind === "private-space") { + await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Unlock voice message" }), + ).toHaveCount(0); + return; + } + + if (entryCase.kind === "voice") { + await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Unlock voice message" }), + ).toHaveCount(1); + return; + } + + if (entryCase.kind === "image-pack") { + await expect( + page.getByRole("heading", { name: "Private albums" }), + ).toBeVisible(); + await expect( + page.getByText(entryCase.character.displayName).last(), + ).toBeVisible(); + return; + } + + await expect( + page.getByRole("heading", { + name: `Buy ${entryCase.character.shortName} a coffee`, + }), + ).toBeVisible(); + }); +} + +function buildEntryUrl(entryCase: FacebookEntryCase): string { + const params = new URLSearchParams({ + target: entryCase.target, + character: entryCase.character.slug, + psid: testPsid, + }); + if (entryCase.promotionType) { + params.set("mode", "promotion"); + params.set("promotion_type", entryCase.promotionType); + } + return `/external-entry?${params.toString()}`; +} + +async function registerEntryPageMocks(page: Page): Promise { + await page.route("**/api/private-zone/posts**", async (route) => { + const url = new URL(route.request().url()); + const characterId = url.searchParams.get("characterId") ?? "elio"; + await route.fulfill({ + json: apiEnvelope({ + characterId, + items: [], + nextCursor: null, + hasMore: false, + creditBalance: 1000, + currency: "credits", + }), + }); + }); + + await page.route("**/api/private-zone/albums**", async (route) => { + await route.fulfill({ + json: apiEnvelope({ + creditBalance: 1000, + pendingImageCount: 0, + hasIncompleteContent: false, + items: [], + }), + }); + }); + + await page.route("**/api/payment/gift-products**", async (route) => { + const url = new URL(route.request().url()); + const characterId = url.searchParams.get("characterId") ?? "elio"; + const planPrefix = + characterId === "elio" + ? "tip" + : `tip_${characterId.replaceAll("-", "_")}`; + await route.fulfill({ + json: apiEnvelope({ + characterId, + categories: [ + { + category: "coffee", + name: "Coffee", + productCount: 1, + imageUrl: null, + }, + ], + plans: [ + { + planId: `${planPrefix}_coffee_usd_4_99`, + planName: "Small Coffee", + orderType: "tip", + tipType: "coffee_small", + category: "coffee", + characterId, + description: "A character-scoped coffee gift", + imageUrl: null, + amountCents: 499, + currency: "USD", + autoRenew: false, + isFirstRechargeOffer: false, + firstRechargeDiscountPercent: 0, + promotionType: null, + }, + ], + }), + }); + }); +} diff --git a/src/lib/navigation/__tests__/external_entry.test.ts b/src/lib/navigation/__tests__/external_entry.test.ts index 8baeb76e..9d98e7ba 100644 --- a/src/lib/navigation/__tests__/external_entry.test.ts +++ b/src/lib/navigation/__tests__/external_entry.test.ts @@ -9,6 +9,21 @@ import { shouldBindExternalFacebookIdentity, } from "../external_entry"; +const facebookBusinessEntryCases = [ + ["private-space", "elio", "chat", null, null, getCharacterRoutes("elio").chat, null], + ["private-space", "maya", "chat", null, null, getCharacterRoutes("maya").chat, null], + ["private-space", "nayeli", "chat", null, null, getCharacterRoutes("nayeli").chat, null], + ["voice", "elio", "chat", "promotion", "voice", getCharacterRoutes("elio").chat, "voice"], + ["voice", "maya", "chat", "promotion", "voice", getCharacterRoutes("maya").chat, "voice"], + ["voice", "nayeli", "chat", "promotion", "voice", getCharacterRoutes("nayeli").chat, "voice"], + ["image-pack", "elio", "private-zone", null, null, getCharacterRoutes("elio").privateZone, null], + ["image-pack", "maya", "private-zone", null, null, getCharacterRoutes("maya").privateZone, null], + ["image-pack", "nayeli", "private-zone", null, null, getCharacterRoutes("nayeli").privateZone, null], + ["coffee", "elio", "tip", null, null, getCharacterRoutes("elio").tip, null], + ["coffee", "maya", "tip", null, null, getCharacterRoutes("maya").tip, null], + ["coffee", "nayeli", "tip", null, null, getCharacterRoutes("nayeli").tip, null], +] as const; + describe("external entry navigation", () => { it("defaults to chat", () => { expect(resolveExternalEntryTarget({})).toBe(ROUTES.chat); @@ -62,6 +77,26 @@ describe("external entry navigation", () => { ).toBe(getCharacterRoutes("elio").privateZone); }); + it.each(facebookBusinessEntryCases)( + "routes Facebook business entry %s for %s", + ( + _entry, + character, + target, + mode, + promotionType, + expectedDestination, + expectedPromotionType, + ) => { + expect(resolveExternalEntryDestination({ target, character })).toBe( + expectedDestination, + ); + expect( + resolveExternalEntryPromotionType({ mode, promotionType }), + ).toBe(expectedPromotionType); + }, + ); + it.each([ ["chat", "elio", getCharacterRoutes("elio").chat], ["chat", "maya", getCharacterRoutes("maya").chat],