# Unif Umeng — 全文文档聚合 > 单文件聚合版。每段都带源路径与标题,方便整体粘贴给 LLM。 ## 目录 - Analytics - Common - Platform & ShareSheetHost - Share - 安装 - 快速上手 - 统计埋点 - 隐私合规(PIPL) - 分享 - 介绍 - Android 原生配置 - iOS 原生配置 - AI Skill - 测试(Mock) - 常见问题 ## Analytics *Source: `docs/api/analytics.md` · Slug: `/api/analytics`* # Analytics U-App 移动统计 API。三个方法都是**同步 `void`**,不返回 Promise。 ## 引用 {#import} ```ts import { Analytics } from '@unif/react-native-umeng'; ``` | 方法 | 签名 | 返回 | | --- | --- | --- | | [`onEvent`](#onevent) | `onEvent(eventId, params?)` | `void` | | [`signIn`](#signin) | `signIn(userId, provider?)` | `void` | | [`signOut`](#signout) | `signOut()` | `void` | :::warning 同步 void,不要 await `Analytics.*` 全是同步方法,**没有 Promise**。`await Analytics.onEvent(...)` 只会 await 一个 `undefined`,没有意义。 ::: --- ## `Analytics.onEvent(eventId, params?)` {#onevent} 自定义事件埋点。 ```ts function onEvent( eventId: string, params?: Record ): void; ``` | 参数 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `eventId` | `string` | ✅ | 友盟后台定义的事件 ID | | `params` | `Record` | — | 事件属性;`number` 值自动 `String()` stringify(友盟 iOS attributes 强制 `NSString`) | ```ts Analytics.onEvent('share_tap', { source: 'detail', count: 1 }); // count 自动转 '1' ``` --- ## `Analytics.signIn(userId, provider?)` {#signin} 用户登录账号埋点。 ```ts function signIn(userId: string, provider?: string): void; ``` | 参数 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `userId` | `string` | ✅ | 业务用户 ID | | `provider` | `string` | — | 登录方式标识,如 `'WX'` / `'DD'` | --- ## `Analytics.signOut()` {#signout} 用户登出。 ```ts function signOut(): void; ``` --- ## 平台支持 {#platform-support} | API | iOS | Android | | --- | --- | --- | | `onEvent` | ✅ | ✅ | | `signIn` | ✅ | ✅ | | `signOut` | ✅ | ✅ | > 埋点需要先完成 [`Common.init()`](./common#init) 才会真正上报;未 `init` 时调用不会崩溃,但数据不上报。 ## 相关 {#related} - [统计埋点指南](../guides/analytics) —— 任务导向用法 - [Common API](./common) —— 初始化(埋点上报的前提) ## Common *Source: `docs/api/common.md` · Slug: `/api/common`* # Common 两段式初始化 API,满足 PIPL 合规要求 —— 用户同意《隐私协议》前 native 不持有 appkey、不上报数据。 ## 引用 {#import} ```ts import { Common } from '@unif/react-native-umeng'; ``` | 方法 | 签名 | 返回 | | --- | --- | --- | | [`preInit`](#preinit) | `preInit(config: UmengInitConfig)` | `Promise` | | [`init`](#init) | `init()` | `Promise` | | [`isInited`](#isinited) | `isInited()` | `Promise` | --- ## `Common.preInit(config)` {#preinit} 预初始化友盟 SDK。可在用户同意《隐私协议》之前调用,推荐 **App 启动后立刻调**。 **行为**:存 config + 注册微信/钉钉平台(`PlatformConfig.setWeixin` / `setDing`),**不上报数据**。**idempotent** —— 重复调只触发一次(模块级 `preInitPromise` 缓存 + native `UmengBootstrap` 的 `@Volatile` 双重保险)。 ```ts function preInit(config: UmengInitConfig): Promise; ``` ### `UmengInitConfig` 字段 {#umenginitconfig} | 字段 | 类型 | 必填 | 默认 | 说明 | | --- | --- | --- | --- | --- | | `appkey` | `string` | ✅ | — | 友盟 appkey | | `channel` | `string` | — | iOS `'App Store'`、Android `'default'` | 渠道标识 | | `wechatAppId` | `string` | — | — | 微信平台 appid;不传则不注册微信分享 | | `wechatAppSecret` | `string` | — | — | 微信平台 appsecret;有 `wechatAppId` 才生效 | | `wechatUniversalLink` | `string` | — | — | 微信 Universal Link(1.8.6+ 强制);**仅 iOS 用**,有 `wechatAppId` 才生效 | | `dingtalkAppId` | `string` | — | — | 钉钉平台 appid;不传则不注册钉钉分享 | > 微信注册要求 `wechatAppId` **与** `wechatAppSecret` 同时非空才生效;钉钉只需 `dingtalkAppId`。 ### 抛出 {#preinit-errors} | `UmengError.code` | 触发 | | --- | --- | | `E_INVALID_OPTIONS` | `config.appkey` 缺失或为空(JS 层先校验,native 侧也会再校验) | | `E_UNKNOWN` | native preInit 其它失败 | --- ## `Common.init()` {#init} 正式启动数据采集。**必须在用户同意《隐私协议》之后调用**,且 `preInit(config)` 必须先调过。 ```ts function init(): Promise; ``` :::warning init 无参 config 全部交给 `preInit`,`init()` **不接收任何参数**。没先 `preInit` 直接 `init` 会 reject `E_INVALID_OPTIONS`。 ::: **行为**:调原生 `UMConfigure.init`(iOS `UMConfigure.initWithAppkey`),真正开始统计与上报。**idempotent** —— 重复调只触发一次。 ### 抛出 {#init-errors} | `UmengError.code` | 触发 | | --- | --- | | `E_INVALID_OPTIONS` | 未先 `preInit` 就调 `init`(iOS native error code `-3`) | | `E_UNKNOWN` | native init 其它失败 | --- ## `Common.isInited()` {#isinited} 查询是否已完成 `init()`(即数据采集是否已开始)。 ```ts function isInited(): Promise; ``` 返回 `true` 表示已 `init`;`preInit` 完成但尚未 `init` 时返回 `false`。 --- ## 最小用法 {#usage} ```ts import { Common } from '@unif/react-native-umeng'; // 1) App 启动:preInit —— 不上报,所有 config 都在这里给 await Common.preInit({ appkey: 'YOUR_APPKEY', wechatAppId: 'wxXXXXXXXX', wechatAppSecret: 'XXXXXXXX', wechatUniversalLink: 'https://your.host/', // 微信 1.8.6+(iOS) dingtalkAppId: 'dingoaXXXXXXXX', }); // 2) 用户同意《隐私协议》后:init() 无参,开始采集 await Common.init(); ``` --- ## 平台支持 {#platform-support} | API | iOS | Android | | --- | --- | --- | | `preInit()` | ✅(桥侧存 config + `setPlaform` 注册平台,不调 `UMConfigure.initWithAppkey`,无上报副作用) | ✅(`UMConfigure.preInit` + `PlatformConfig.setWeixin/setDing`) | | `init()` | ✅(`UMConfigure.initWithAppkey`) | ✅(`UMConfigure.init`) | | `isInited()` | ✅ | ✅ | > `wechatUniversalLink` 仅 iOS 生效(Android 无此概念)。 ## 相关 {#related} - [隐私合规(PIPL)](../guides/privacy-pipl) —— 两段式初始化的合规依据与时序 - [快速上手](../getting-started/quick-start) —— 完整接入流程 - [常见问题 → init 顺序](../troubleshooting#init-order) ## Platform & ShareSheetHost *Source: `docs/api/platform-sharesheethost.md` · Slug: `/api/platform-sharesheethost`* # Platform & ShareSheetHost 分享目标枚举与命令式面板的宿主组件。 ## 引用 {#import} ```ts import { Platform, ShareSheetHost, SUPPORTED_PLATFORMS, PLATFORM_DISPLAY_NAMES, PLATFORM_DEFAULT_SUBTITLES, PLATFORM_BRAND_COLORS, } from '@unif/react-native-umeng'; ``` --- ## `Platform` 枚举 {#platform-enum} 本桥首版支持的**分享目标平台**: ```ts enum Platform { WECHAT_SESSION = 'wechat_session', DINGTALK = 'dingtalk', } ``` | 枚举值 | 字符串值 | 说明 | | --- | --- | --- | | `Platform.WECHAT_SESSION` | `'wechat_session'` | 微信好友会话 / 群聊 | | `Platform.DINGTALK` | `'dingtalk'` | 钉钉工作群 / 好友 | :::danger 不是 react-native 的 Platform umeng 的 `Platform` 是**分享目标枚举**,**没有 `.OS`**。判断操作系统请用 `react-native` 的 `Platform`;同时用到两者时起别名: ```ts import { Platform } from 'react-native'; // OS 判断 import { Platform as ShareTarget } from '@unif/react-native-umeng'; // 分享目标 ``` ::: ### 相关常量 {#platform-constants} | 常量 | 类型 | 说明 | | --- | --- | --- | | `SUPPORTED_PLATFORMS` | `ReadonlyArray` | 支持的平台列表(也是面板默认渲染顺序):`[WECHAT_SESSION, DINGTALK]` | | `PLATFORM_DISPLAY_NAMES` | `Record` | 显示名:微信 / 钉钉 | | `PLATFORM_DEFAULT_SUBTITLES` | `Record` | 面板默认副标题:`'发送给好友或群'` / `'发送至工作群'` | | `PLATFORM_BRAND_COLORS` | `Record` | 品牌色:`#07C160`(微信)/ `#2595E8`(钉钉),用作面板平台前导小块实色 | --- ## `` {#share-sheet-host} 分享面板的宿主组件,**无任何 props**。订阅模块级 `shareSheetController`,在 `Share.openSheet()` 触发时渲染 design 的 `BottomSheet` + `Cell` 面板。 ```ts const ShareSheetHost: React.FC; // 无 props ``` ### 挂载 {#mount} **必须在 App 根挂载一次**,且位于 `GestureHandlerRootView` 和 design 的 `ThemeProvider` 内部(面板用 `useTheme` / `BottomSheet`): ```tsx import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { ThemeProvider } from '@unif/react-native-design'; import { ShareSheetHost } from '@unif/react-native-umeng'; export default function App() { return ( ); } ``` :::warning openSheet 依赖此宿主 未挂载时 `Share.openSheet()` 立即 reject(`E_UNKNOWN`,message `No mounted`);**一次只能开一个面板**,重入会 reject。挂载细节见[快速上手](../getting-started/quick-start#mount-host)。 ::: > 测试时官方 mock 的 `ShareSheetHost` 渲染 `null`(不引 design),见[测试](../testing)。 --- ## 平台支持 {#platform-support} | | iOS | Android | | --- | --- | --- | | `Platform` 枚举 / 常量 | ✅ | ✅ | | `` | ✅ | ✅ | ## 相关 {#related} - [Share API](./share#opensheet) —— `openSheet` / `shareXxx` 用法与参数 - [分享指南](../guides/sharing) —— 任务导向用法与坑 - [快速上手](../getting-started/quick-start#mount-host) —— 宿主挂载位置 ## Share *Source: `docs/api/share.md` · Slug: `/api/share`* # Share 分享 API,支持**命令式面板**(推荐)和**直拉单平台**两种模式,目标平台为微信会话 / 钉钉。 ## 引用 {#import} ```ts import { Share } from '@unif/react-native-umeng'; ``` | 方法 | 签名 | 返回 | | --- | --- | --- | | [`openSheet`](#opensheet) | `openSheet(payload, options?)` | `Promise` | | [`shareText`](#sharetext) | `shareText(options: ShareTextOptions)` | `Promise` | | [`shareImage`](#shareimage) | `shareImage(options: ShareImageOptions)` | `Promise` | | [`shareLink`](#sharelink) | `shareLink(options: ShareLinkOptions)` | `Promise` | | [`isInstalled`](#isinstalled) | `isInstalled(platform: Platform)` | `Promise` | | [`listPlatforms`](#listplatforms) | `listPlatforms()` | `Promise` | :::danger 取消 / 失败走 reject,不走 resolve `openSheet` 与所有 `shareXxx` **只有成功才 resolve**(resolve 到手的 `ShareResult.code` 恒为 `'success'`);用户取消、分享失败、目标未安装都会**抛 `UmengError`**。永远 `try/catch`,详见[取消与失败的处理](#reject-on-cancel)。 ::: --- ## `Share.openSheet(payload, options?)` {#opensheet} 命令式拉起分享面板(**推荐用法**)。需在 App 根挂载 ``,否则 Promise 立即 reject(`E_UNKNOWN`,message `No mounted`)。一次只能开一个面板,重入直接 reject。 ```ts function openSheet( payload: ShareSheetPayload, options?: ShareSheetOptions ): Promise; ``` ### `ShareSheetPayload` {#sharesheetpayload} 判别联合,`type` 决定其余字段: ```ts type ShareSheetPayload = | { type: 'text'; text: string } | { type: 'image'; image: string; thumb?: string } | { type: 'link'; title: string; url: string; description?: string; thumb?: string }; ``` | `type` | 字段 | 必填 | 说明 | | --- | --- | --- | --- | | `'text'` | `text: string` | ✅ | 纯文字内容 | | `'image'` | `image: string` | ✅ | 图片网络 URL(本地路径 / base64 暂不支持) | | | `thumb?: string` | — | 缩略图 | | `'link'` | `title: string` | ✅ | 链接标题 | | | `url: string` | ✅ | 链接 URL | | | `description?: string` | — | 链接描述 | | | `thumb?: string` | — | 缩略图 | ### `ShareSheetOptions` {#sharesheetoptions} | 字段 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `title` | `string` | `'分享至'` | 面板标题 | | `cancelText` | `string` | `'取消'` | 取消按钮文案 | | `subtitles` | `Partial>` | 见 `PLATFORM_DEFAULT_SUBTITLES` | 各平台副标题覆盖 | | `hideUninstalled` | `boolean` | `false` | `true` 隐藏未安装平台;默认显示但置灰 | ```tsx import { Share, UmengError } from '@unif/react-native-umeng'; try { const r = await Share.openSheet( { type: 'link', title: '问问看', url: 'https://example.com', description: '一句话描述' }, { title: '分享到', hideUninstalled: true } ); // r.code === 'success' } catch (e) { if (e instanceof UmengError && e.code === 'E_USER_CANCEL') { /* 用户取消,通常静默 */ } } ``` --- ## `Share.shareText(options)` {#sharetext} 直拉单平台分享纯文字,跳过面板。 ```ts function shareText(options: ShareTextOptions): Promise; ``` | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `platform` | `Platform` | ✅ | 目标平台([`Platform` 枚举](./platform-sharesheethost#platform-enum)) | | `text` | `string` | ✅ | 文字内容 | `text` 为空时抛 `E_INVALID_OPTIONS`。 --- ## `Share.shareImage(options)` {#shareimage} 直拉单平台分享图片。 ```ts function shareImage(options: ShareImageOptions): Promise; ``` | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `platform` | `Platform` | ✅ | 目标平台 | | `image` | `string` | ✅ | 图片网络 URL(本地路径 / base64 暂不支持) | | `thumb` | `string` | — | 缩略图 URL | `image` 为空时抛 `E_INVALID_OPTIONS`。本地图(截图 / 相册路径 / base64)传不进原生层 —— 需先上传拿到 `https://` URL 再分享。 --- ## `Share.shareLink(options)` {#sharelink} 直拉单平台分享图文链接。 ```ts function shareLink(options: ShareLinkOptions): Promise; ``` | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `platform` | `Platform` | ✅ | 目标平台 | | `title` | `string` | ✅ | 链接标题 | | `url` | `string` | ✅ | 链接 URL | | `description` | `string` | — | 链接描述 | | `thumb` | `string` | — | 缩略图 URL | `title` 或 `url` 为空时抛 `E_INVALID_OPTIONS`。 --- ## `Share.isInstalled(platform)` {#isinstalled} 查询指定平台 App 是否已安装。 ```ts function isInstalled(platform: Platform): Promise; ``` > Android 在无前台 Activity 时无法可靠判断,保守返回 `false`。 --- ## `Share.listPlatforms()` {#listplatforms} 获取全部支持平台的安装状态列表(按 `SUPPORTED_PLATFORMS` 顺序)。`` 用它渲染面板的平台条目。 ```ts function listPlatforms(): Promise; ``` 返回 `PlatformInfo[]`: | 字段 | 类型 | 说明 | | --- | --- | --- | | `platform` | `Platform` | 平台枚举值 | | `installed` | `boolean` | 是否已安装 | | `displayName` | `string` | 平台显示名称(如 `'微信'` / `'钉钉'`) | --- ## 返回值 `ShareResult` {#shareresult} ```ts interface ShareResult { code: 'success' | 'cancel' | 'failed'; platform: Platform; message?: string; } ``` | `code` | 何时出现 | | --- | --- | | `'success'` | 唯一会 **resolve** 的取值 —— resolve 到手的 `ShareResult` 必为它 | | `'cancel'` / `'failed'` | **不会 resolve** —— JS 层翻成 `UmengError`(见下)抛出,存在于 `ShareResult` 仅为类型完整 | --- ## 取消与失败:reject-on-cancel {#reject-on-cancel} `openSheet` 与所有 `shareXxx` 把 native 的 `cancel` / `failed` 翻成 `UmengError` **抛出**,只有 `success` 才 resolve: ```ts // ❌ Incorrect:取消 / 失败不会 resolve,判别分支永远到不了 const r = await Share.openSheet(payload); if (r.code === 'cancel') { /* 永远到不了 */ } ``` ```ts // ✅ Correct:try/catch 看 e.code;resolve 的 r.code 必为 'success' try { const r = await Share.openSheet(payload); // r.code === 'success' } catch (e) { if (e instanceof UmengError) { switch (e.code) { case 'E_USER_CANCEL': /* 用户取消,静默 */ break; case 'E_PLATFORM_NOT_INSTALLED': /* 目标 App 未安装 */ break; case 'E_SHARE_FAILED': /* 分享失败,查 e.message */ break; } } } ``` 可能抛出的 `UmengError.code`: | `code` | 触发 | | --- | --- | | `E_USER_CANCEL` | 用户点取消 / 点遮罩 / 平台侧取消 | | `E_SHARE_FAILED` | 分享失败(未配 URL Scheme、网络错、内容不合规等) | | `E_PLATFORM_NOT_INSTALLED` | 目标微信 / 钉钉未安装(面板内点击未安装平台时) | | `E_PLATFORM_NOT_SUPPORTED` | 传了不在 `SUPPORTED_PLATFORMS` 的平台 | | `E_INVALID_OPTIONS` | 必填字段缺失(`shareText` 缺 `text`、`shareLink` 缺 `title`/`url` 等) | | `E_UNKNOWN` | 未挂 Host、面板重入、Android 无前台 Activity、SDK 未知错 | 完整错误码表见[常见问题 → 错误码速查](../troubleshooting#error-codes)。 --- ## 平台支持 {#platform-support} | API | iOS | Android | | --- | --- | --- | | `openSheet` | ✅ | ✅ | | `shareText` / `shareImage` / `shareLink` | ✅ | ✅ | | `isInstalled` | ✅ | ✅ | | `listPlatforms` | ✅ | ✅ | ## 相关 {#related} - [分享指南](../guides/sharing) —— 任务导向用法、payload 类型、坑 - [Platform & ShareSheetHost](./platform-sharesheethost) —— `Platform` 枚举 + 宿主组件挂载 - [常见问题](../troubleshooting#error-codes) —— 错误码速查与分享无回调排障 ## 安装 *Source: `docs/getting-started/installation.md` · Slug: `/getting-started/installation`* # 安装 装齐 `@unif/react-native-umeng` 的同伴包,完成 iOS 编译,再做一步原生回调配置。**分享面板的 UI 依赖 `@unif/react-native-design`,peerDeps 缺一即崩** —— 本页以 `package.json` 的 `peerDependencies` 为准逐项列出。 ## 环境要求 | 要求 | 版本 | | --- | --- | | React Native | **0.85+**(仅新架构 Fabric + TurboModules) | | React | 19+ | | iOS | 由友盟 U-Share / U-App SDK 决定 | | Android | 由友盟 U-Share / U-App SDK 决定 | :::info 仅支持新架构 本库是 TurboModule 桥,**仅支持 React Native 0.85+ 新架构**。旧架构(Bridge)不在目标范围。 ::: --- ## 1. 安装依赖 {#安装依赖} 以下同伴包**全部必装**(以 `package.json` 的 `peerDependencies` 为准): ```sh yarn add @unif/react-native-umeng \ @unif/react-native-design \ react-native-gesture-handler \ react-native-svg ``` 各包的作用与版本约束: | 包 | 版本约束 | 作用 | | --- | --- | --- | | `@unif/react-native-design` | `>=0.1.2` | 分享面板 UI(`Cell` / `Button` / `useThemedStyles`),**必装**(见下) | | `react-native-gesture-handler` | `>=2.21.0` | design 组件的手势底层(design 的 peer,连带必装) | | `react-native-svg` | `>=15` | 面板平台图标 | > `react` / `react-native` 也声明为 peer(`*`),任何 RN 工程都已自带,无需为本库单独安装。 :::warning `@unif/react-native-design` 是分享面板的 UI 壳,不能省 `` 面板本体是 RN `Modal` 底部弹层,内部用 design 的 `Cell` / `Button` / `useThemedStyles` 渲染,并且**必须在 design 的 `ThemeProvider` 与 `GestureHandlerRootView` 内**才能工作(design 组件的触摸经 gesture-handler)。这套挂载约定见[快速上手](./quick-start)。缺 design 或缺 `ThemeProvider`,分享面板无法渲染。 ::: :::note 只用统计、不用分享? 即使如此,`@unif/react-native-design` 等仍是 `peerDependencies`(非可选),包管理器会要求安装齐全。统计(`Analytics.*`)本身不依赖 design,但建议按上表装齐以避免 peer 告警。 ::: --- ## 2. iOS:pod install 安装或升级依赖后,**必须重新执行 pod install**: ```sh cd ios && bundle exec pod install ``` 完成后用 Xcode 或 `npx react-native run-ios` 重新编译运行。 :::warning 模拟器跑不起分享 友盟 U-Share 在 Apple Silicon Mac 模拟器上有 `EXCLUDED_ARCHS=arm64` 限制,且模拟器没有真微信 / 钉钉。**分享一律真机验证**,详见[常见问题](../troubleshooting)。 ::: --- ## 3. Android Android 端依赖随 Gradle 自动同步,无需手动 link。直接 `npx react-native run-android` 即可编译。 > 微信 / 钉钉的分享回调 Activity 需要落在宿主包名下,见下一步原生配置。 --- ## 4. 原生回调配置(必做) 分享后能否跳回 App,完全取决于原生侧的 URL Scheme / 回调 Activity 注册。**模板不要凭记忆编**,按对应平台文档逐项配置: - [iOS 原生配置](../native-setup/ios) —— `Info.plist` 的 `LSApplicationQueriesSchemes` / `CFBundleURLTypes`,`AppDelegate` 转发 `handleOpen(_:options:)`,微信 Universal Link。 - [Android 原生配置](../native-setup/android) —— `WXEntryActivity` / `DDShareActivity` 必须在**宿主包名**下,钉钉 `appId` 与 JS 侧 `preInit({ dingtalkAppId })` 一致。 > 友盟 `appkey` / `appsecret` / Universal Link 等都通过 JS `Common.preInit(config)` 传,**不写在 Info.plist / Gradle**。 --- ## 下一步 - [快速上手](./quick-start) —— 5 分钟跑通初始化 + 第一次分享 - [指南 → 隐私合规(PIPL)](../guides/privacy-pipl) —— preInit / init 两段式时序 - [API 参考 → Common](../api/common) —— preInit / init / isInited 完整参数 ## 快速上手 *Source: `docs/getting-started/quick-start.md` · Slug: `/getting-started/quick-start`* # 快速上手 5 分钟跑通:根挂 `` → 启动时 `preInit` → 用户同意后 `init` → `await Share.openSheet()` 拉起面板。 :::warning 分享必须真机运行 分享会调起原生微信 / 钉钉,**模拟器没有真 App,无法完成回调跳转**(属预期行为)。先完成[安装](./installation)(peerDeps + `pod install` + 原生回调配置)再运行本例。 ::: --- ## ① 在 App 根挂 `` {#mount-host} `` 是命令式分享面板的宿主,**必须在 App 根挂一次**,且位于 `GestureHandlerRootView` 和 design 的 `ThemeProvider` 内: ```tsx title="App.tsx(或根组件)" import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { ThemeProvider } from '@unif/react-native-design'; import { ShareSheetHost } from '@unif/react-native-umeng'; export default function App() { return ( {/* 根上挂一次,位置不影响显示(打开时全屏覆盖) */} ); } ``` 不挂 Host,`Share.openSheet()` 会立即 reject(`No mounted`)。已用 design 系统其他组件而挂过 `ThemeProvider` 的,沿用即可。 ## ② App 启动后立刻 `preInit`(此时不上报) {#preinit} `Common.preInit(config)` 只存配置、注册微信 / 钉钉平台,**不上报任何数据**,因此可以(也应该)在用户同意《隐私协议》之前调。**所有配置都在这里给**: ```ts import { Common } from '@unif/react-native-umeng'; await Common.preInit({ appkey: 'YOUR_UMENG_APPKEY', // 必填 channel: 'App Store', // 可选,默认 iOS='App Store' / Android='default' wechatAppId: 'wxXXXXXXXX', // 不传则不注册微信分享 wechatAppSecret: 'XXXXXXXX', // 有 wechatAppId 才生效 wechatUniversalLink: 'https://your.host/', // 微信 1.8.6+(iOS)要求 dingtalkAppId: 'dingoaXXXXXXXX', // 不传则不注册钉钉分享 }); ``` ## ③ 用户同意后,`init` 开始采集(无参) {#init} ```ts // 仅在用户点「同意《隐私协议》」之后调用 await Common.init(); // ⚠️ 无参 —— config 已给 preInit ``` `Common.init()` **不接收 config**(配置已给 `preInit`)。没先 `preInit` 直接 `init` 会 reject。两段式合规细节见[隐私合规(PIPL)](../guides/privacy-pipl)。 ## ④ 拉起分享面板 {#open-sheet} ```tsx import { Share, UmengError } from '@unif/react-native-umeng'; async function onShareTap() { try { const r = await Share.openSheet({ type: 'link', title: '问问看', url: 'https://example.com', description: '一句话描述', }); // 走到这里说明分享成功:r.code 恒为 'success' console.log(r.platform); // 'wechat_session' | 'dingtalk' } catch (e) { if (e instanceof UmengError && e.code === 'E_USER_CANCEL') { // 用户取消,通常静默 } // 其它如 E_SHARE_FAILED / E_PLATFORM_NOT_INSTALLED:兜底提示 } } ``` :::danger 取消 / 失败走 reject,不走 resolve `Share.openSheet()` **只有成功才 resolve**(`r.code` 恒为 `'success'`);用户取消、分享失败都会**抛 `UmengError`**。务必 try/catch,**不要写 `if (r.code === 'cancel')`**(永远到不了)。详见[分享指南](../guides/sharing)。 ::: --- ## 统计埋点(可选) 初始化完成后即可埋点。`Analytics.*` 都是**同步 `void`,不要 await**: ```ts import { Analytics } from '@unif/react-native-umeng'; Analytics.onEvent('share_click', { source: 'detail', count: 1 }); // 数字自动转字符串 Analytics.signIn('user-123', 'WX'); // provider 可选 Analytics.signOut(); ``` --- ## 下一步 - [指南 → 分享](../guides/sharing) —— 面板 vs 直拉、内容类型、取消失败处理 - [指南 → 统计埋点](../guides/analytics) —— `onEvent` / `signIn` / `signOut` 详解 - [指南 → 隐私合规(PIPL)](../guides/privacy-pipl) —— 两段式初始化时序 - [API 参考 → Common](../api/common) —— preInit / init / isInited 完整参数 ## 统计埋点 *Source: `docs/guides/analytics.md` · Slug: `/guides/analytics`* # 统计埋点 本页介绍如何用 `@unif/react-native-umeng` 记录 U-App 移动统计 —— 自定义事件与账号登录 / 登出埋点。 :::warning 三个方法都是同步 `void`,不要 await `Analytics.onEvent` / `signIn` / `signOut` **没有返回 Promise**,直接同步调用即可。`await` 一个 `undefined` 没有意义。 ::: --- ## 自定义事件 {#on-event} ```ts import { Analytics } from '@unif/react-native-umeng'; Analytics.onEvent('login'); // 无参数 Analytics.onEvent('share_click', { source: 'detail' }); // 字符串参数 Analytics.onEvent('page_view', { page: 'home', duration: 30 }); // 含数字参数 ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | `eventId` | `string` | 事件 ID(需与友盟后台配置一致) | | `params` | `Record?` | 事件属性,可选 | :::note 数字参数自动字符串化 `params` 里 value 为 `number` 时会自动 `String()` 转成字符串 —— 友盟 iOS 的 attributes 强制要求 `NSString`。所以 `{ duration: 30 }` 上报时是 `"30"`,你不必手动转。 ::: --- ## 账号登录埋点 {#sign-in} ```ts Analytics.signIn('user-123', 'WX'); // provider 可选 ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | `userId` | `string` | 业务用户 ID | | `provider` | `string?` | 登录方式标识,如 `'WX'` / `'DD'` | ## 账号登出埋点 {#sign-out} ```ts Analytics.signOut(); ``` --- ## 易错点:不要 await `Analytics.*` {#no-await} ```ts // ❌ Incorrect:它们是同步 void,await 一个 undefined 没意义 await Analytics.onEvent('login'); const ok = await Analytics.signIn('user-123'); // ok 永远是 undefined ``` ```ts // ✅ Correct:直接同步调用 Analytics.onEvent('login'); Analytics.signIn('user-123', 'WX'); Analytics.signOut(); ``` --- :::warning 采集需先 `Common.init()` `Analytics.*` 的数据上报依赖 `Common.init()` 已调过。若 `init` 未执行(用户未同意《隐私协议》),调用**不会崩溃**,但数据不会上报。两段式合规见[隐私合规(PIPL)](./privacy-pipl)。 ::: ## 相关 - [Analytics API](../api/analytics) —— 完整签名 - [Common API](../api/common) —— preInit / init / isInited ## 隐私合规(PIPL) *Source: `docs/guides/privacy-pipl.md` · Slug: `/guides/privacy-pipl`* # 隐私合规(PIPL) 本库的初始化采用 **preInit → init** 两段式,满足 PIPL(个人信息保护法)对「用户同意前不采集」的要求。本页讲清两段的职责、时序与两个易错点。 --- ## 两段式设计 {#two-phase} | 阶段 | 调用时机 | 行为 | | --- | --- | --- | | `Common.preInit(config)` | App 启动后立刻,**可在用户同意之前** | 存 config + 注册微信 / 钉钉平台,**不上报任何数据** | | `Common.init()` | 用户点「同意《隐私协议》」之后 | 正式启动友盟数据采集 | ```ts import { Common } from '@unif/react-native-umeng'; // 1) App 启动:preInit —— 不上报,所有 config 都在这里给 await Common.preInit({ appkey: 'YOUR_UMENG_APPKEY', wechatAppId: 'wxXXXX', wechatAppSecret: 'XXXX', wechatUniversalLink: 'https://your.host/', // 微信 1.8.6+(iOS)要求 dingtalkAppId: 'dingoaXXXX', }); // 2) 用户同意《隐私协议》之后:开始采集 await Common.init(); // ⚠️ 无参 —— config 已给 preInit ``` > 两个方法都是 **idempotent**(幂等),重复调只触发一次,可放心多次调用。 :::danger init 必须在用户同意之后 `Common.init()` **必须在用户明确同意《隐私协议》之后**才能调用。隐私弹窗弹出前、或用户拒绝时,不可调 `init()`。违反将导致合规风险。 ::: --- ## 推荐接入时序 {#timeline} ``` App 启动 └─▶ Common.preInit(config) // 立刻调,注册微信/钉钉,不采集 用户进入隐私协议弹窗 └─▶ 用户点「同意」 └─▶ Common.init() // 仅此刻调,开始采集 ``` --- ## iOS / Android 差异 {#platform-notes} - **Android** —— Native 侧在 `preInit` 时调友盟官方 `UMConfigure.preInit(context, appkey, channel)` 并注册微信 / 钉钉平台,行为与 JS `Common.preInit()` 一致 —— **不上报**;`init` 时才调 `UMConfigure.init` 启动采集。 - **iOS** —— 友盟 iOS SDK **没有 preInit 接口**。`preInit` 只把 config 存进 native 状态并跑平台配置,真正的 `UMConfigure.initWithAppkey` **推迟到 `init()` 才执行**。因此 `Common.init()` 调用之前,桥不会调任何会上报的友盟 iOS API。 无论哪个平台,只要保证 `init()` 在用户同意后调,即满足合规要求。 --- ## 易错点(Incorrect / Correct) {#gotchas} ### 1. `init` 带参 {#init-args} 配置只给 `preInit`,`init()` **无参**: ```ts // ❌ Incorrect:init 不接收 config await Common.init({ appkey: '...' }); ``` ```ts // ✅ Correct:config 给 preInit,init 无参 await Common.preInit({ appkey: '...', /* ... */ }); await Common.init(); ``` ### 2. 没先 `preInit` 就 `init`(或 init 早于同意) {#init-order} `init` 前必须先 `preInit`,否则会 reject(`E_INVALID_OPTIONS`): ```ts // ❌ Incorrect:没 preInit 直接 init —— reject;或在用户同意前就 init —— 违规 await Common.init(); ``` ```ts // ✅ Correct:启动 preInit;用户同意后才 init await Common.preInit({ appkey: '...' }); // App 启动 // …… 展示隐私弹窗,用户点「同意」…… await Common.init(); // 同意后 ``` --- ## 相关 - [快速上手](../getting-started/quick-start#preinit) —— preInit / init 在启动流程中的位置 - [Common API](../api/common) —— preInit / init / isInited 完整参数 ## 分享 *Source: `docs/guides/sharing.md` · Slug: `/guides/sharing`* # 分享 本页介绍如何用 `@unif/react-native-umeng` 把内容分享到**微信会话**或**钉钉** —— 命令式分享面板、直拉单平台、三种内容类型,以及取消 / 失败的正确处理。 :::info 首版只支持微信会话 + 钉钉 `Platform` 枚举只有 `WECHAT_SESSION`(微信会话)与 `DINGTALK`(钉钉)。**没有朋友圈 / QQ / 微博**;传未支持的平台会抛 `E_PLATFORM_NOT_SUPPORTED`。 ::: --- ## 命令式面板(推荐) {#open-sheet} `Share.openSheet(payload, options?)` 拉起 design 的 `BottomSheet` 分享面板,用户选平台后发起分享: ```tsx import { Share, UmengError } from '@unif/react-native-umeng'; async function onShareTap() { try { const r = await Share.openSheet({ type: 'link', title: '问问看', url: 'https://example.com', description: '一句话描述', }); // 成功:r = { code: 'success', platform: 'wechat_session' | 'dingtalk', message? } } catch (e) { if (e instanceof UmengError && e.code === 'E_USER_CANCEL') { // 用户取消,通常静默 } // 其它兜底见下「取消与失败」 } } ``` 面板的外观文案可用第二参 `options` 覆盖: | 字段 | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | `title` | `string` | `'分享至'` | 面板标题 | | `cancelText` | `string` | `'取消'` | 取消按钮文案 | | `subtitles` | `Partial>` | 内置 | 各平台副标题覆盖 | | `hideUninstalled` | `boolean` | `false` | `true` 隐藏未安装平台(默认置灰) | > `` 必须先在 App 根挂一次,否则 `openSheet` 立即 reject。挂载方式见[快速上手](../getting-started/quick-start#mount-host)。 --- ## 直拉单平台(跳过面板) {#share-xxx} 不需要面板、想直接发到某个平台时,用 `shareText` / `shareImage` / `shareLink`。它们与面板共用同一套逻辑,但需显式传 `platform`: ```tsx import { Share, Platform } from '@unif/react-native-umeng'; // 链接 await Share.shareLink({ platform: Platform.WECHAT_SESSION, title: '标题', url: 'https://example.com', description: '描述', // 可选 thumb: 'https://example.com/thumb.png', // 可选 }); // 纯文本 await Share.shareText({ platform: Platform.DINGTALK, text: '纯文字内容' }); // 图片 await Share.shareImage({ platform: Platform.WECHAT_SESSION, image: 'https://example.com/pic.png', thumb: 'https://example.com/thumb.png', // 可选 }); ``` > 直拉变体同样**取消 / 失败 reject、成功 resolve**(见下)。想先判断目标 App 是否安装,用 `await Share.isInstalled(Platform.WECHAT_SESSION)`。 --- ## 三种内容类型 {#payload-types} `openSheet` 的 `payload` 是一个判别联合,`type` 取 `'text' | 'image' | 'link'`: | `type` | 字段 | 说明 | | --- | --- | --- | | `'text'` | `text` | 纯文本 | | `'image'` | `image`、`thumb?` | 图片(**仅网络 URL**;本地路径 / base64 传不进原生层,暂不支持) | | `'link'` | `title`、`url`、`description?`、`thumb?` | 图文链接 | 直拉变体一一对应:`shareText({ platform, text })`、`shareImage({ platform, image, thumb? })`、`shareLink({ platform, title, url, description?, thumb? })`。完整参数表见 [Share API](../api/share)。 --- ## 取消与失败:走 reject,不走 resolve {#reject-on-cancel} 这是最容易踩的坑。`openSheet` 与所有 `shareXxx` **只有成功才 resolve**(resolve 到手的 `r.code` 恒为 `'success'`);用户取消、分享失败、目标未安装都会**抛 `UmengError`**: ```ts // ❌ Incorrect:取消 / 失败不会 resolve,这样永远判不到 const r = await Share.openSheet(payload); if (r.code === 'cancel') { /* 永远到不了 */ } if (r.code === 'failed') { /* 永远到不了 */ } ``` ```ts // ✅ Correct:try/catch 看 e.code;resolve 的 r.code 必为 'success' import { Share, UmengError } from '@unif/react-native-umeng'; try { const r = await Share.openSheet(payload); // r.code === 'success' } catch (e) { if (e instanceof UmengError) { switch (e.code) { case 'E_USER_CANCEL': /* 用户取消,静默 */ break; case 'E_PLATFORM_NOT_INSTALLED': /* 目标 App 未安装 */ break; case 'E_SHARE_FAILED': /* 分享失败,查 e.message */ break; // 其它见下表 } } } ``` 可能抛出的 `UmengError.code`: | `code` | 触发 | | --- | --- | | `E_USER_CANCEL` | 用户点取消 / 点遮罩 / 平台侧取消 | | `E_SHARE_FAILED` | 分享失败(未配 URL Scheme、网络错、内容不合规等) | | `E_PLATFORM_NOT_INSTALLED` | 目标微信 / 钉钉未安装 | | `E_PLATFORM_NOT_SUPPORTED` | 传了不在白名单的平台 | | `E_INVALID_OPTIONS` | 必填字段缺失(如 `shareLink` 缺 `title` / `url`) | | `E_UNKNOWN` | 未挂 Host、面板重入、或无法归类的错误 | 完整错误码与排障见[常见问题](../troubleshooting)。 --- ## 易错点(Incorrect / Correct) {#gotchas} ### 1. 没挂 `` {#no-host} ```tsx // ❌ Incorrect:没挂 Host,openSheet 立即 reject(E_UNKNOWN: "No mounted") ``` ```tsx // ✅ Correct:App 根挂一次(且一次只能开一个面板,重入会 reject) ``` 挂载位置与 `GestureHandlerRootView` / `ThemeProvider` 嵌套关系见[快速上手](../getting-started/quick-start#mount-host)。 ### 2. 把 umeng 的 `Platform` 当成 React Native 的 {#platform-confusion} umeng 的 `Platform` 是**分享目标枚举**(`wechat_session` / `dingtalk`),**不是** `react-native` 的 `Platform`,它**没有 `.OS`**: ```ts // ❌ Incorrect:这不是 react-native 的 Platform import { Platform } from '@unif/react-native-umeng'; if (Platform.OS === 'ios') {} // 没有 .OS ``` ```ts // ✅ Correct:判 OS 用 RN 的;分享目标用 umeng 的,混用时起别名 import { Platform } from 'react-native'; // OS 判断 import { Platform as ShareTarget } from '@unif/react-native-umeng'; // 分享目标 if (Platform.OS === 'ios') { /* ... */ } await Share.shareText({ platform: ShareTarget.WECHAT_SESSION, text: '…' }); ``` --- ## 相关 - [Share API](../api/share) —— 全量 `options` 参数表与返回类型 - [Platform & ShareSheetHost](../api/platform-sharesheethost) —— `Platform` 枚举 + `ShareSheetHost` - [常见问题](../troubleshooting) —— 分享无回调 / 错误码 / 真机限制 ## 介绍 *Source: `docs/intro.md` · Slug: `/intro`* # @unif/react-native-umeng 友盟(U-Share + U-App 移动统计)的 **React Native 新架构桥**:一次 `await Share.openSheet(payload)` 拉起分享面板,一行 `Analytics.onEvent(id)` 记录埋点,统计采集遵循 PIPL 两段式合规。 [![npm](https://img.shields.io/npm/v/@unif/react-native-umeng.svg?color=cb3837&logo=npm)](https://www.npmjs.com/package/@unif/react-native-umeng) [![CI](https://github.com/unif-design/react-native-umeng/actions/workflows/ci.yml/badge.svg)](https://github.com/unif-design/react-native-umeng/actions/workflows/ci.yml) [![License](https://img.shields.io/npm/l/@unif/react-native-umeng.svg?color=blue)](https://github.com/unif-design/react-native-umeng/blob/main/LICENSE) [![Docs](https://img.shields.io/badge/docs-unif--design.github.io-orange.svg)](https://unif-design.github.io/react-native-umeng/) ## 这个库是什么 它在友盟官方 SDK(U-Share 社会化分享 + U-App 移动统计)之上,封装出一套**新架构(TurboModule)友好、UI 文案中文**的 React Native 接口。对外只有三个命名空间加一个宿主组件: - **`Common`** —— `preInit` / `init` / `isInited`,PIPL 两段式初始化。 - **`Share`** —— `openSheet`(命令式分享面板)/ `shareText` / `shareImage` / `shareLink` / `isInstalled` / `listPlatforms`。 - **`Analytics`** —— `onEvent` / `signIn` / `signOut`,自定义事件与账号埋点。 - **``** —— 命令式分享面板的宿主组件,在 App 根挂一次。 分享面板的 UI 直接复用 [`@unif/react-native-design`](https://www.npmjs.com/package/@unif/react-native-design) 的 `BottomSheet` + `Cell`,与 design 的 `toast()` / `confirm()` 同一套「单例 + Host」模式。 ## 解决什么问题 直接接友盟原生 SDK,你要分别处理 iOS / Android 的初始化时序、平台注册、分享回调跳回、PIPL 合规启停,还要自己拼一套分享面板 UI。本库把这些收敛成几次声明式调用: ```tsx import { Share, UmengError } from '@unif/react-native-umeng'; try { const r = await Share.openSheet({ type: 'link', title: '问问看', url: 'https://example.com' }); // 只有成功才走到这里:r.code 恒为 'success' } catch (e) { if (e instanceof UmengError && e.code === 'E_USER_CANCEL') { /* 用户取消 */ } } ``` ## 核心概念 - **首版只支持微信会话 + 钉钉** —— `Platform` 枚举只有 `WECHAT_SESSION` 与 `DINGTALK` 两个成员。**没有朋友圈、QQ、微博**;传未支持的平台会抛 `E_PLATFORM_NOT_SUPPORTED`。 - **分享面板 = 单例 controller + Host 组件** —— `Share.openSheet()` 是推荐用法,它经一个模块级单例拉起 `` 渲染的 `BottomSheet`。**Host 必须在 App 根挂一次**,否则 `openSheet` 立即 reject;一次只能开一个面板。 - **取消 / 失败走 reject,不走 resolve** —— `openSheet` 与 `shareText/shareImage/shareLink` 在用户取消或分享失败时**抛 `UmengError`**(`E_USER_CANCEL` / `E_SHARE_FAILED`)。**resolve 到手的结果 `code` 恒为 `'success'`** —— 永远 try/catch,别去判 `r.code === 'cancel'`(到不了)。 - **PIPL 两段式:`preInit`(不上报)→ 同意 →`init`(开始采集)** —— `Common.preInit(config)` 在 App 启动后立刻调,只存配置、注册平台,**不上报任何数据**;用户同意《隐私协议》后再调**无参**的 `Common.init()` 才正式启动采集。 - **统计是同步 `void`** —— `Analytics.onEvent / signIn / signOut` 没有 Promise,**不要 await**。 ## 能力 - **命令式分享面板** —— `Share.openSheet({ type, ... })` 一行拉起 `BottomSheet`,用户选平台,Promise resolve 成功 / reject 取消失败。 - **三种分享内容** —— 文本(`'text'`)、图片(`'image'`)、链接(`'link'`),面板与直拉变体共用同一套 `payload`。 - **直拉单平台** —— 不需要面板时,`Share.shareText / shareImage / shareLink` 跳过 UI 直接发到指定平台。 - **PIPL 合规初始化** —— `preInit` / `init` 两段式,满足「用户同意前不采集」。 - **自定义事件 + 账号埋点** —— `Analytics.onEvent / signIn / signOut`,数字参数自动字符串化。 - **官方 Jest mock** —— 随包导出 `./mock`,测试里整包替换,无需手写 stub。 ## 何时使用 | 适用 | 不适用 | | --- | --- | | 分享到**微信会话**或**钉钉**(文本 / 图片 / 链接) | 分享到**朋友圈 / QQ / 微博** —— 首版不支持 | | 需要一套现成的中文分享面板 UI | 完全自定义的分享 UI(可直拉 `shareXxx` 自绘) | | 友盟自定义事件 / 账号登录登出埋点 | 非友盟的统计后端 | | PIPL 两段式合规采集启停 | —— | ## 平台支持 | 平台 | 支持 | | --- | --- | | iOS | ✅ | | Android | ✅ | | Web / 模拟器 | ❌(分享需真机) | :::info 仅支持新架构 本库是 TurboModule 桥,**仅支持 React Native 0.85+ 新架构**、React 19。旧架构(Bridge)不在目标范围。 ::: :::warning 分享必须真机验证 分享会调起原生微信 / 钉钉,模拟器没有真 App 也无法完成回调跳转(iOS 还有 `EXCLUDED_ARCHS=arm64` 限制)。**分享改动一律真机验证 —— 这是预期行为,不是 bug。** ::: ## 下一步 - [安装](./getting-started/installation) —— 装齐 peerDeps、跑 pod install、原生配置指针 - [快速上手](./getting-started/quick-start) —— 5 分钟跑通初始化 + 第一次分享 - [指南 → 分享](./guides/sharing) —— 面板 / 直拉、内容类型、取消失败处理 - [指南 → 隐私合规(PIPL)](./guides/privacy-pipl) —— 两段式初始化时序 - [API 参考 → Common](./api/common) —— 完整 API 文档 ## Android 原生配置 *Source: `docs/native-setup/android.md` · Slug: `/native-setup/android`* # Android 原生配置 分享后能否跳回 App 全靠回调 Activity 的注册。**模板别凭记忆编**,逐项核对本页。 --- ## `android/app/src/main/AndroidManifest.xml` {#manifest} :::tip 不需要写的内容(已自动合并) - **`` 与 ``** —— `@unif/react-native-umeng` 的 library Manifest 已声明 `INTERNET` / `ACCESS_NETWORK_STATE` / `ACCESS_WIFI_STATE` 权限,以及 ``(`com.tencent.mm` 微信、`com.alibaba.android.rimet` 钉钉);Android manifest merger 自动合并到宿主。 - **友盟相关 ``** —— appkey 等通过 JS [`Common.preInit(config)`](../api/common#preinit) 传。 ::: **仅需注册两个回调 Activity**(微信 / 钉钉 SDK 硬限制:必须在宿主包名下,SDK 用 `getPackageName() + ".wxapi.WXEntryActivity"` / `+ ".ddshare.DDShareActivity"` 反射查找,**不能放在 library 包**): ```xml ``` --- ## 微信回调 — `/wxapi/WXEntryActivity.kt` {#wxentryactivity} 继承友盟提供的基类 `WXCallbackActivity`,空类即可: ```kotlin package com.example.app.wxapi import com.umeng.socialize.weixin.view.WXCallbackActivity class WXEntryActivity : WXCallbackActivity() ``` :::warning 超类是友盟的 `WXCallbackActivity` 超类必须是 `com.umeng.socialize.weixin.view.WXCallbackActivity`(友盟封装了微信 `IWXAPIEventHandler` 的回调链路),**不是**微信原生的 `WXEntryActivity`。包路径必须是 `<宿主包名>.wxapi.WXEntryActivity`。 ::: --- ## 钉钉回调 — `/ddshare/DDShareActivity.kt` {#ddshareactivity} 继承 `Activity` 并实现 `IDDAPIEventHandler`: ```kotlin package com.example.app.ddshare import android.app.Activity import android.os.Bundle import com.example.app.BuildConfig import com.android.dingtalk.share.ddsharemodule.DDShareApiFactory import com.android.dingtalk.share.ddsharemodule.IDDAPIEventHandler import com.android.dingtalk.share.ddsharemodule.IDDShareApi import com.android.dingtalk.share.ddsharemodule.message.BaseReq import com.android.dingtalk.share.ddsharemodule.message.BaseResp class DDShareActivity : Activity(), IDDAPIEventHandler { private lateinit var iddShareApi: IDDShareApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ⚠️ 这个 appId 必须跟 JS 端 Common.preInit({ dingtalkAppId: '...' }) 传的值一致。 // 钉钉 SDK 要求 Activity onCreate 时就传 appId 建 ShareApi 实例,且冷启动可能直接 // 拉起本 Activity(JS 还没跑),所以只能 native 侧拿、读不到 JS config —— 这是钉钉 // 官方 Android 接入的固有形态。为避免两处写死漂移,用构建时单一数据源 // BuildConfig.DINGTALK_APPID(定义见下)。 val appId = BuildConfig.DINGTALK_APPID iddShareApi = DDShareApiFactory.createDDShareApi(this, appId, false) iddShareApi.handleIntent(intent, this) } override fun onReq(req: BaseReq) {} override fun onResp(resp: BaseResp) { // 友盟钉钉回调由 SDK 内部 hook,此处 finish 即可 finish() } } ``` :::warning appId 必须与 JS `preInit({ dingtalkAppId })` 一致 钉钉 appId 在 `DDShareActivity.onCreate` 传入,同一个值也要传给 JS [`Common.preInit({ dingtalkAppId })`](../api/common#umenginitconfig)。推荐用 `BuildConfig.DINGTALK_APPID` 作为单一数据源,避免两处写死漂移。 ::: **`BuildConfig` 单一数据源(推荐):** ```gradle // android/app/build.gradle android { defaultConfig { buildConfigField "String", "DINGTALK_APPID", "\"dingoaXXXXXXXX\"" } buildFeatures { buildConfig = true } // AGP 8+ 默认关,需显式开 } ``` --- ## 宿主依赖 — 显式声明钉钉 / 微信 SDK {#sdk-deps} 回调 Activity 落在**宿主包名**下、直接 `import` 钉钉 / 友盟微信 SDK 类,而 `@unif/react-native-umeng` 以 Gradle `implementation` 声明这些 SDK(不向宿主传递编译期依赖)。因此宿主 `app/build.gradle` 需显式声明: ```gradle // android/app/build.gradle —— 版本对齐 @unif/react-native-umeng 的 android/build.gradle dependencies { implementation "com.alibaba.android:ddsharesdk:1.2.2" // DDShareActivity import 的钉钉 SDK implementation "com.umeng.umsdk:share-wx:7.3.7" // WXEntryActivity 继承的 WXCallbackActivity implementation "com.tencent.mm.opensdk:wechat-sdk-android:6.8.34" // 上者超类链(编译期可见) } ``` > 仅做钉钉分享:只需 `ddsharesdk`;仅做微信:只需 `share-wx` + `wechat-sdk-android`。runtime 所需的其余友盟 SDK(`common` / `asms` / `share-core` / `share-dingding`)由 library 的 `implementation` 经 runtime classpath 提供,宿主不用重复。 --- ## 宿主 MainActivity {#mainactivity} :::tip 不需要 override 任何回调 不需要 override `onActivityResult` / `onDestroy` 转发给 `UMShareAPI` —— `UmengShareModule` 实现 `ActivityEventListener` + `LifecycleEventListener`,自动接管 Activity 回调链路,并在 `onHostDestroy` / `invalidate()` 时释放 `UMShareAPI`。 ::: --- ## Proguard / R8 {#proguard} :::tip 不需要写 Proguard 规则 `@unif/react-native-umeng` 通过 `android/consumer-rules.pro` 自动给宿主 App 合并 R8 / proguard 规则(保留友盟 / 微信 / 钉钉相关 class 不被混淆)。release build 直接 `./gradlew assembleRelease`,不会因混淆 crash。 ::: --- ## 平台支持 {#platform-support} | 配置项 | iOS | Android | | --- | --- | --- | | AndroidManifest 回调 Activity | — | ✅ 必填 | | `WXEntryActivity.kt` | — | ✅ 必填 | | `DDShareActivity.kt` | — | ✅ 必填 | | 宿主 `build.gradle` 声明钉钉/微信 SDK | — | ✅ 必填(**不自动**,见 [#sdk-deps](#sdk-deps)) | | 权限 / `` | — | ✅ 自动合并 | | MainActivity override | — | ❌ 不需要 | | Proguard rules | — | ❌ 不需要(自动合并) | ## 相关 {#related} - [iOS 原生配置](./ios) —— Info.plist / AppDelegate 转发 - [快速上手](../getting-started/quick-start) —— 完整接入流程 - [常见问题 → 分享无回调](../troubleshooting#native-callback) —— 原生未注册的排障 ## iOS 原生配置 *Source: `docs/native-setup/ios.md` · Slug: `/native-setup/ios`* # iOS 原生配置 分享后能否跳回 App 全靠原生侧 URL Scheme 与回调注册。**模板别凭记忆编**,逐项核对本页。 :::tip appkey 走 JS,不写 plist 友盟 appkey、微信 AppSecret、Universal Link 等配置通过 JS [`Common.preInit({ appkey, wechatAppId, ... })`](../api/common#preinit) 传入,**不写在 Info.plist**。Info.plist 只配 iOS 系统强制的 URL Scheme 白名单 + 回调 scheme。 ::: --- ## `ios//Info.plist` {#info-plist} ```xml LSApplicationQueriesSchemes weixin weixinULAPI weixinURLParamsAPI wechat dingtalk dingtalk-open dingtalk-sso CFBundleURLTypes CFBundleTypeRoleEditor CFBundleURLNameweixin CFBundleURLSchemes wxXXXXXXXX CFBundleTypeRoleEditor CFBundleURLNamedingtalk CFBundleURLSchemes dingoaXXXXXXXX ``` | 键 | 必填 | 说明 | | --- | --- | --- | | `LSApplicationQueriesSchemes` | ✅ | 第三方 App scheme 查询白名单,缺则 `isInstalled` / 跳转判断不准 | | `CFBundleURLTypes` | ✅ | 回调 scheme(`wx`+appid、`dingoa`+appid),缺则分享后跳不回 App | --- ## `ios//AppDelegate.swift` {#appdelegate} 把 `open url` 与 `continue userActivity` 转发给桥导出的 `UmengBootstrap`。 ```swift import UIKit // UmengBootstrap 是 @unif/react-native-umeng 桥导出的 Objective-C class, // CocoaPods 装好后由 ReactNativeUmeng pod 的 public header 暴露给宿主, // Swift 端自动 bridge,无需写 bridging header。 @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { return UmengBootstrap.shared().handleOpen(url, options: options) } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { return UmengBootstrap.shared().handleUniversalLink(userActivity) } } ``` :::danger 是 `handleOpen(_:options:)`,不是 `handleOpenURL` 桥侧 Objective-C 方法签名是 `handleOpenURL:options:`,首参类型 `NSURL`。Swift 的 omit-needless-words 规则会去掉方法名末尾与首参类型重复的 `URL`,导入成 **`handleOpen(_:options:)`**。写 `handleOpenURL` 会编译报 `has been renamed to 'handleOpen(_:options:)'`。
Swift omit-needless-words 规则说明 Swift 导入 ObjC 时,若方法名末尾的词与首参类型名(去 `NS` 前缀后)完全匹配,则自动省略: - `handleOpenURL:options:` → 首参 `NSURL` → 末尾 `URL` 与类型名 `URL` 匹配 → 省略 → **`handleOpen(_:options:)`** - `handleUniversalLink:` → 首参 `NSUserActivity` → 末尾 `Link` 与 `UserActivity` 不匹配 → 不省略 → 保持 **`handleUniversalLink(_:)`**
::: | 转发方法(Swift 侧) | 触发场景 | | --- | --- | | `handleOpen(_:options:)` | App 通过 URL Scheme 被分享回调拉起 | | `handleUniversalLink(_:)` | 微信 Universal Link 回跳(1.8.6+ 必需) | --- ## `ios/Podfile` {#podfile} 无需特殊配置,标准 RN 新架构 Podfile 即可(`use_native_modules!` 会自动装入 `ReactNativeUmeng` pod 及其友盟依赖 `UMCommon` / `UMDevice` / `UMShare`)。 ```ruby target 'YourApp' do config = use_native_modules! use_react_native!(:path => config[:reactNativePath]) post_install do |installer| react_native_post_install(installer, config[:reactNativePath]) end end ``` :::warning 模拟器限制 —— 用真机测分享 友盟 U-Share 在 Apple Silicon Mac 模拟器上有 `EXCLUDED_ARCHS=arm64` 限制(旧式 `.framework` 未出 arm64 simulator slice)。**用真机测试微信 / 钉钉分享**。强制清 `EXCLUDED_ARCHS` 反而可能在编译期触发 arm64 链接错。 ::: --- ## 微信 Universal Link {#universal-link} 微信 SDK 1.8.6+ 强制 Universal Link: 1. 到 Apple Developer Portal 为 App ID 启用 **Associated Domains** capability。 2. 在服务器部署 `apple-app-site-association` 文件(路径 `/.well-known/apple-app-site-association`)。 3. Xcode Entitlements 加 `applinks:your.host`。 4. 把对应的 `https://your.host/` 传给 [`Common.preInit({ wechatUniversalLink })`](../api/common#umenginitconfig)。 详见微信开放平台官方文档。 --- ## 平台支持 {#platform-support} | 配置项 | iOS | Android | | --- | --- | --- | | `LSApplicationQueriesSchemes` | ✅ 必填 | — | | `CFBundleURLTypes` | ✅ 必填 | — | | AppDelegate 转发 | ✅ 必填 | — | | 微信 Universal Link | ✅ 1.8.6+ 强制 | — | ## 相关 {#related} - [Android 原生配置](./android) —— 回调 Activity / 权限 / queries - [快速上手](../getting-started/quick-start) —— 完整接入流程 - [常见问题 → 分享无回调](../troubleshooting#native-callback) —— 原生未注册的排障 ## AI Skill *Source: `docs/skills.md` · Slug: `/skills`* # AI Skill:unif-umeng ## 这是什么 `unif-umeng` 是一个 **Agent Skill**,教 AI 编码助手(Claude Code / Cursor / Codex)正确调用 `@unif/react-native-umeng` 的 API、避免常见幻觉。 它把友盟分享与统计的关键约定、易错点和参考索引打包给 AI,让助手在你的项目里写代码时按真实 API 来,而不是凭记忆瞎猜。 ## 覆盖什么 **何时会触发:** 用 `@unif/react-native-umeng` 做友盟分享(微信会话 / 钉钉)与统计——分享面板、埋点 `onEvent`、登录登出统计、PIPL 两段式初始化,或排查分享无回调 / init 顺序 / cancel。 **覆盖的能力:** - U-Share 分享:`ShareSheetHost` 挂载 + `openSheet`,取消 / 失败走 reject 的正确处理。 - PIPL 两段式初始化:`preInit`(给 config)→ 同意 → `init`(无参),顺序不能反。 - U-App 统计:`onEvent` 埋点、登录 / 登出,方法是同步 void、别 await。 - 易错点:cancel/failure 误当 resolve、`init` 带参、忘挂 host、umeng 的 `Platform` 与 RN 的混用。 > 朋友圈 / QQ / 微博首版不支持(只有微信会话 + 钉钉);拍照 / 扫码各有专门 skill。 ## 如何安装 **Claude Code 插件市场:** ```bash /plugin marketplace add unif-design/skills /plugin install unif@unif-skills ``` **或用 skills CLI:** ```bash npx skills add unif-design/skills ``` ## 在 GitHub 查看 skills 全部开源,发布在插件市场仓库 `unif-design/skills`。本 skill 的源码与参考文档: 👉 **[github.com/unif-design/skills · unif-umeng](https://github.com/unif-design/skills/tree/main/skills/unif-umeng)** --- 装了之后,在你的项目里让 AI 写 `@unif/react-native-umeng` 代码会更准。 ## 测试(Mock) *Source: `docs/testing.md` · Slug: `/testing`* # 测试(Mock) 本库依赖 `NativeUmeng*` TurboModule 与 `@unif/react-native-design`,在 Jest 环境直接加载会崩溃。官方随包提供整包 mock,在测试里一行替换即可。 --- ## 启用 mock {#enable} 在 Jest setup 或单个测试文件里整包替换: ```ts jest.mock('@unif/react-native-umeng', () => require('@unif/react-native-umeng/mock') ); ``` 替换后: - **`Share.*` 默认走 happy-path** —— `shareText` / `shareImage` / `shareLink` / `openSheet` 默认 **resolve 成功**(`{ code: 'success', platform }`)。`isInstalled` 默认 `true`,`listPlatforms` 默认全平台已安装。 - **`Analytics.*` 是同步 `jest.fn()`** —— `onEvent` / `signIn` / `signOut` 与真实实现一致(同步 `void`)。 - **`Common.*` 是 `jest.fn()`** —— `preInit` / `init` 默认 `resolve()`,`isInited` 默认 `resolve(false)`。 - **`` 渲染 `null`** —— 不引 design,无需在测试里挂任何 Provider。 - **类型 / 常量 / `UmengError` 保留真实实现** —— `Platform`、`SUPPORTED_PLATFORMS`、`PLATFORM_*`、`UmengError` 等不碰 native,直接复用真值,无需额外 stub。 完整 mock 行为: | 模块 | 默认行为 | | --- | --- | | `Common.preInit` / `init` | `jest.fn()`,`Promise.resolve()` | | `Common.isInited` | `jest.fn()`,`Promise.resolve(false)` | | `Share.shareText` / `shareImage` / `shareLink` | `jest.fn()`,resolve `{ code: 'success', platform }` | | `Share.openSheet` | `jest.fn()`,resolve `{ code: 'success', platform: WECHAT_SESSION }` | | `Share.isInstalled` | `jest.fn()`,`Promise.resolve(true)` | | `Share.listPlatforms` | `jest.fn()`,全平台 `installed: true` | | `Analytics.onEvent` / `signIn` / `signOut` | `jest.fn()`(同步 void) | | `ShareSheetHost` | 渲染 `null` | | `Platform` / `UmengError` / 常量 | 真实值(不 mock) | --- ## 覆盖单次返回:取消 / 失败 {#override} 默认是成功。要测「取消」「失败」,mock 额外导出 `shareSuccess` / `shareCancel` / `shareFailed` 助手,配合 `mockResolvedValueOnce` 覆盖一次: ```ts import { Share, Platform } from '@unif/react-native-umeng'; import { shareCancel, shareFailed } from '@unif/react-native-umeng/mock'; // 模拟用户取消 (Share.shareText as jest.Mock).mockResolvedValueOnce( shareCancel(Platform.WECHAT_SESSION) ); // 模拟分享失败 (Share.openSheet as jest.Mock).mockResolvedValueOnce( shareFailed(Platform.DINGTALK, '网络错误') ); ``` 助手签名: ```ts shareSuccess(platform: Platform): ShareResult shareCancel(platform: Platform): ShareResult shareFailed(platform: Platform, message?: string): ShareResult ``` :::note mock 的 `shareCancel` / `shareFailed` 返回的是 ShareResult,不是抛错 mock 助手返回普通 `ShareResult`(`code: 'cancel' | 'failed'`),用于让 `jest.fn` **resolve** 这个值。这与生产实现不同 —— 生产里 `Share.openSheet` 取消 / 失败是 **reject `UmengError`**(见[分享指南](./guides/sharing#reject-on-cancel))。若要测试 reject 分支,改用 `mockRejectedValueOnce(new UmengError('E_USER_CANCEL', '...'))`。 ::: --- ## 完整示例 {#example} ```ts import { Share, Platform, UmengError } from '@unif/react-native-umeng'; import { shareSuccess } from '@unif/react-native-umeng/mock'; jest.mock('@unif/react-native-umeng', () => require('@unif/react-native-umeng/mock') ); describe('分享', () => { it('默认 resolve 成功', async () => { const r = await Share.openSheet({ type: 'text', text: 'hi' }); expect(r.code).toBe('success'); }); it('可覆盖为指定平台成功', async () => { (Share.openSheet as jest.Mock).mockResolvedValueOnce( shareSuccess(Platform.DINGTALK) ); const r = await Share.openSheet({ type: 'text', text: 'hi' }); expect(r.platform).toBe(Platform.DINGTALK); }); it('可覆盖为 reject(取消)', async () => { (Share.openSheet as jest.Mock).mockRejectedValueOnce( new UmengError('E_USER_CANCEL', 'User cancelled') ); await expect(Share.openSheet({ type: 'text', text: 'hi' })).rejects.toMatchObject({ code: 'E_USER_CANCEL', }); }); }); ``` --- ## 下一步 - [分享指南 → 取消与失败](./guides/sharing#reject-on-cancel) —— 生产里 reject-on-cancel 的语义 - [常见问题](./troubleshooting) —— 真机 / 模拟器限制与排障 ## 常见问题 *Source: `docs/troubleshooting.md` · Slug: `/troubleshooting`* # 常见问题 按**症状 → 原因 → 解法**排查。多数分享问题集中在「把取消 / 失败当 resolve」「没挂 Host」「init 顺序」「在模拟器上跑」四类。 --- ## 症状:分享点了「没回调」/ 取消失败的分支永远进不去 {#no-callback-js} 最常见的误解 —— **`Share.openSheet()` / `shareXxx` 取消、失败时是 reject,不是 resolve**。如果只 `await` 不 try/catch,取消 / 失败会变成未捕获的 rejection,看起来像「没回调」。 ```ts // ❌ Incorrect:取消 / 失败不会 resolve,判别分支永远到不了,还可能抛未捕获错误 const r = await Share.openSheet(payload); if (r.code === 'cancel') { /* 永远到不了 */ } ``` ```ts // ✅ Correct:try/catch 看 e.code;resolve 的 r.code 必为 'success' try { const r = await Share.openSheet(payload); // r.code === 'success' } catch (e) { if (e instanceof UmengError && e.code === 'E_USER_CANCEL') { /* 取消 */ } // E_SHARE_FAILED / E_PLATFORM_NOT_INSTALLED 等见下表 } ``` 详见[分享指南 → 取消与失败](./guides/sharing#reject-on-cancel)。 --- ## 症状:`openSheet` 立即 reject,message 含 "No <ShareSheetHost /> mounted" {#no-host} 没在 App 根挂 ``(reject `E_UNKNOWN`)。 ```tsx // ❌ Incorrect:没挂 Host ``` ```tsx // ✅ Correct:App 根挂一次,且在 GestureHandlerRootView + ThemeProvider 内 ``` > 若 message 是 "Another ShareSheet is already open",说明**一次只能开一个面板**,前一个还没关。等它 resolve / reject(或取消)后再开。挂载细节见[快速上手](./getting-started/quick-start#mount-host)。 --- ## 症状:`Common.init()` reject / 统计不上报 {#init-order} `init` 前必须先 `preInit`,且 `init` **无参**。 ```ts // ❌ Incorrect:没 preInit 直接 init(reject),或 init 带 config await Common.init({ appkey: '...' }); ``` ```ts // ✅ Correct:config 给 preInit;用户同意后调无参 init await Common.preInit({ appkey: '...', /* ... */ }); // App 启动 await Common.init(); // 用户同意后,无参 ``` 统计不上报最常见原因:`init` 还没调(用户未同意《隐私协议》)。此时 `Analytics.*` 不会崩溃,但数据不上报。两段式见[隐私合规(PIPL)](./guides/privacy-pipl)。 --- ## 症状:模拟器上分享编译失败 / 无法跑起来 / 不能真分享 {#simulator} ✅ **这是预期行为,不是 bug。用真机验证分享。** - 模拟器没有真微信 / 钉钉,**无法完成回调跳转**,分享链路测不通。 - 友盟 U-Share 在 **Apple Silicon Mac 模拟器**上有 `EXCLUDED_ARCHS=arm64` 限制(旧式 `.framework` 未出 arm64 simulator slice)。这是友盟 SDK 已知限制,**强制清 `EXCLUDED_ARCHS` 反而可能触发 arm64 链接错**。 :::tip 在 CI / 模拟器里测逻辑 不要在模拟器里测真实分享。单元测试用[测试(Mock)](./testing)页的 `jest.mock` 方案,在无原生环境跑通分享流程逻辑。 ::: --- ## 症状:Android 后台调 `Share.openSheet()` 报 `E_UNKNOWN`("No current Activity") {#no-activity} ✅ **分享 API 依赖前台 Activity,不能在后台 / App 不可见时调。** Android 在无前台 Activity 时会 reject `E_UNKNOWN`,`message` 为 `No current Activity; cannot invoke share` —— 用 `message` 区分此场景与其它 `E_UNKNOWN`。请在用户有操作的前台场景(如按钮回调)中调用分享。 --- ## 症状:分享后回调不跳回 App(原生未注册) {#native-callback} 分享后跳回 App 全靠原生侧 URL Scheme / 回调 Activity。**模板别凭记忆编**,按平台逐项核对: ### iOS - `Info.plist` 缺 `CFBundleURLTypes` 回调 scheme(微信 = `wx`+appid,钉钉 = `dingoa`+appid),或缺 `LSApplicationQueriesSchemes` 白名单。 - `AppDelegate` 没把 `open url` / `continue userActivity` 转发给桥导出的 `UmengBootstrap`。注意 Swift 侧方法名是 **`handleOpen(_:options:)`**(omit-needless-words),不是 `handleOpenURL`。 逐项见 [iOS 原生配置](./native-setup/ios)。 ### Android - `WXEntryActivity`(超类 `WXCallbackActivity`)/ `DDShareActivity` **必须在宿主包名下** —— 微信 / 钉钉 SDK 通过 `getPackageName() + ".wxapi.WXEntryActivity"` / `+ ".ddshare.DDShareActivity"` 反射查找,放在 library 包里查不到、回调丢失。 - 钉钉 `appId` 在 Activity `onCreate` 写死,要与 JS `preInit({ dingtalkAppId })` 一致。 逐项见 [Android 原生配置](./native-setup/android)。 --- ## 错误码速查 {#error-codes} `UmengError.code` 全量含义: | `code` | 含义 | 触发场景 | 处理 | | --- | --- | --- | --- | | `E_USER_CANCEL` | 用户取消 | 点取消 / 点遮罩 / 平台侧取消 | 通常静默 | | `E_SHARE_FAILED` | 分享失败 | 未配 URL Scheme、网络错、内容不合规等 | 查 `message`;核对原生回调配置 | | `E_PLATFORM_NOT_INSTALLED` | 目标 App 未安装 | 微信 / 钉钉未装 | `isInstalled()` 预检,或 `hideUninstalled: true` | | `E_PLATFORM_NOT_SUPPORTED` | 平台不在白名单 | 传了非 `WECHAT_SESSION` / `DINGTALK` 的值 | 用 `Platform` 枚举值 | | `E_INVALID_OPTIONS` | 参数缺失 / 非法 | `shareLink` 缺 `title`/`url`、`preInit` 缺 `appkey`、没 `preInit` 就 `init` | 检查必填字段 / 初始化顺序 | | `E_NOT_INITIALIZED` | 预留错误码 | — | — | | `E_UNKNOWN` | 未归类错误 | 未挂 Host、面板重入、Android 无前台 Activity、SDK 未知错 | 看 `message` / `nativeError` 区分 | > `UmengError` 还带 `nativeError` 字段(原始错误)。错误码在 JS 层的判别用法见[分享指南](./guides/sharing#reject-on-cancel)。