> ## Documentation Index
> Fetch the complete documentation index at: https://www.ayrshare.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 自建关联弹窗

> 直连模式（Direct Mode）——从你自己的按钮出发，一次关联一个网络，弹窗由你自己打开并监视。

export const PlansAvailable = ({plans = [], maxPackRequired}) => {
  let displayPlans = plans;
  if (plans && plans.length === 1) {
    const lowerCasePlan = plans[0].toLowerCase();
    if (lowerCasePlan === "business") {
      displayPlans = ["Launch", "Business", "Enterprise"];
    } else if (lowerCasePlan === "premium") {
      displayPlans = ["Premium", "Launch", "Business", "Enterprise"];
    }
  }
  return <Note>
Available on {displayPlans.length === 1 ? "the " : ""}
{displayPlans.join(", ").replace(/\b\w/g, l => l.toUpperCase())}{" "}
{displayPlans.length > 1 ? "plans" : "plan"}.

{maxPackRequired && <span onClick={() => window.open('https://www.ayrshare.com/docs/additional/maxpack', '_self')} className="flex items-center mt-2 cursor-pointer">
 <span className="px-1.5 py-0.5 rounded text-sm" style={{
    backgroundColor: '#C264B6',
    color: 'white',
    fontSize: '12px'
  }}>
   Max Pack required
 </span>
</span>}
</Note>;
};

<PlansAvailable plans={["business"]} maxPackRequired={true} />

直连模式（Direct Mode）从你自己控制台中的按钮出发，**一次关联一个社交网络**。你为该网络创建一个
link session，在弹窗中打开它返回的 URL，你的页面就能听到发生了什么。你的用户永远不会看到列出
所有网络的页面，离开你应用的时间也不会超过网络自身登录所需的时间。

## 你需要哪种界面

三种形态，且前两种是同一个集成。本页是你自己构建的那一种。

