> ## 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.

# 关联完成事件

> 在用户关联账号的那一刻收到通知，而不是靠轮询。

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={false} />

当你在弹窗中打开关联页面时，你自己的页面可以监听其中发生的事情：你的用户连接了 Reddit、他们
中途退出了、连接失败了。每种结果都对应一个事件，因此你的 UI 会在事情发生的那一刻更新，而不是
等定时器。

在创建链接时设置 [`origin`](/docs/apis/profiles/create-link-session)，在弹窗中打开返回的 `url`，
然后监听即可。此外别无所需，而未带 `origin` 创建的链接没有任何变化——行为与以往完全一致。

<Note>
  **在使用[嵌入式小组件](/docs/multiple-users/connect-widget)？这些已经为你接好了。** 脚本会接收这些
  事件，去掉 `connect:` 前缀后交给 `connect.on("success", …)`——无需编写 `message` 监听器、
  origin 检查或 `popup.closed` 轮询。本页是底层的线协议（wire protocol），当**你**拥有窗口时
  ——直连模式，或你自己打开的弹窗——就以本页为准来构建。
</Note>

<Note>
  事件只发送到你在链接上设置的确切 `origin`，且只发送给打开弹窗的窗口。尽管如此，请始终在
  监听器中检查 `event.origin`：任何页面都可以向你的窗口发送消息，而 origin 是消息中唯一无法
  伪造的部分。
</Note>

## 事件列表

发送到你页面的每条消息都是形如 `{ source: "ayrshare", version: 1, event, ... }` 的对象，并且
只要事件与某个网络相关就会携带 `network`。有两个事件不携带：来自托管关联页面的
`connect:closed`——在那里它表示你的用户按下了 Done，而不是某次连接结束——以及小组件的
`connect:ready`（见下文）。由你自己代码合成的结果——被拦截的弹窗，或你的用户关闭的窗口——不是
来自我们，只携带你赋予它们的内容。

<Note>
  [嵌入式小组件](/docs/multiple-users/connect-widget)在一个事件上有所不同。它的 `ready` 宣告某个插槽
  已就绪，而与网络无关，因此不携带 `network`——而弹窗的 `ready` 会标明它为哪个网络打开。如果你
  用一个监听器同时处理两种界面，请在 `ready` 上防御性地读取 `network`。

  小组件还会新增一个此界面永不发送的 `cancelled` reason：`superseded`，即第二次 `popup()` 调用
  替换了仍在进行中的尝试。这里只有一个窗口、一个结果，没有可被替换的东西。
</Note>

| 事件                  | 额外字段                    | 发送时机                                                                                  |
| ------------------- | ----------------------- | ------------------------------------------------------------------------------------- |
| `connect:ready`     |                         | 页面已加载且链接已通过检查。                                                                        |
| `connect:started`   |                         | 你的用户已被送往社交网络。                                                                         |
| `connect:selection` | `step`                  | 屏幕上出现了选择器或表单——你的用户有事要做。                                                               |
| `connect:success`   | `refId`、`displayName`   | 账号已关联**且已保存**。`refId` 是账号被连接到的 User Profile。`displayName` 是账号名称，我们尚未获得时省略。            |
| `connect:error`     | `message`，以及存在时的 `code` | 连接失败。`code` 与[错误参考](/docs/errors/overview)一致；失败没有已收录错误码时不存在，你自己代码片段合成的任何结果（例如被拦截的弹窗）上也不存在。 |
| `connect:cancelled` | `reason`                | 你的用户中途退出。`reason` 为 `popupClosed`、`scopesDenied` 或 `userCancelled`。                   |
| `connect:closed`    |                         | 弹窗即将自行关闭。来自托管关联页面时不携带 `network`，在那里它表示你的用户按下了 Done，而不是某次连接结束。                         |

每次连接恰好到达 `connect:success`、`connect:error` 或 `connect:cancelled` 之一。
`connect:closed` 不在其中——它随后到达，告诉你弹窗是有意关闭的。

`connect:success` 只在账号保存之后发送，因此紧随其后的
[`GET /user`](/docs/apis/user/profile-details) 已能显示已连接的账号。

<Note>
  `connect:error` 之后弹窗保持打开，让你的用户能读到出了什么问题。`connect:success` 和
  `connect:cancelled` 之后它会自行关闭。调试期间可在 URL 后加 `&autoClose=false`，让它在任何
  情况下都保持打开。
