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

# Ayrshare Connect 小组件

> 只需一个 script 标签，把我们的按钮嵌入你的页面，让你的用户在你自己的控制台内关联社交账号。

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

Ayrshare Connect 小组件把我们的关联按钮放进**你自己的控制台**。你加载一个脚本，在布局中每个网络
所属的位置放一个插槽（slot），我们就会在那里渲染一个按钮，并且它已经显示该账号是否已关联。你的
用户点击它，无需离开你的页面即可关联账号——最多出现一个弹窗，也就是社交网络自己的弹窗。

你不需要编写任何弹窗处理、OAuth 回调、会话刷新或按网络区分的逻辑。当某个网络在它那一侧做出变更
时，修复会在我们部署的那一刻通过我们的框架发布；你无需重新部署任何东西。

## 你需要哪种界面

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

<div className="my-8">
  <Frame caption="嵌入式框架：我们的卡片渲染在你自己的布局内，每个网络一个插槽，或一个插槽容纳多个网络。">
    <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="一个客户控制台，卡片中嵌入了 Instagram、TikTok 和 LinkedIn 的 Ayrshare Connect 卡片" width="2400" height="1120" data-path="images/multiple-users/connect-widget-frames.webp" />
  </Frame>
</div>

<div className="my-8">
  <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>
</div>

<div className="my-8">
  <Frame caption="托管关联页面：我们托管的页面，带有你的 logo 和颜色，你的用户要离开你的应用去使用它。">
    <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="显示所有可用网络的托管社交关联页面" width="2336" height="1906" data-path="images/multiple-users/connect-widget-hosted.webp" />
  </Frame>
</div>

<Note>
  两个小组件行是**同一个集成**，而不是两个。一次 `init` 同时给你两者：在需要我们按钮的地方挂载
  框架，在其他任何地方从你自己的按钮调用 `popup()`。它们共享同一个会话，并在同一组处理器上报告
  结果。

  [直连模式](/docs/multiple-users/connect-direct-mode)（Direct Mode）是**不带**我们脚本的同一个弹窗——
  适用于带有严格 Content-Security-Policy 的页面、服务器端渲染的页面或原生应用。在那里由你自己
  打开并监视弹窗。
</Note>

## 添加脚本

固定（pin）一个版本及其哈希，或不带哈希地跟随一个渠道。两者绝不要同时使用——在会变动的 URL 上
添加 `integrity` 属性会在我们下一次发布时失效，因为它指向的文件已经合理地发生了变化。

```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>
```

| 路径                                      | 缓存     | `integrity`   |
| --------------------------------------- | ------ | ------------- |
| `/ayrshare-connect/<version>/widget.js` | 不可变，一年 | **是** —— 请固定它 |
| `/ayrshare-connect/v1/widget.js`        | 五分钟    | 否             |
| `/ayrshare-connect/latest/widget.js`    | 五分钟    | 否             |

**`v1` 是值得推荐的渠道。** 它会获得修复，但绝不会跨越破坏性变更。`latest` 从定义上就会跨越
主版本，因此它终究会把一个你尚未审阅过其行为的版本交给你的页面。