| 界面                                                                                                  | 你的用户看到的                         | 白标程度                            | 适用场景                                            |
| --------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------- | ----------------------------------------------- |
| **小组件——嵌入式框架**（[mount](/docs/multiple-users/connect-widget#mount-a-slot)）                                | 我们的按钮内联显示在你自己的布局中；除网络自身的弹窗外没有弹窗 | **最强。** 你的页面、你的字体和配色，你的用户从不离开它  | 你的控制台为每个网络设有一行，希望关联就地完成。需要 Max Pack。            |
| **小组件——你自己的按钮**（[popup](/docs/multiple-users/connect-widget#your-own-button)）                            | 你的按钮，然后是网络对应的一个弹窗               | **强。** 弹窗是我们的，但十分短暂，并继承你的外观     | 你想使用自己的按钮和样式，且布局中不出现框架。需要 Max Pack。             |
| **托管关联页面**（[使用方法](/docs/multiple-users/api-integration-business#single-sign-on-with-jwt-authentication)） | 我们托管的页面，带有你的 logo、颜色和自定义 CSS    | **最弱。** 这是我们的页面，你的用户要离开你的页面去使用它 | 你想要一个可以直接发出去的链接，或通过邮件接入用户。无需构建任何东西，无需 Max Pack。 |

<Note>
  **直连模式就是第三行的弹窗，只是没有我们的脚本。** 如果你的页面可以加载脚本，
  [小组件](/docs/multiple-users/connect-widget)会替你完成本页的一切——它自己打开并监视弹窗，还能嵌入
  框架。当你的页面无法加载第三方脚本，或界面是原生应用而非浏览器时，直连模式才是你需要的。
</Note>

<Frame caption="你的按钮，你的页面。弹窗是用户唯一会看到的我们的东西，而且只在网络需要的时间内出现。">
  <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-popup.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=b68021d477703d97a5994ee85d299be6" alt="一个带有自有 Connect 按钮的客户控制台，以及显示 Facebook 交接屏幕的 Ayrshare 弹窗" width="2880" height="1317" data-path="images/multiple-users/connect-widget-popup.webp" />
</Frame>

## 你要构建的内容

四个步骤。第一步在你的服务器上，其余在你的页面中。

<Steps>
  <Step title="为单个网络创建会话">
    从你的后端调用[创建 Link Session](/docs/apis/profiles/create-link-session)，带上
    `mode: "connect"`、`network`，以及你页面运行所在的 `origin`。

    ```javascript Your backend theme={"system"}
    const response = await fetch("https://api.ayrshare.com/api/profiles/link-sessions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.AYRSHARE_API_KEY}`,
        "Profile-Key": profileKey,
      },
      body: JSON.stringify({
        mode: "connect",
        network: "reddit",
        origin: "https://app.example.com",
      }),
    });

    const { url } = await response.json();
    ```

    你会得到一个指向单网络连接页面的 `url`，而没有 `token`——token 在 URL 内部。请把整个 URL
    当作密码对待：它会让你的用户登录其 User Profile。

    <Warning>
      请在你的服务器上创建会话，绝不要在浏览器中。该调用需要你的 API Key。
    </Warning>
  </Step>

  <Step title="在点击处理器中同步打开它">
    弹窗必须由 `window.open` **在点击处理器本身之内**打开。浏览器只在仍在处理用户点击时才允许
    弹窗，而这个许可撑不过一次 `await`——因此先获取 URL 再在回调中打开，必然会被弹窗拦截。

    请在渲染按钮时或用户悬停按钮时获取 URL。到点击时你应该已经拿到它了。

    ```javascript Your page theme={"system"}
    // `url` 已提前获取。点击与 window.open 之间没有任何异步操作。
    button.addEventListener("click", () => {
      const popup = window.open(url, "ayrshare-connect", "width=600,height=800");
      if (!popup) {
        showError("Allow popups for this site to connect an account.");
        return;
      }
      listenForOutcome(popup, url);
    });
    ```
  </Step>

  <Step title="监听结果">
    因为你传入了 `origin`，弹窗会为其中发生的每件事向你的页面发送事件：`connect:success`、
    `connect:error`、`connect:cancelled`，以及其间的进度事件。每次连接恰好到达这三者之一。

    [关联完成事件](/docs/multiple-users/link-completion-events)提供完整的事件表和可直接复制的监听器
    ——上面的 `listenForOutcome` 就是那段代码。其中有两处很容易漏掉，且都会造成真实的 bug：

    * **检查 `event.origin`**，与你打开的 URL 的 origin 比对。任何页面都可以向你的窗口发送
      消息，而 origin 是消息中唯一无法伪造的部分。
    * **轮询 `popup.closed`**，并在得出结论前留出短暂的宽限期。用户手动关闭的弹窗什么也不会
      发送，而没有宽限期，一次成功的连接可能会被报告为已取消。
  </Step>

  <Step title="处理每种结局">
    | 结局                  | 含义                          | 应对                                                                |
    | ------------------- | --------------------------- | ----------------------------------------------------------------- |
    | `connect:success`   | 账号已关联**且已保存**               | 刷新该行。紧随其后的 [`GET /user`](/docs/apis/user/profile-details) 已能看到它。       |
    | `connect:error`     | 连接失败                        | 显示 `message`。失败有已收录错误码时 `code` 存在，否则不存在。                          |
    | `connect:cancelled` | 你的用户中途退出，或关闭了窗口             | 保持该行原样。`reason` 为 `popupClosed`、`scopesDenied` 或 `userCancelled`。 |
    | 什么也没到达              | 链接已过期、已吊销或不存在，页面从未得知向哪里发送事件 | 轮询[获取 Link Session](/docs/apis/profiles/get-link-session)，它会权威地报告这些状态。 |
  </Step>
</Steps>

<Tip>
  构建期间在 URL 后加上 `&autoClose=false`。这样弹窗在每种结局之后都保持打开而不是自行关闭，
  你就能读到它显示的内容。
</Tip>

## 各网络注意事项

大多数网络就是一个弹窗，仅此而已：你的用户点击、在网络上授权、弹窗关闭。以下是构建前值得了解的
例外。

<AccordionGroup>
  <Accordion title="X 需要你自己的 API Key">
    直连模式下的 X 使用**你的** X Developer App 凭据，在创建会话时通过
    [创建 Link Session](/docs/apis/profiles/create-link-session) 的 `X-Twitter-OAuth1-Api-Key` 和
    `X-Twitter-OAuth1-Api-Secret` header 提供。

    **不带**这些 header 为 `twitter` 或 `x` 创建的会话会在弹窗打开时被拒绝：你的用户被告知该
    连接不可用且看不到任何表单，你的页面收到带 `message` 且**无 `code`** 的 `connect:error`。
    这是有意的。缺失的凭据是你的，不是你用户的，绝不能要求终端用户输入你的 API Key。

    对比 Bluesky：应用密码（app password）是终端用户自己的凭据——连接页面确实会收集它，在弹窗
    内的表单中。
  </Accordion>

  <Accordion title="Facebook 在 Meta 登录前显示一个按钮">
    `network: "facebook"` 会在弹窗中显示一个按钮，Meta 自己的登录由那次点击打开——Meta 要求其
    登录必须由托管其 SDK 的页面内的点击发起。你的用户点击两次而不是一次；其余没有任何不同。

    当 Instagram **通过 Facebook Page** 关联时行为相同——即会话带有
    `instagramLinkMethod: "facebook"`，或你账户的
    [Instagram Login](/docs/multiple-users/manage-user-profiles#instagram-login) 设置选择了该流程时。
    使用直接 Instagram Login 则没有额外按钮。
  </Accordion>

  <Accordion title="Bluesky 和 Telegram 显示页面内容，而非跳转">
    两者都不会把你的用户送往网络登录页。弹窗改为渲染内容：Bluesky 是句柄和应用密码表单，
    Telegram 是一个待使用的验证码。结果事件在两种情况下都相同。

    X 不属于这一组。会话带上你的 Key 时，它无需向你的用户询问任何内容即可完成；不带时则被拒绝
    ——见上文。
  </Accordion>

  <Accordion title="Telegram 在带外完成">
    Telegram 显示一个验证码而不跳转到任何地方，连接在你的用户使用该验证码时完成——那时弹窗已经
    不在了。没有可等待的浏览器事件，因此请轮询[获取 Link Session](/docs/apis/profiles/get-link-session)
    并关注 `completedNetworks`。
  </Accordion>

  <Accordion title="Facebook Groups 无法通过这种方式连接">
    Facebook Groups 不是关联目标，因此 `network: "fbg"` 在创建会话时返回 `code: 508`。

    WhatsApp 在直连模式下**可用**。它会在弹窗中打开 Meta 的 Embedded Signup，结果事件与其他
    任何网络相同。
  </Accordion>
</AccordionGroup>

<h2 id="native-apps">
  原生应用
</h2>

原生应用在**系统浏览器**中打开同一个 `url`，并通过轮询
[获取 Link Session](/docs/apis/profiles/get-link-session) 得知结果。将 `origin` 设置为你的自定义
scheme（`myapp://connected`），让页面有办法回到你的应用；自定义 scheme 无法接收事件，因为没有
可供发送的浏览器窗口。