</Note>

## 监听

这段代码做了两件容易被漏掉的事。它检查 `event.origin`，并留意用户手动关闭的弹窗——已关闭的
窗口什么也发不出来，轮询是唯一的察觉方式。

```javascript theme={"system"}
function connectAccount(url) {
  // 从拿到的 URL 推导 origin，而不要硬编码：
  // 如果你的账户使用自己的关联域名，弹窗就运行在该域名上。
  const popupOrigin = new URL(url).origin;

  const popup = window.open(url, "ayrshare-connect", "width=600,height=800");
  if (!popup) {
    handleOutcome({ event: "connect:error", message: "The popup was blocked." });
    return;
  }

  // 在任何代码可能调用 `cleanup` 之前声明：否则过早到达的消息
  // 会在 `poll` 的暂时性死区中命中它并抛出异常。
  let poll;

  const cleanup = () => {
    clearInterval(poll);
    window.removeEventListener("message", onMessage);
  };

  const onMessage = event => {
    // 两个检查都要做，而不只是 origin：否则同一 origin 上的另一个
    // 标签页或框架发来的消息也会被你的处理器相信。
    if (event.origin !== popupOrigin || event.source !== popup) return;
    const message = event.data;
    if (!message || message.source !== "ayrshare") return;

    if (["connect:success", "connect:error", "connect:cancelled"].includes(message.event)) {
      // 用 `finally`，这样你自己的处理器抛出异常也不会让轮询继续运行——
      // 它稍后会看到已关闭的弹窗，并在你已有的结果之上再报告一次 `cancelled`。
      // 清理在 `connect:error` 之后同样重要，那时弹窗保持打开以便用户阅读。
      try {
        handleOutcome(message);
      } finally {
        cleanup();
      }
    }
  };
  window.addEventListener("message", onMessage);

  // 手动关闭的弹窗什么也不发送，所以要留意它。先等一下再下结论：
  // 弹窗在发送后会立即自行关闭，你第一次看到窗口消失时消息可能仍在途中。
  let closedAt = null;
  poll = setInterval(() => {
    if (!popup.closed) return;
    if (closedAt === null) {
      closedAt = Date.now();
      return;
    }
    if (Date.now() - closedAt < 750) return;

    cleanup();
    handleOutcome({ event: "connect:cancelled", reason: "popupClosed" });
  }, 500);
}
```

## 错误

`connect:error` 携带与 API 其余部分相同的错误码，因此你在这里看到的错误码含义与其他任何地方
一致。专属于此界面的一个：

| 代码    | 含义                                                                       |
| ----- | ------------------------------------------------------------------------ |
| `513` | 链接以单网络连接窗口的形式被打开，但它不是为此创建的。只有你自己拼装该 URL 时才会遇到——本端点返回的 `url` 总是与它创建的链接一致。 |

其余都是社交网络自身的失败，以该失败已有的错误码报告——例如 `322` 表示 Instagram 授权问题，
其中包括账号仍是 Personal 而非 Professional 的情况。

### 失效链接以不同方式到达

**已过期**、**已吊销**或**不存在**的链接会在页面得知向哪里发送事件之前就被拒绝，因此它无法发送
事件。你的用户会在屏幕上看到原因及其错误码，而你的页面会在他们关闭窗口时听到
`connect:cancelled`。

要区分这几种情况，请轮询[获取 Link Session](/docs/apis/profiles/get-link-session)：它权威地报告
`expired` 和 `revoked`，对不存在的链接返回 `502`。

## 如果你不想管理弹窗

两个选项，只有第二个会失去事件。

**让小组件拥有窗口。** [嵌入式小组件](/docs/multiple-users/connect-widget)会替你打开并监视它，并把
上面的每个事件交付给你的处理器。你仍会在账号保存的那一刻收到 `success`；只是不用编写那些管道
代码。它的框架还意味着大多数网络在到达网络自身登录之前根本不会打开弹窗。

**改为轮询。** 如果确实无法使用弹窗——服务器端渲染的应用，或在系统浏览器中打开链接的移动应用
——请轮询[获取 Link Session](/docs/apis/profiles/get-link-session)。它会在账号保存后立即报告
`completedAt` 和 `completedNetworks`，这与 `connect:success` 本应发送的时刻相同。Telegram 总是
以这种方式完成，因为它在带外完成，没有任何浏览器回调。
