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

# Widget Ayrshare Connect

> Cho phép người dùng của bạn liên kết tài khoản mạng xã hội từ ngay trong dashboard của riêng bạn, với một thẻ script duy nhất và các nút của chúng tôi được nhúng trong trang của bạn.

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} />

Widget Ayrshare Connect đặt các nút liên kết của chúng tôi **bên trong dashboard của riêng bạn**.
Bạn tải một script, thả một slot vào bất cứ nơi nào một mạng thuộc về trong bố cục của bạn, và
chúng tôi render một nút ở đó đã hiển thị sẵn tài khoản đã được kết nối hay chưa. Người dùng của
bạn nhấp vào nó và liên kết tài khoản mà không rời khỏi trang của bạn — nhiều nhất một cửa sổ bật
lên, là của chính mạng xã hội.

Bạn không phải viết xử lý cửa sổ bật lên, không có OAuth callback, không có làm mới phiên, và
không có logic theo từng mạng. Khi một mạng thay đổi điều gì đó ở phía họ, bản sửa lỗi được đưa
vào bên trong các khung của chúng tôi ngay khi chúng tôi triển khai; bạn không phải triển khai lại
gì cả.

## Bạn Muốn Bề mặt Nào

| Bề mặt | Người dùng của bạn thấy | White-label | Chọn khi |
| - | - | - | - |
| **Widget — khung nhúng** ([`mount()`](#mount-a-slot)) | các nút của chúng tôi hiển thị ngay trong bố cục của bạn; không có cửa sổ bật lên nào cho đến cửa sổ của chính mạng xã hội | **Mạnh nhất.** Trang của bạn, phông chữ và màu sắc của bạn, và người dùng của bạn không bao giờ rời khỏi nó | bạn có một dashboard với một hàng cho mỗi mạng và muốn việc liên kết diễn ra tại chỗ. Max Pack. |
| **Widget — nút của riêng bạn** ([`popup()`](#your-own-button)) | nút của bạn, rồi một cửa sổ bật lên cho mạng xã hội | **Mạnh.** Cửa sổ bật lên là của chúng tôi, nhưng nó xuất hiện ngắn và kế thừa giao diện của bạn | bạn muốn nút và kiểu dáng của riêng mình, và không có khung nào trong bố cục của bạn. Max Pack. |
| **Trang liên kết được lưu trữ** ([hướng dẫn](/docs/multiple-users/api-integration-business#single-sign-on-with-jwt-authentication)) | một trang do chúng tôi lưu trữ, mang logo, màu sắc và CSS tùy chỉnh của bạn | **Yếu nhất.** Đó là trang của chúng tôi, và người dùng của bạn rời khỏi trang của bạn để dùng nó | bạn muốn một liên kết duy nhất để gửi đi, hoặc bạn đang onboard qua email. Không phải xây dựng gì, không cần Max Pack. |

<div className="my-8">
  <Frame caption="Khung nhúng: các thẻ của chúng tôi được render bên trong bố cục của riêng bạn, một slot cho mỗi mạng hoặc một slot cho nhiều mạng.">
    <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-frames.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=d6c26228a418421772f753f31b0dfc59" alt="Một dashboard khách hàng với các thẻ Ayrshare Connect cho Instagram, TikTok và LinkedIn được nhúng trong một card" width="2400" height="1120" data-path="images/multiple-users/connect-widget-frames.webp" />
  </Frame>
</div>

<div className="my-8">
  <Frame caption="Nút của riêng bạn: bạn render nút, và một cửa sổ bật lên ngắn của chúng tôi xử lý mạng xã hội.">
    <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="Một dashboard khách hàng với các nút Connect của riêng nó và một cửa sổ bật lên Ayrshare hiển thị màn hình chuyển giao của Facebook" width="2880" height="1317" data-path="images/multiple-users/connect-widget-popup.webp" />
  </Frame>
</div>

<div className="my-8">
  <Frame caption="Trang liên kết được lưu trữ: một trang do chúng tôi lưu trữ, mang logo và màu sắc của bạn, mà người dùng của bạn rời khỏi ứng dụng của bạn để dùng.">
    <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-hosted.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=4d22a992f6e70808fd9ad9f065ed3879" alt="Trang liên kết mạng xã hội được lưu trữ hiển thị mọi mạng khả dụng" width="2336" height="1906" data-path="images/multiple-users/connect-widget-hosted.webp" />
  </Frame>
</div>

<Note>
  Hai hàng widget là **một tích hợp**, không phải hai. Một lần `init` duy nhất cho bạn cả hai: gắn
  các khung ở nơi bạn muốn có nút của chúng tôi, và gọi `popup()` từ nút của riêng bạn ở mọi nơi
  khác. Chúng dùng chung một phiên và báo cáo trên cùng các handler.

  [Chế độ trực tiếp (Direct Mode)](/docs/multiple-users/connect-direct-mode) là cùng cửa sổ bật lên đó
  **nhưng không có** script của chúng tôi — dành cho trang có Content-Security-Policy nghiêm ngặt,
  trang render phía máy chủ, hoặc ứng dụng gốc. Ở đó bạn tự mở và tự theo dõi cửa sổ bật lên.
</Note>

## Thêm Script

Ghim một phiên bản cùng hash của nó, hoặc theo một channel mà không có hash. Đừng bao giờ dùng cả
hai — một thuộc tính `integrity` trên một URL di động sẽ ngừng hoạt động ở lần phát hành tiếp theo
của chúng tôi, vì tệp mà nó chỉ định đã thay đổi một cách hợp lệ.

```html Pinned version theme={"system"}
<script
  src="https://app.ayrshare.com/ayrshare-connect/1.2.0/widget.js"
  integrity="sha384-qQ/of8CnYj26nF9RkQ6qk3D1vvzWesu7mpBK6MFK2D0EmmgRJHfbDmRowFBfltmz"
  crossorigin="anonymous"
></script>
```

```html Tracking v1 theme={"system"}
<script src="https://app.ayrshare.com/ayrshare-connect/v1/widget.js" crossorigin="anonymous"></script>
```

| Đường dẫn | Caching | `integrity` |
| - | - | - |
| `/ayrshare-connect/<version>/widget.js` | bất biến, một năm | **có** — hãy ghim nó |
| `/ayrshare-connect/v1/widget.js` | năm phút | không |
| `/ayrshare-connect/latest/widget.js` | năm phút | không |

**`v1` là channel nên khuyến nghị.** Nó nhận các bản sửa lỗi nhưng không bao giờ vượt qua một thay
đổi phá vỡ tương thích. `latest` theo định nghĩa sẽ vượt qua các phiên bản major, nên đến một lúc
nào đó nó sẽ đưa cho trang của bạn một phiên bản mà bạn chưa xem xét hành vi.

Hash của mọi phiên bản được công bố trong
[`manifest.json`](https://app.ayrshare.com/ayrshare-connect/manifest.json), tệp này cũng nêu rõ
mỗi channel hiện đang phục vụ phiên bản nào:

```json manifest.json theme={"system"}
{
  "versions": {
    "1.2.0": { "integrity": "sha384-qQ/of8CnYj26nF9RkQ6qk3D1vvzWesu7mpBK6MFK2D0EmmgRJHfbDmRowFBfltmz" }
  },
  "latest": "1.2.0",
  "channels": { "v1": "1.2.0", "latest": "1.2.0" }
}
```

Mỗi bundle cũng mở đầu bằng một comment nêu phiên bản của chính nó, là cách nhanh nhất để cho
chúng tôi biết một trang thực sự đang chạy phiên bản nào:

```js theme={"system"}
/*! ayrshare-connect 1.2.0 */
```

### Content-Security-Policy

Nếu trang của bạn gửi Content-Security-Policy, nó cần **hai** mục, cả hai đều nêu host mà bạn tải
script từ đó:

```
script-src https://app.ayrshare.com;
frame-src  https://app.ayrshare.com;
```

Đó là toàn bộ danh sách. Bạn **không cần mục `connect-src`** cho chúng tôi — các khung của chúng
tôi truy cập API của chúng tôi từ bên trong chính chúng, không phải từ trang của bạn — và **không
cần mục nào cho cửa sổ bật lên**, vốn là các cửa sổ cấp cao nhất mà chính sách của bạn không kiểm
soát. Cả hai mục đều đã được đo lường: bỏ `script-src` sẽ chặn script, và bỏ `frame-src` sẽ chặn
khung.

## Khởi động một Instance

`session` là tùy chọn bắt buộc duy nhất. Nó được gọi **một lần cho mỗi instance**, không phải một
lần cho mỗi mount, và phải trả về phản hồi của backend của bạn từ
[Tạo một Link Session](/docs/apis/profiles/create-link-session) với `mode: "connect"` —
`{ sessionId, token, expiresAt }` — đúng như khi nó được trả về.

```javascript Your page theme={"system"}
const connect = AyrshareConnect.init({
  session: () => fetch("/my-api/ayrshare-session").then(r => r.json()),
  appearance: { "--ayr-connect-accent": "#0B7A6C" },
  maxHeight: 800,
});
```

```javascript Your backend theme={"system"}
app.get("/my-api/ayrshare-session", async (req, res) => {
  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": profileKeyFor(req.user),
    },
    body: JSON.stringify({ mode: "connect", origin: "https://app.example.com" }),
  });

  res.json(await response.json());
});
```

Một phiên widget **không** nêu `network` nào — nó cấp quyền cho mọi mạng mà tài khoản của bạn cho
phép, và mạng nào xuất hiện được quyết định theo từng mount, trong trang của bạn. Nó mang theo một
`origin`, và đó là điều duy nhất khiến các khung render được: một khung kiểm tra trang đang nhúng
nó với giá trị đó và từ chối render ở bất kỳ nơi nào khác.

<Warning>
  Hãy tạo phiên trên máy chủ của bạn. Lệnh gọi cần API key của bạn, và token nó trả về sẽ đăng
  nhập người dùng của bạn vào User Profile của họ — hãy coi nó như một mật khẩu.
</Warning>

Chúng tôi gọi lại `session` trước khi token hết hạn, nên widget tiếp tục hoạt động trên một trang
được mở cả ngày. Một lệnh gọi bị reject, hoặc không trả về token, được thử lại hai lần nữa — sau
0,5 giây, rồi 1 giây — trước khi chúng tôi bỏ cuộc và phát ra `error`. Vì vậy một lần làm mới tốn
nhiều nhất ba lệnh gọi đến endpoint của bạn.

| Tùy chọn | Mặc định | Tác dụng |
| - | - | - |
| `session` | — | **Bắt buộc.** Trả về một link session chế độ connect. |
| `appearance` | mặc định của chúng tôi | Các design token, được áp dụng bên trong mọi khung. Xem [Giao diện](#appearance). |
| `css` | không có | Một chuỗi CSS được áp dụng bên trong mọi khung. Xem [CSS tùy chỉnh](#custom-css). |
| `maxHeight` | `600` | Một khung có thể cao đến đâu trước khi nó tự cuộn bên trong. |

<h2 id="mount-a-slot">
  Gắn một Slot
</h2>

Một `mount` cho mỗi slot. Yêu cầu một mạng, vài mạng, hoặc mọi thứ mà phiên cho phép — mức độ chi
tiết là của bạn, nên một slot có thể là một hàng đơn trong một bảng có sẵn hoặc một panel chứa tất
cả.

```javascript theme={"system"}
connect.mount("#instagram-slot", { network: "instagram" });
connect.mount("#some-slot", { networks: ["facebook", "tiktok", "x"] });
connect.mount("#everything");

const row = connect.mount(document.querySelector("#tall"), { maxHeight: 1200 });
row.unmount();
```

`mount` nhận một CSS selector hoặc một element, và trả về `{ unmount, element }`. Nó throw nếu
target không khớp gì cả — mà hầu như luôn là một slot chưa tồn tại, vì vậy hãy mount sau khi
markup của bạn đã có trong document.

Mỗi mount là một iframe. Nó báo cáo chiều cao của chính nó cho chúng tôi và chúng tôi thay đổi
kích thước để khớp, nên bố cục của bạn tự sắp xếp lại khi nội dung của chúng tôi thay đổi; vượt
quá `maxHeight`, khung tự cuộn bên trong thay vì tràn ra khỏi trang của bạn. **Slot hẹp nhất chúng
tôi hỗ trợ là 300px.**

Các khóa mạng là của chính Ayrshare, và các cách viết bí danh cũng hoạt động: `instagram` và
`instagramapi` đều có nghĩa là `instagramApi`, và `x` có nghĩa là `twitter`. Một khóa không phải
là một mạng sẽ không render thẻ nào.

<h2 id="your-own-button">
  Nút của Riêng Bạn
</h2>

Một khách hàng muốn dùng nút của riêng họ thay vì một trong các khung của chúng tôi sẽ gọi `popup`
thay thế. Nó chạy cùng một luồng, trên cùng một phiên, và báo cáo trên cùng các handler.

<Warning>
  **Hãy gọi nó trực tiếp bên trong click handler, không await gì trước nó.** Trình duyệt chỉ cho
  phép một cửa sổ bật lên trong khi nó vẫn đang xử lý cú nhấp của người dùng, và quyền đó không
  sống sót qua một `await`. Dù sao cũng không có gì cần await — phiên đã được tạo lúc `init`.
</Warning>

```javascript theme={"system"}
linkedInButton.addEventListener("click", () => {
  connect.popup({ network: "linkedin" });
});
```

`popup` trả về `{ close(), network }`, và **luôn luôn** trả về một handle — kể cả sau một cửa sổ
bật lên bị chặn, khi đó `close()` không làm gì — nên mã của bạn không bao giờ phải null-check nó
trước khi gọi `close()`.

Mọi mạng đều hoạt động ở đây, kể cả những mạng mà một khung hoàn tất bên trong panel của chính nó:
Facebook hiển thị màn hình giải thích chuyển giao của nó trong cửa sổ bật lên, Bluesky và X hiển
thị form thông tin xác thực của chúng, còn LinkedIn, Pinterest, YouTube và Google Business đi ra
mạng xã hội rồi quay lại.

Nó throw một cách đồng bộ với ba điều là lỗi lập trình — không có `network`, một instance đã bị
destroy, hoặc một phiên chưa resolve. Một cửa sổ bật lên bị chặn **không** phải một trong số đó:
điều đó phát ra `error` với `reason: "popupBlocked"`, vì người dùng của bạn không làm gì sai.

Nhiều nhất một cửa sổ bật lên được mở tại một thời điểm. Lệnh gọi thứ hai đóng cửa sổ thứ nhất và
báo cáo `cancelled` với `reason: "superseded"` trên nó. Một cửa sổ bật lên do một trong các
**khung** của chúng tôi mở là một thứ khác và không bao giờ bị đụng đến, nên nút của bạn không thể
hủy một luồng đang chạy bên trong một slot đã mount.

<Note>
  Việc phiên có được phép liên kết một mạng hay không là câu trả lời của **máy chủ**, không phải
  của script. Một phiên được giới hạn cho Bluesky mà được yêu cầu LinkedIn sẽ nhận được một sự từ
  chối hiển thị trong cửa sổ bật lên và được báo cáo dưới dạng `error`. Script chỉ kiểm tra rằng
  một mạng đã được nêu tên.
</Note>

## React

Script không phụ thuộc framework, nên React không cần gì đặc biệt từ chúng tôi — nhưng bốn điều về
vòng đời của nó đáng làm đúng ngay từ lần đầu.

Tải script **một lần**, bên ngoài cây component của bạn. Trong Next.js đó là `next/script` trong
root layout của bạn; trong Vite hoặc Create React App đó là một thẻ trong `index.html`. Tải nó
theo từng component sẽ chạy lại nó ở mỗi lần mount.

```jsx ConnectAccounts.jsx theme={"system"}
import { useEffect, useRef, useState } from "react";

export function ConnectAccounts() {
  const slot = useRef(null);
  const [linked, setLinked] = useState([]);

  useEffect(() => {
    // Bên trong effect, để ref đã được gắn: mount() throw nếu target của nó
    // chưa tồn tại, và đó chính xác là điều xảy ra nếu bạn gọi nó trong lúc render.
    const connect = window.AyrshareConnect.init({
      session: () => fetch("/my-api/ayrshare-session").then(r => r.json()),
      appearance: { "--ayr-connect-accent": "#0B7A6C" },
    });

    connect.mount(slot.current, { networks: ["instagram", "tiktok", "x"] });

    const stop = connect.on("success", ({ network }) => {
      setLinked(current => [...current, network]);
    });
    // `success` là một trong mười sự kiện mà widget báo cáo. Xem phần Sự kiện
    // bên dưới để có danh sách đầy đủ, bao gồm `state`, thứ thay thế polling.

    // destroy() dọn đi các khung, message listener và các timer cùng với nó.
    // Không có dòng này, một lần đổi route sẽ để lại cả ba.
    return () => {
      stop();
      connect.destroy();
    };
  }, []);

  return <div ref={slot} />;
}
```

<Warning>
  **Không bao giờ dùng lại một instance sau `destroy()`.** Một instance đã bị destroy sẽ vẫn bị
  destroy — `popup()` throw trên nó, và `mount()` sẽ không đưa nó trở lại. Hãy tạo một instance
  mới trong lần chạy effect tiếp theo, đó chính là điều đoạn mã trên làm.
</Warning>

Hai hệ quả của mẫu đó, và cả hai đều không phải bug:

* **Trong môi trường phát triển bạn sẽ thấy callback session được gọi hai lần.** Strict Mode của
  React chạy các effect theo trình tự mount → unmount → mount, nên instance được tạo, bị destroy
  và được tạo lại. Phần cleanup ở trên làm cho điều đó an toàn; nó tốn thêm một lệnh gọi đến
  backend của bạn trong dev và không tốn gì trong production.
* **Giữ các dependency của effect ổn định.** Một array literal được truyền thẳng vào `mount` từ
  một lần render của component cha là một giá trị mới mỗi lần, nên một effect phụ thuộc vào nó sẽ
  gỡ widget xuống và dựng lại nó ở mỗi lần render. Hãy memoize nó, hoặc giữ nó là hằng số như ở
  trên.

## Sự kiện

Đăng ký với `on`, hàm này trả về một hàm hủy đăng ký. Các handler nhận payload của sự kiện và
mount mà nó đến từ; `off(name, handler)` làm cùng công việc khi bạn muốn đặt tên cho handler.

```javascript theme={"system"}
const stop = connect.on("success", ({ network, displayName }, mount) => {
  refreshRow(network, displayName, mount.element);
});
stop();

connect.on("error", ({ network, code, message }) => report(code, message));
connect.destroy(); // every frame, listener and timer
```

Mười sự kiện. Mọi sự kiện đều mang một `network` ngoại trừ `ready`, và ngoại trừ loại `error` duy
nhất hoàn toàn không liên quan đến một mạng — xem [`error` có hai nguồn](#error-has-two-sources)
bên dưới.

| Sự kiện | Phát ra khi | Cũng mang theo |
| - | - | - |
| `ready` | một khung đã được mount và sẵn sàng | — |
| `click` | người dùng của bạn nhấp vào một mạng, **theo cả hai chiều** | `action`: `"connect"` hoặc `"unlink"` |
| `started` | một lần thử liên kết đang diễn ra | — |
| `selection` | người dùng của bạn đã đến một màn hình chọn hoặc một form | `step` |
| `success` | tài khoản đã được kết nối **và đã lưu** | `displayName` (bị lược bỏ khi chưa biết), `refId` |
| `unlinked` | một tài khoản đã kết nối bị gỡ, và việc gỡ đã được lưu | — |
| `error` | lần thử thất bại | `code`, `message`, `reason` |
| `cancelled` | người dùng của bạn rút lui | `reason` |
| `closed` | cửa sổ bật lên mà lần thử này dùng đã đóng | — |
| `state` | trạng thái tài khoản của một mạng, lúc mount và ở mỗi lần thay đổi | `state`, `since` |

**Bốn trong số đó là kết cục** — `success`, `unlinked`, `error` và `cancelled` — và đúng một trong
số chúng đến cho mỗi lần thử. `closed` là một thông báo vòng đời đi theo sau một kết cục chứ không
phải là một kết cục.

`click` phát ra **trước** khi bất kỳ công việc liên kết nào bắt đầu, nên nó báo cáo một cú nhấp mà
một cửa sổ bật lên bị chặn hoặc một phiên đã chết sau đó từ chối. Đây là sự kiện để dùng cho
analytics của riêng bạn; `started` mới là sự kiện có nghĩa là một lần thử thực sự đang chạy.

### Reasons

| Sự kiện | `reason` | Có nghĩa là |
| - | - | - |
| `error` | `popupBlocked` | trình duyệt từ chối mở cửa sổ bật lên |
| `cancelled` | `popupClosed` | người dùng của bạn tự tay đóng cửa sổ |
| `cancelled` | `scopesDenied` | người dùng của bạn từ chối một quyền mà mạng xã hội yêu cầu |
| `cancelled` | `userCancelled` | người dùng của bạn rút lui, hoặc mã của bạn gọi `handle.close()` |
| `cancelled` | `superseded` | một lệnh gọi `popup()` thứ hai thay thế lần thử này |

<h3 id="error-has-two-sources">
  `error` Có Hai Nguồn
</h3>

Chỉ một trong hai là thất bại liên kết, và chúng mang các trường khác nhau.

* Một **lỗi liên kết** mang `network`, `code` và mount mà nó đến từ.
* Một **lỗi phiên** — chúng tôi không thể tạo hoặc làm mới phiên của bạn — chỉ mang `message`, vì
  không có gì đang được liên kết vào lúc đó.

Hãy destructure một cách phòng thủ: `code` và `network` là `undefined` ở loại thứ hai.

### `state` Giúp Bạn Khỏi Polling

`state` là kênh dữ liệu chứ không phải báo cáo về một lần thử. Mỗi khung phát ra một sự kiện cho
mỗi mạng khi nó mount, mang trạng thái hiện tại của mạng đó và mốc thời gian `since` mà nó giữ
trạng thái đó từ đó, và một sự kiện nữa mỗi khi một trạng thái thay đổi — **bao gồm cả các thay
đổi bắt nguồn từ phía chúng tôi**, chẳng hạn một token chết chuyển sang trạng thái cần liên kết
lại. Vì vậy bạn có thể vận hành toàn bộ UI của mình từ widget mà không phải polling bất cứ thứ gì.

Các giá trị là cùng một enum mà
[`GET /profiles` với `include=state`](/docs/apis/profiles/get-profiles) trả về: `linked`, `unlinked`,
`identityVerificationRequired`, `restricted`, `rateLimited`, `suspended`.

## Hủy liên kết

Các thẻ của chúng tôi hủy liên kết cũng như liên kết. Người dùng của bạn nhấp vào một mạng đã kết
nối, xác nhận, và tài khoản được gỡ:

1. `click` phát ra với `action: "unlink"`.
2. `unlinked` phát ra khi việc gỡ đã được lưu.

Một lần hủy liên kết **thất bại** báo cáo `error`, và một lần mà người dùng của bạn rút lui ở bước
xác nhận báo cáo `cancelled`. Không có sự kiện hủy-liên-kết-thất-bại riêng.

<h2 id="appearance">
  Giao diện
</h2>

Stylesheet của khách hàng không thể chạm vào bên trong một khung cross-origin, nên việc tạo kiểu
được truyền dưới dạng dữ liệu mà chúng tôi áp dụng bên trong nó. Truyền `appearance` cho `init`
dưới dạng các CSS custom property; mỗi property chúng tôi không nhận được sẽ giữ giá trị mặc định
của chúng tôi.

```javascript theme={"system"}
const connect = AyrshareConnect.init({
  session: getSession,
  appearance: {
    "--ayr-connect-font-family": "Inter, system-ui, sans-serif",
    "--ayr-connect-surface-bg": "#111318",
    "--ayr-connect-surface-fg": "#F2F3F7",
    "--ayr-connect-accent": "#0B7A6C",
    "--ayr-connect-accent-fg": "#FFFFFF",
    "--ayr-connect-radius": "12px",
  },
});
```

<Warning>
  **Hãy đặt màu theo cặp.** Một token nền không có token chữ đi kèm là cách duy nhất làm cho thứ
  này tạo ra thứ gì đó không đọc được: đặt riêng `--ayr-connect-surface-bg` thành một giá trị tối
  và `--ayr-connect-surface-fg` mặc định của chúng tôi vẫn là màu xanh navy tối. Không gì có thể
  suy ra nửa còn lại giúp bạn.
</Warning>

Khi hoàn toàn không có `appearance`, mọi token giữ giá trị mặc định và một khung trông như thế
này:

<div className="my-8">
  <Frame caption="Các token mặc định: bề mặt trắng, chữ xanh navy tối, màu nhấn indigo, bo góc 8px.">
    <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-appearance-default.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=62f9ce7e061c8bb58132960b9e270f4a" alt="Ba thẻ Ayrshare Connect với giao diện mặc định" width="920" height="528" data-path="images/multiple-users/connect-widget-appearance-default.webp" />
  </Frame>
</div>

Truyền một vài token và cùng khung đó khoác lên bảng màu của bạn. Ví dụ này chuyển các bề mặt sang
xanh nhạt, làm đậm chữ và màu nhấn cho tương xứng, và bo góc thêm một chút:

```javascript theme={"system"}
const connect = AyrshareConnect.init({
  session: getSession,
  appearance: {
    "--ayr-connect-surface-bg": "#EFF6FF",
    "--ayr-connect-border-color": "#BFDBFE",
    "--ayr-connect-surface-fg": "#0F2A5F",
    "--ayr-connect-surface-fg-muted": "#4A6A9A",
    "--ayr-connect-accent": "#1D4ED8",
    "--ayr-connect-radius": "14px",
    "--ayr-connect-spacing": "10px",
  },
});
```

<div className="my-8">
  <Frame caption="Cùng ba thẻ đó sau khi các token ở trên được áp dụng.">
    <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-appearance-themed.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=686d1ad900628a6d4c75dc242f1ab38c" alt="Ba thẻ Ayrshare Connect được tạo kiểu lại với bề mặt xanh nhạt và màu nhấn xanh đậm hơn" width="920" height="574" data-path="images/multiple-users/connect-widget-appearance-themed.webp" />
  </Frame>
</div>

**Chỉ có một bảng màu và không có preset sáng/tối.** Không có gì phụ thuộc vào
`prefers-color-scheme`, một cách có chủ đích — chủ đề của trang bạn có thể không khớp với hệ điều
hành của người dùng, và một media query sẽ âm thầm lấn át các màu bạn đã chọn. Một dashboard tối
được tạo chủ đề bằng cách cung cấp các giá trị tối.

Danh sách token này là một hợp đồng được hỗ trợ mà chúng tôi giữ nguyên qua các phiên bản.

### Các Token

| Token | Mặc định |
| - | - |
| `--ayr-connect-font-family` | `Inter, "Segoe UI", system-ui, -apple-system, sans-serif` |
| `--ayr-connect-font-size` | `16px` |
| `--ayr-connect-font-size-sm` | `12px` |
| `--ayr-connect-label-font-weight` | `600` |
| `--ayr-connect-status-font-size` | `12px` |
| `--ayr-connect-status-font-weight` | `600` |
| `--ayr-connect-surface-bg` | `#FFFFFF` |
| `--ayr-connect-surface-bg-hover` | `#F7F7F7` |
| `--ayr-connect-surface-fg` | `#010629` |
| `--ayr-connect-surface-fg-muted` | `#56596F` |
| `--ayr-connect-border-color` | `#DDDEE2` |
| `--ayr-connect-border-width` | `1px` |
| `--ayr-connect-radius` | `8px` |
| `--ayr-connect-spacing` | `8px` |
| `--ayr-connect-accent` | `#4553EE` |
| `--ayr-connect-accent-fg` | `#FFFFFF` |
| `--ayr-connect-focus-ring-color` | `#4553EE` |
| `--ayr-connect-focus-ring-width` | `2px` |
| `--ayr-connect-disabled-fg` | `#6E7185` |
| `--ayr-connect-status-radius` | `4px` |
| `--ayr-connect-status-success-bg` | `#EBFFF8` |
| `--ayr-connect-status-success-fg` | `#237C5C` |
| `--ayr-connect-status-warning-bg` | `#FFF7EF` |
| `--ayr-connect-status-warning-fg` | `#702E00` |
| `--ayr-connect-status-critical-bg` | `#FFE5E1` |
| `--ayr-connect-status-critical-fg` | `#AD1902` |
| `--ayr-connect-status-info-bg` | `#ECF9FF` |
| `--ayr-connect-status-info-fg` | `#1F6686` |
| `--ayr-connect-callout-bg` | `#FFF7EF` |
| `--ayr-connect-callout-fg` | `#702E00` |
| `--ayr-connect-partner-name` | `""` |
| `--ayr-connect-icon-size` | `32px` |
| `--ayr-connect-avatar-size` | `40px` |

Một giá trị không phải CSS hợp lệ cho token của nó sẽ bị **bỏ qua, kèm một cảnh báo trong console
của bạn**, thay vì được áp dụng. Điều đó quan trọng hơn nghe có vẻ: một số `8` không đơn vị cho
`--ayr-connect-spacing` là một chuỗi hoàn toàn vô hại nhưng sẽ làm mất hiệu lực mọi phép tính đọc
nó và làm sụp đổ bố cục, mà không có lỗi ở đâu cả. Hãy cho các độ dài một đơn vị.

### Tên Của Riêng Bạn Trên Màn hình Chuyển giao

Trước khi chúng tôi chuyển người dùng của bạn sang một mạng, chúng tôi hiển thị một màn hình ngắn
nêu tên họ đang kết nối thông qua ai. Hai hook cho phép bạn biến nó thành của riêng mình, và cả
hai đều ổn định qua các phiên bản.

`--ayr-connect-partner-name` đặt nhãn. Đây là token duy nhất mà giá trị của nó là **văn bản**, nên
nó phải được đặt trong dấu nháy như một chuỗi CSS — một giá trị không có dấu nháy là không hợp lệ
và hoàn toàn không render gì:

```javascript theme={"system"}
AyrshareConnect.init({
  session: getSession,
  appearance: { "--ayr-connect-partner-name": "'Acme Social'" },
  css: "[data-ayr-connect-partner-mark]::after { background-image: url('https://cdn.example.com/mark.svg') }",
});
```

Logo không phải là một token. Chúng tôi cung cấp phần đánh dấu dưới dạng một element đã được định
vị và định kích thước, và bạn lấp đầy nó bằng một `background-image` thông qua `css`, như ở trên —
một token có thể tải một hình ảnh từ bên trong document của chúng tôi không phải là thứ chúng tôi
chấp nhận, nên yêu cầu đến từ một rule bạn viết chứ không phải từ một giá trị bạn truyền cho chúng
tôi.

<Note>
  `[data-ayr-connect-partner-mark]` và `[data-ayr-connect-partner-name]` là **ngoại lệ** của lưu ý
  về CSS tùy chỉnh bên dưới: hai selector này là một phần của hợp đồng và chúng tôi giữ chúng qua
  các phiên bản.
</Note>

<h3 id="custom-css">
  CSS Tùy chỉnh
</h3>

`css` nhận một chuỗi được áp dụng bên trong mọi khung, cho các trường hợp mà token không bao phủ.

```javascript theme={"system"}
AyrshareConnect.init({ session: getSession, css: "button { letter-spacing: 0.01em }" });
```

<Warning>
  **CSS tùy chỉnh không được hỗ trợ qua các phiên bản.** Các selector của nó nhắm vào markup nội
  bộ của chúng tôi, thứ thay đổi giữa các bản phát hành — một rule hoạt động hôm nay có thể âm
  thầm ngừng khớp sau bất kỳ bản cập nhật nào. Hợp đồng token ở trên là phần chúng tôi giữ. Hãy
  ghim một phiên bản nếu bạn phụ thuộc vào CSS tùy chỉnh.
</Warning>

Các cửa sổ bật lên kế thừa `appearance` và `css` của instance đúng như các khung, nên người dùng
của bạn không phải nhìn thấy các giá trị mặc định trung tính của chúng tôi xuất hiện giữa chừng
một luồng.

## Đáng Biết Trước Khi Bạn Phát hành

<AccordionGroup>
  <Accordion title="Một cửa sổ bật lên có phiên đã hết hạn báo cáo cancelled, không phải error">
    Một cửa sổ bật lên bạn mở bằng `popup()` không thể được cho biết vì sao một token bị từ chối —
    để nói ra điều đó nó sẽ phải tin một origin mà nó chưa xác thực, điều mà mô hình bảo mật của
    chúng tôi không cho phép. Vì vậy một cửa sổ bật lên mang một token đã chết sẽ đóng lại và hiện
    ra dưới dạng `cancelled` với `reason: "popupClosed"` thay vì `error`.

    Trên thực tế điều này hiếm: một cửa sổ bật lên được mở trước một lần làm mới âm thầm vẫn tiếp
    tục hoạt động, vì nó đã xác thực token của nó khi mở. Nếu bạn thấy các kết quả `popupClosed`
    không giải thích được, hãy kiểm tra rằng endpoint `session` của bạn đang trả về một phiên mới.
  </Accordion>

  <Accordion title="Một phiên cho mỗi instance, không phải một phiên cho mỗi mount">
    Mười bốn mount dùng chung một token và tốn một lệnh gọi đến backend của bạn, không phải mười
    bốn. Nếu bạn muốn các slot có phạm vi khác nhau — một `allowedSocial` khác trên một số slot —
    hãy chạy một `init` thứ hai với phiên riêng của nó thay vì kỳ vọng một mount thu hẹp nó.
  </Accordion>

  <Accordion title="Các khung chỉ render trên origin bạn đã khai báo">
    Mỗi phiên mang `origin` mà trang của bạn chạy trên đó, và một khung so sánh trang đang nhúng
    nó với giá trị đó trước khi render bất cứ thứ gì. Một khung được nhúng ở nơi khác sẽ trống và
    không gửi sự kiện nào. Không có allowlist để đăng ký và không có gì để cấu hình — hãy gửi đúng
    `origin` khi bạn tạo phiên.
  </Accordion>

  <Accordion title="Chiều cao được xử lý giúp bạn, và không phải là một sự kiện">
    Các khung báo cáo chiều cao của chúng cho script, và script thay đổi kích thước chúng. Bố cục
    của bạn chỉ việc sắp xếp lại. Không có sự kiện resize nào để đăng ký, và không có gì để đo ở
    phía bạn.
  </Accordion>
</AccordionGroup>

## Yêu cầu

<ul className="custom-bullets">
  <li>
    **[Max Pack](/docs/additional/maxpack)**. Một phiên widget là một phiên chế độ connect, và tạo một
    phiên như vậy mà không có Max Pack trả về `code: 504`. Hãy liên hệ bộ phận hỗ trợ nếu bạn cần
    bật chế độ connect trên một tài khoản không có nó.
  </li>

  <li>
    Một **`origin`** trên mỗi phiên — origin chính xác mà trang của bạn chạy trên đó. Bỏ qua nó
    trả về `code: 505`; một giá trị không phải là một origin `https`, một custom scheme, hoặc
    `http://localhost` trả về `code: 506`.
  </li>

  <li>
    **Không có `network`** trên phiên. Tham số đó là thứ biến một phiên thành
    [chế độ trực tiếp](/docs/multiple-users/connect-direct-mode), và URL của một phiên chế độ trực tiếp
    không phải là thứ script mong đợi.
  </li>
</ul>

Mọi mã ở trên đều có trong tài liệu tham khảo
[Lỗi Link Session](/docs/errors/errors-ayrshare#link-session-errors), kèm thông báo chính xác mà API
trả về và việc cần làm với nó.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.