* **iOS** —— `ASWebAuthenticationSession` 或 `SFSafariViewController`。
* **Android** —— Chrome Custom Tabs。

<Warning>
  **绝不要在嵌入式 webview 中打开链接 URL**（`WKWebView`、`UIWebView`、Android `WebView`）。
  社交网络拒绝在其中进行身份验证：Google 会以 `disallowed_useragent` 拒绝登录，Meta 则直接
  屏蔽。你的用户看到的是网络自己的错误页面，而不是我们的，你这一侧无论怎么改都无济于事。上面的
  系统浏览器组件正是为此而存在，并能让用户留在你的应用内。
</Warning>

## 直连模式的要求

<ul className="custom-bullets">
  <li>
    **[Max Pack](/docs/additional/maxpack)**。没有它创建 connect 模式的会话会返回 `code: 504`，
    无论请求中还写了什么。
  </li>

  <li>
    每个会话都要有 **`origin`**。没有允许列表，也没有注册步骤——每次调用时发送即可。省略它返回
    `code: 505`；不是 `https` origin、自定义 scheme 或 `http://localhost` 的值返回 `code: 506`。
  </li>

  <li>
    一个你的账户已启用的 **`network`**。无法识别的名称返回 `code: 508`；能识别但你的账户未启用
    的返回 `code: 509`，你可以在
    [社交网络](/docs/multiple-users/manage-user-profiles#set-social-networks-access)页面自行修复。
  </li>

  <li>
    **不要有** `allowedSocial`。它不能与 `network` 组合（`code: 507`）——单网络会话本身就是
    自己的允许列表。
  </li>
</ul>

上述每一项都收录在 [Link Session 错误](/docs/errors/errors-ayrshare#link-session-errors)参考中，
包含 API 返回的消息。

## 后续步骤

<Card title="关联完成事件" icon="tower-broadcast" href="/docs/multiple-users/link-completion-events" horizontal>
  弹窗发送的每个事件，以及接收它们的监听器。
</Card>

## 相关内容

<Card title="创建 Link Session" icon="link" href="/docs/apis/profiles/create-link-session#connect-mode" horizontal>
  `mode`、`origin` 和 `network` 参数，以及响应形态。
</Card>

<Card title="获取 Link Session" icon="magnifying-glass" href="/docs/apis/profiles/get-link-session" horizontal>
  当你无法使用弹窗时，轮询获取完成情况。
</Card>
