> ## 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` 監聽器、
  不必檢查來源、不必輪詢 `popup.closed`。本頁是底層的線路協定，也是當**你**擁有視窗時所要
  依循的內容：直接模式，或你自行開啟的彈出視窗。
</Note>

<Note>
  事件只會傳送到你在連結上設定的確切 `origin`，且只傳給開啟彈出視窗的視窗。無論如何，
  請一律在監聽器中檢查 `event.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) {
  // Derive the origin from the URL you were given rather than hard-coding one:
  // if your account uses its own linking domain, the popup runs on that.
  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;
  }

  // Declared before anything can call `cleanup`: a message arriving early would
  // otherwise hit `poll` in its temporal dead zone and throw.
  let poll;

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

  const onMessage = event => {
    // Both checks, not just the origin: another tab or frame on the same origin
    // could otherwise post a message your handler would believe.
    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`, so your own handler throwing cannot leave the poll running —
      // it would later see the closed popup and report `cancelled` on top of the
      // outcome you already had. Cleanup also matters after `connect:error`,
      // where the popup stays open so your user can read it.
      try {
        handleOutcome(message);
      } finally {
        cleanup();
      }
    }
  };
  window.addEventListener("message", onMessage);

  // A hand-closed popup sends nothing, so watch for it. Wait a moment before
  // deciding: the popup closes itself right after sending, and the message can
  // still be in flight when you first see the window go.
  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 其他部分相同的錯誤碼，因此你在這裡看到的錯誤碼與它在其他任何地方的
意義相同。專屬於此介面的一個：

| Code  | 意義                                                                             |
| ----- | ------------------------------------------------------------------------------ |
| `513` | 連結被當成單一網路的 connect 視窗開啟，但並非為此建立。只有你自行組出該 URL 時才會遇到——此端點回傳的 `url` 一定與它所建立的連結相符。 |

其他任何錯誤都是社群網路自身的失敗，並以該失敗原本的錯誤碼回報——例如 Instagram 授權問題的
`322`，其中包括仍是個人帳號而非專業帳號的情況。

### 失效的連結以不同的方式呈現

**已過期**、**已撤銷**或**不存在**的連結會在頁面得知該把事件送到哪裡之前就被拒絕，因此無法送出
任何事件。你的使用者會在畫面上看到原因與其錯誤碼，而你的頁面會在他們關閉視窗時聽到
`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
一律以這種方式完成，因為它在頻外完成，完全沒有瀏覽器回呼。