每个版本的哈希都发布在
[`manifest.json`](https://app.ayrshare.com/ayrshare-connect/manifest.json) 中，其中还标明了
每个渠道当前提供的版本：

```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" }
}
```

每个 bundle 的开头还有一条注明自身版本的注释，这是告诉我们某个页面实际运行的版本的最快方式：

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

### Content-Security-Policy

如果你的页面发送 Content-Security-Policy，它需要**两条**条目，都指向你加载脚本的主机：

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

这就是全部清单。你**不需要为我们添加 `connect-src` 条目**——我们的框架从其内部访问我们的 API，
而不是从你的页面——也**不需要为弹窗添加条目**，弹窗是顶层窗口，不受你的策略约束。这两条条目都
经过实际验证：移除 `script-src` 会阻止脚本，移除 `frame-src` 会阻止框架。

## 启动一个实例

`session` 是唯一必需的选项。它**每个实例调用一次**，而不是每次 mount 调用一次，并且必须把你的
后端对带 `mode: "connect"` 的[创建 Link Session](/docs/apis/profiles/create-link-session) 调用的响应
——`{ sessionId, token, expiresAt }`——原样返回。

```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());
});
```

小组件会话**不**指定 `network`——它授权你的账户允许的所有网络，具体显示哪些由每次 mount 在你的
页面中决定。它确实携带一个 `origin`，而这正是让框架得以渲染的唯一条件：框架会将嵌入它的页面与
该值进行比对，在其他任何地方都拒绝渲染。

<Warning>
  请在你的服务器上创建会话。该调用需要你的 API Key，而它返回的 token 会让你的用户登录其
  User Profile——请像对待密码一样对待它。
</Warning>

我们会在 token 过期之前再次调用 `session`，因此在一个整天开着的页面上小组件也能持续工作。如果
调用被拒绝或未返回 token，会再重试两次——分别在 0.5 秒和 1 秒之后——然后我们放弃并发出
`error`。因此一次刷新最多花费三次对你端点的调用。

| 选项           | 默认值    | 作用                                             |
| ------------ | ------ | ---------------------------------------------- |
| `session`    | —      | **必需。** 返回一个 connect 模式的 link session。         |
| `appearance` | 我们的默认值 | 设计 token，应用于每个框架内部。参见[外观](#appearance)。        |
| `css`        | 无      | 应用于每个框架内部的一段 CSS 字符串。参见[自定义 CSS](#custom-css)。 |
| `maxHeight`  | `600`  | 框架在改为内部滚动之前可以增长到的高度。                           |

<h2 id="mount-a-slot">
  挂载插槽
</h2>

每个插槽一次 `mount`。可以请求一个网络、多个网络，或会话允许的所有网络——粒度由你决定，因此
一个插槽可以是现有表格中的一行，也可以是容纳所有内容的一个面板。

```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` 接受 CSS 选择器或元素，并返回 `{ unmount, element }`。如果目标没有匹配到任何内容，它会
抛出异常——这几乎总是因为插槽还不存在，所以请在你的标记进入文档之后再 mount。

每次 mount 都是一个 iframe。它会向我们报告自身高度，我们据此调整其大小，因此你的布局会随我们
内容的变化而重排；超过 `maxHeight` 时，框架改为内部滚动，而不会溢出你的页面。**我们支持的最窄
插槽是 300px。**

网络键名使用 Ayrshare 自己的名称，别名拼写同样有效：`instagram` 和 `instagramapi` 都表示
`instagramApi`，`x` 表示 `twitter`。不是网络的键不会渲染任何卡片。

<h2 id="your-own-button">
  你自己的按钮
</h2>

宁愿使用自己的按钮而不是我们框架的客户可以改为调用 `popup`。它运行同样的流程、使用同一个会话，
并在同一组处理器上报告结果。

<Warning>
  **请直接在点击处理器内调用它，之前不要 await 任何东西。** 浏览器只在仍在处理用户点击时才允许
  弹窗，而这个许可撑不过一次 `await`。反正也没有什么需要 await 的——会话在 `init` 时就已创建。
</Warning>

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

`popup` 返回 `{ close(), network }`，并且**总是**返回一个句柄——包括弹窗被拦截之后（此时
`close()` 什么也不做）——因此你的代码在调用 `close()` 之前永远不必做空值检查。

所有网络在这里都可用，包括那些在框架自身面板内完成的网络：Facebook 在弹窗中显示其交接说明，
Bluesky 和 X 显示它们的凭据表单，而 LinkedIn、Pinterest、YouTube 和 Google Business 会跳转到
网络并返回。

对三种属于编程错误的情况它会同步抛出异常——没有 `network`、实例已被销毁、或会话尚未解析完成。
弹窗被拦截**不是**其中之一：那会发出带 `reason: "popupBlocked"` 的 `error`，因为你的用户没有
做错任何事。

同一时间最多只有一个弹窗打开。第二次调用会关闭第一个弹窗，并在其上报告带 `reason: "superseded"`
的 `cancelled`。我们的**框架**打开的弹窗是另一回事，绝不会被触及，因此你的按钮无法取消在挂载的
插槽内运行的流程。

<Note>
  会话是否可以关联某个网络由**服务器**决定，而不是脚本。一个仅限 Bluesky 的会话被要求关联
  LinkedIn 时，会在弹窗中渲染拒绝信息并报告为 `error`。脚本只检查是否指定了网络。
</Note>

## React

脚本不依赖任何框架，因此 React 不需要我们做任何特殊处理——但它的生命周期有四件事值得第一次就
做对。

**只加载一次**脚本，且在你的组件树之外。在 Next.js 中是根布局里的 `next/script`；在 Vite 或
Create React App 中是 `index.html` 里的一个标签。按组件加载会在每次挂载时重新运行它。

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

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

  useEffect(() => {
    // 放在 effect 内部，确保 ref 已附加：mount() 在目标尚不存在时会抛出异常，
    // 而在渲染期间调用它恰恰会遇到这种情况。
    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` 是小组件报告的十个事件之一。完整列表参见下方的“事件”一节，
    // 其中包括可替代轮询的 `state`。

    // destroy() 会把框架、message 监听器和定时器一并清除。
    // 少了这一步，路由切换会把这三样东西都留在原地。
    return () => {
      stop();
      connect.destroy();
    };
  }, []);

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

<Warning>
  **绝不要在 `destroy()` 之后重用实例。** 被销毁的实例会一直保持销毁状态——在其上调用 `popup()`
  会抛出异常，`mount()` 也无法让它复活。请在下一次 effect 运行中创建新实例，上面的代码正是这样
  做的。
</Warning>

这种模式有两个后果，都不是 bug：

* **在开发环境中你会看到 session 回调触发两次。** React 的 Strict Mode 以 mount → unmount →
  mount 的顺序运行 effect，因此实例会被创建、销毁、再创建。上面的清理让这一切是安全的；它在
  开发环境多花一次对你后端的调用，在生产环境一次也不多。
* **保持 effect 依赖稳定。** 从父级渲染直接传入 `mount` 的数组字面量每次都是新值，因此依赖它的
  effect 会在每次渲染时拆掉并重建小组件。请对它做 memoize，或像上面那样保持常量。

## 事件

用 `on` 订阅，它返回一个取消订阅函数。处理器会收到事件负载以及事件来源的 mount；当你更愿意用
具名处理器时，`off(name, 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(); // 所有框架、监听器和定时器
```

十个事件。除 `ready` 之外每个都携带 `network`，另有一种与网络完全无关的 `error` 也不携带——
参见下方 [`error` 有两个来源](#error-has-two-sources)。

| 事件          | 触发时机                   | 额外携带                              |
| ----------- | ---------------------- | --------------------------------- |
| `ready`     | 框架已挂载并就绪               | —                                 |
| `click`     | 你的用户点击了某个网络，**两个方向都算** | `action`：`"connect"` 或 `"unlink"` |
| `started`   | 一次关联尝试正在进行             | —                                 |
| `selection` | 你的用户到达了选择器或表单          | `step`                            |
| `success`   | 账号已关联**且已保存**          | `displayName`（未知时省略）、`refId`      |
| `unlinked`  | 已关联的账号被移除，且移除已保存       | —                                 |
| `error`     | 尝试失败                   | `code`、`message`、`reason`         |
| `cancelled` | 你的用户中途退出               | `reason`                          |
| `closed`    | 本次尝试使用的弹窗已关闭           | —                                 |
| `state`     | 某个网络的账号状态，在挂载时及每次变化时   | `state`、`since`                   |

**其中四个是结局**——`success`、`unlinked`、`error` 和 `cancelled`——每次尝试恰好到达一个。
`closed` 是跟在结局之后的生命周期通知，本身不是结局。

`click` 在任何关联工作开始**之前**触发，因此它也会报告那些随后被拦截的弹窗或失效会话拒绝的
点击。它是用于你自己的分析统计的事件；`started` 才意味着一次尝试真正在运行。

### Reason 取值

| 事件          | `reason`        | 含义                                 |
| ----------- | --------------- | ---------------------------------- |
| `error`     | `popupBlocked`  | 浏览器拒绝打开弹窗                          |
| `cancelled` | `popupClosed`   | 你的用户手动关闭了窗口                        |
| `cancelled` | `scopesDenied`  | 你的用户拒绝了网络请求的某项权限                   |
| `cancelled` | `userCancelled` | 你的用户中途退出，或你的代码调用了 `handle.close()` |
| `cancelled` | `superseded`    | 第二次 `popup()` 调用替换了这次尝试            |

<h3 id="error-has-two-sources">
  `error` 有两个来源
</h3>

其中只有一个是关联失败，且两者携带的字段不同。

* **关联错误**携带 `network`、`code` 以及它来源的 mount。
* **会话错误**——我们无法创建或刷新你的会话——只携带 `message`，因为当时没有任何东西在被关联。

请防御性地解构：在第二种情况下 `code` 和 `network` 为 `undefined`。

### `state` 让你免于轮询

`state` 是数据通道，而不是对某次尝试的报告。每个框架在挂载时会为每个网络发出一个 `state`，携带
该网络的当前状态以及它保持该状态起始的 `since` 时间戳；每当状态变化时再发出一个——**包括源自
我们这一侧的变化**，例如 token 失效进入需要重新关联的状态。因此你可以完全由小组件驱动你的 UI，
而无需轮询任何东西。

取值与 [`GET /profiles` 带 `include=state`](/docs/apis/profiles/get-profiles) 返回的枚举相同：
`linked`、`unlinked`、`identityVerificationRequired`、`restricted`、`rateLimited`、`suspended`。

## 解除关联

我们的卡片既能关联也能解除关联。你的用户点击一个已连接的网络并确认，账号即被移除：

1. `click` 触发，带 `action: "unlink"`。
2. 移除保存后，`unlinked` 触发。

**失败**的解除关联会报告 `error`，用户在确认步骤退出的会报告 `cancelled`。没有单独的
“解除关联失败”事件。

<h2 id="appearance">
  外观
</h2>

客户的样式表无法进入跨源框架，因此样式以数据的形式传输，由我们在框架内应用。将 `appearance`
作为 CSS 自定义属性传给 `init`；我们没有收到的每一项都保持默认值。

```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>
  **成对设置颜色。** 只设置背景 token 而不设置旁边的前景 token，是让它产生不可读结果的唯一方式：
  单独把 `--ayr-connect-surface-bg` 设为深色，而我们默认的 `--ayr-connect-surface-fg` 仍是深
  海军蓝。没有任何机制能替你推断出另一半。
</Warning>

完全不传 `appearance` 时，每个 token 都保持默认值，框架看起来是这样：

<div className="my-8">
  <Frame caption="默认 token：白色表面、深海军蓝文字、靛蓝强调色、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="三个使用默认外观的 Ayrshare Connect 卡片" width="920" height="528" data-path="images/multiple-users/connect-widget-appearance-default.webp" />
  </Frame>
</div>

传入几个 token，同样的框架就会呈现你的配色。下面的示例把表面变为淡蓝色，将文字和强调色加深以
匹配，并把圆角略微加大：

```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="应用上述 token 之后的同样三个卡片。">
    <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="三个 Ayrshare Connect 卡片，重新设计为淡蓝色表面和更深的蓝色强调色" width="920" height="574" data-path="images/multiple-users/connect-widget-appearance-themed.webp" />
  </Frame>
</div>

**只有一套配色，没有明暗预设。** 没有任何逻辑依赖 `prefers-color-scheme`，这是有意的——你页面
的主题未必与用户的操作系统一致，而媒体查询会悄悄推翻你选择的颜色。深色控制台通过提供深色取值来
设置主题。

这份 token 列表是我们跨版本保持的受支持契约。

### Token 列表

| Token                              | 默认值                                                       |
| ---------------------------------- | --------------------------------------------------------- |
| `--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`                                                    |

对其 token 而言不是合法 CSS 的值会**被忽略，并在你的控制台输出一条警告**，而不会被应用。这比
听起来更重要：给 `--ayr-connect-spacing` 传一个不带单位的 `8` 是个完全无辜的字符串，但它会使
读取它的所有计算失效并让布局塌陷，而且任何地方都不会报错。请给长度带上单位。

### 在交接屏幕上显示你自己的名称

在把用户交给社交网络之前，我们会显示一个简短的屏幕，标明他们正通过谁进行连接。两个钩子让你把它
变成自己的，而且两者都跨版本稳定。

`--ayr-connect-partner-name` 设置标签。它是唯一取值为**文本**的 token，因此必须作为 CSS 字符串
加引号——不带引号的值是非法的，什么也不会渲染：

```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 不是 token。我们把标志（mark）作为已定位、已设定尺寸的元素提供，你通过 `css` 用
`background-image` 来填充它，如上所示——我们不接受能从我们文档内部获取图片的 token，因此该请求
来自你编写的规则，而不是你传给我们的值。

<Note>
  `[data-ayr-connect-partner-mark]` 和 `[data-ayr-connect-partner-name]` 是下方自定义 CSS
  注意事项的**例外**：这两个选择器是契约的一部分，我们会跨版本保留它们。
</Note>

<h3 id="custom-css">
  自定义 CSS
</h3>

`css` 接受一个应用于每个框架内部的字符串，用于 token 覆盖不到的情况。

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

<Warning>
  **自定义 CSS 不跨版本受支持。** 它的选择器针对我们的内部标记，而内部标记会在版本之间变化——
  今天有效的规则可能在任何一次更新后悄悄失效。上面的 token 契约才是我们保留的部分。如果你依赖
  自定义 CSS，请固定一个版本。
</Warning>

弹窗会和框架一样继承实例的 `appearance` 和 `css`，因此你的用户不会在流程进行到一半时看到我们的
中性默认样式。

## 发布前值得了解的事

<AccordionGroup>
  <Accordion title="会话已过期的弹窗报告 cancelled 而不是 error">
    用 `popup()` 打开的弹窗无法被告知 token 被拒绝的原因——要说明原因，它就必须信任一个尚未
    验证的来源，而我们的安全模型不允许这样做。因此携带失效 token 的弹窗会关闭，并以带
    `reason: "popupClosed"` 的 `cancelled` 而非 `error` 呈现。

    实践中这种情况很少见：在静默刷新之前打开的弹窗会继续工作，因为它在打开时验证了自己的
    token。如果你看到无法解释的 `popupClosed` 结果，请检查你的 `session` 端点是否返回了新鲜的
    会话。
  </Accordion>

  <Accordion title="每个实例一个会话，而不是每次 mount 一个">
    十四个 mount 共享一个 token，只花费一次对你后端的调用，而不是十四次。如果你想要不同作用域的
    插槽——其中一些使用不同的 `allowedSocial`——请运行第二个带有自己会话的 `init`，而不要指望
    某个 mount 去收窄它。
  </Accordion>

  <Accordion title="框架只在你声明的 origin 上渲染">
    每个会话都携带你页面运行所在的 `origin`，框架在渲染任何内容之前会将嵌入它的页面与该值比对。
    嵌入在其他地方的框架保持空白且不发送任何事件。没有需要注册的允许列表，也没有需要配置的东西
    ——在创建会话时发送正确的 `origin` 即可。
  </Accordion>

  <Accordion title="高度已为你处理好，而且不是事件">
    框架向脚本报告自身高度，脚本据此调整框架大小。你的布局只管重排即可。没有可订阅的 resize
    事件，你这一侧也没有任何需要测量的东西。
  </Accordion>
</AccordionGroup>

## 要求

<ul className="custom-bullets">
  <li>
    **[Max Pack](/docs/additional/maxpack)**。小组件会话是 connect 模式的会话，没有 Max Pack 创建会
    返回 `code: 504`。如果你需要在没有 Max Pack 的账户上启用 connect 模式，请联系支持团队。
  </li>

  <li>
    每个会话都要有 **`origin`**——你页面运行所在的确切 origin。省略它返回 `code: 505`；不是
    `https` origin、自定义 scheme 或 `http://localhost` 的值返回 `code: 506`。
  </li>

  <li>
    会话上**不要有 `network`**。该参数会让会话变成[直连模式](/docs/multiple-users/connect-direct-mode)，
    而直连模式会话的 URL 不是脚本所期望的。
  </li>
</ul>

上述每个错误码都收录在 [Link Session 错误](/docs/errors/errors-ayrshare#link-session-errors)参考中，
包含 API 返回的确切消息以及应对方法。
