> ## 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="A customer dashboard with its own Connect buttons and an Ayrshare popup showing the Facebook hand-off screen" 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` was fetched earlier. Nothing async between the click and 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 的來源比對。任何頁面都可以向你的視窗傳送訊息，
      而來源是訊息中唯一無法偽造的部分。
    * **輪詢 `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 金鑰">
    直接模式中的 X 使用**你自己的** X Developer App 憑證，在建立工作階段時以
    `X-Twitter-OAuth1-Api-Key` 與 `X-Twitter-OAuth1-Api-Secret` 標頭提供給
    [建立 Link Session](/docs/apis/profiles/create-link-session)。

    為 `twitter` 或 `x` 建立、但**沒有**這些標頭的工作階段，會在彈出視窗開啟時被拒絕：你的
    使用者會被告知此連結不可用且不會看到任何表單，而你的頁面會收到帶 `message` 且**沒有
    `code`** 的 `connect:error`。這是刻意的。缺少的憑證是你的，不是你使用者的，而終端使用者
    絕不應被要求輸入你的 API 金鑰。

    對照 Bluesky：其 app password 是終端使用者自己的憑證——這一項連結頁面確實會收集，
    在彈出視窗內的表單中。
  </Accordion>

  <Accordion title="Facebook 在 Meta 登入之前多顯示一顆按鈕">
    `network: "facebook"` 會在彈出視窗中顯示一顆按鈕，Meta 自己的登入從該次點擊開啟——Meta
    要求其登入必須由承載其 SDK 的頁面內的點擊啟動。你的使用者要點兩次而不是一次；其餘沒有差別。

    當 Instagram **透過 Facebook 粉絲專頁**連結時——也就是工作階段帶有
    `instagramLinkMethod: "facebook"`，或你帳號的
    [Instagram Login](/docs/multiple-users/manage-user-profiles#instagram-login) 設定選擇該流程時——
    行為相同。使用直接的 Instagram Login 則沒有額外的按鈕。
  </Accordion>

  <Accordion title="Bluesky 與 Telegram 顯示頁面內容，而非轉址">
    兩者都不會把你的使用者送去網路登入。彈出視窗改為渲染內容：Bluesky 是 handle 與
    app password 的表單，Telegram 是一組要使用的代碼。無論哪種方式，結果事件都相同。

    X 不屬於這一組。工作階段帶有你的金鑰時，它不需要向使用者要求任何東西即可完成；沒有金鑰時
    則會被拒絕——見上文。
  </Accordion>

  <Accordion title="Telegram 在頻外完成">
    Telegram 顯示一組代碼而不做任何轉址，連結會在你的使用者使用該代碼時完成——那時彈出視窗
    已經不在了。沒有可以等待的瀏覽器事件，因此請輪詢
    [取得 Link Session](/docs/apis/profiles/get-link-session) 並觀察 `completedNetworks`。
  </Accordion>

  <Accordion title="Facebook 社團無法以這種方式連結">
    Facebook 社團不是連結目標，因此 `network: "fbg"` 在建立工作階段時會回傳 `code: 508`。

    WhatsApp **可以**在直接模式中使用。它會在彈出視窗中開啟 Meta 的 Embedded Signup，
    結果事件與其他網路相同。
  </Accordion>
</AccordionGroup>

## 原生應用程式

原生應用程式開啟同樣的 `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)**。在沒有 Max Pack 的情況下建立 connect 模式的工作階段
    會回傳 `code: 504`，無論請求的其他內容為何。
  </li>

  <li>
    每個工作階段都要有 **`origin`**。沒有允許清單，也沒有註冊步驟——你在每次呼叫時送出它。
    省略它會回傳 `code: 505`；不是 `https` 來源、自訂 scheme 或 `http://localhost` 的值會回傳
    `code: 506`。
  </li>

  <li>
    一個你的帳戶已啟用的 **`network`**。無法識別的名稱會回傳 `code: 508`；名稱有效但你的帳戶
    尚未啟用時會回傳 `code: 509`，你可以在
    [Social Networks](/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>
