add-app-clip

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Add an App Clip to an Expo App

为Expo应用添加App Clip

Adds an iOS App Clip target to an Expo project. The Clip lives in
targets/clip/
, ships alongside the parent app, and is invoked from a URL on the app's domain via an Apple App Site Association (AASA) file.
The parent app's bundle ID becomes
com.<username>.<app-name>
and the Clip's is automatically derived as
<parent>.clip
(e.g.
com.bacon.may20.clip
).
为Expo项目添加iOS App Clip目标。该Clip位于
targets/clip/
目录下,与主应用一同分发,可通过应用域名上的URL,借助Apple App Site Association(AASA)文件调用。
主应用的Bundle ID将变为
com.<username>.<app-name>
,Clip的Bundle ID会自动派生为
<parent>.clip
(例如
com.bacon.may20.clip
)。

1. Set
bundleIdentifier
and
appleTeamId

1. 设置
bundleIdentifier
appleTeamId

bun create target
warns if these are missing. Add to
app.json
:
json
{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.<username>.<app-name>",
      "appleTeamId": "XX57RJ5UTD"
    }
  }
}
如果缺少这些配置,
bun create target
会发出警告。请在
app.json
中添加:
json
{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.<username>.<app-name>",
      "appleTeamId": "XX57RJ5UTD"
    }
  }
}

2. Add the App Clip target

2. 添加App Clip目标

sh
bun create target clip
This installs
@bacons/apple-targets
, adds it to the
plugins
array in
app.json
, and writes:
  • targets/clip/expo-target.config.js
    — the target's config plugin
  • targets/clip/Info.plist
    — Clip Info.plist
  • targets/clip/AppDelegate.swift
    ,
    Assets.xcassets
    , etc.
Pick a good icon or reuse the existing one defined in the app — check it with
bunx expo config
under the
icon
or
ios.icon
key.
sh
bun create target clip
这会安装
@bacons/apple-targets
,将其添加到
app.json
plugins
数组中,并生成以下文件:
  • targets/clip/expo-target.config.js
    —— 目标的配置插件
  • targets/clip/Info.plist
    —— Clip的Info.plist文件
  • targets/clip/AppDelegate.swift
    Assets.xcassets
    等文件
选择合适的图标或复用应用中已定义的图标 —— 可通过
bunx expo config
查看
icon
ios.icon
键下的配置。

3. Wire up associated domains

3. 关联域名配置

The parent app and the Clip each need the Associated Domains entitlement pointing at the domain that hosts the AASA file.
In
app.json
, add both
applinks:
(parent) and
appclips:
(Clip invocation) entries:
json
{
  "expo": {
    "ios": {
      "associatedDomains": [
        "applinks:may20.expo.app",
        "appclips:may20.expo.app"
      ]
    }
  }
}
In
targets/clip/expo-target.config.js
, declare the Clip's entitlement:
js
/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = (config) => ({
  type: "clip",
  icon: "https://github.com/expo.png",
  entitlements: {
    "com.apple.developer.associated-domains": ["appclips:may20.expo.app"],
  },
});
If you skip this,
expo prebuild
will print:
Apple App Clip may require the associated domains entitlement but none were found
.
主应用和Clip都需要关联域名权限,指向托管AASA文件的域名。
app.json
中,添加
applinks:
(主应用)和
appclips:
(Clip调用)条目:
json
{
  "expo": {
    "ios": {
      "associatedDomains": [
        "applinks:may20.expo.app",
        "appclips:may20.expo.app"
      ]
    }
  }
}
targets/clip/expo-target.config.js
中,声明Clip的权限:
js
/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = (config) => ({
  type: "clip",
  icon: "https://github.com/expo.png",
  entitlements: {
    "com.apple.developer.associated-domains": ["appclips:may20.expo.app"],
  },
});
如果跳过此步骤,
expo prebuild
会输出:
Apple App Clip may require the associated domains entitlement but none were found
(Apple App Clip可能需要关联域名权限,但未找到)。

4. Register bundle IDs and create the App Store entry

4. 注册Bundle ID并创建App Store条目

sh
bunx setup-safari
This logs in to the Apple Developer account, registers
com.bacon.may20
, creates the App Store Connect entry, and prints:
  • A starter
    apple-app-site-association
    JSON
  • A
    <meta name="apple-itunes-app">
    tag with the iTunes app id
  • Team ID, iTunes ID, and Bundle ID
sh
bunx setup-safari
这会登录Apple开发者账户,注册
com.bacon.may20
,创建App Store Connect条目,并输出:
  • 初始的
    apple-app-site-association
    JSON文件
  • 包含iTunes应用ID的
    <meta name="apple-itunes-app">
    标签
  • 团队ID、iTunes ID和Bundle ID

5. Host the AASA file

5. 托管AASA文件

App Clips are invoked when iOS fetches
https://<your-domain>/.well-known/apple-app-site-association
and finds a matching
appclips
entry.
sh
mkdir -p public/.well-known
touch public/.well-known/apple-app-site-association
Paste the JSON
setup-safari
printed, but add an
appclips
block
for the Clip's full app ID (
<TeamID>.<ClipBundleID>
). The output of
setup-safari
only covers the parent app:
json
{
  "applinks": {
    "details": [
      {
        "appIDs": ["XX57RJ5UTD.com.bacon.may20"],
        "components": [{ "/": "*", "comment": "Matches all routes" }]
      }
    ]
  },
  "appclips": {
    "apps": ["XX57RJ5UTD.com.bacon.may20.clip"]
  },
  "activitycontinuation": {
    "apps": ["XX57RJ5UTD.com.bacon.may20"]
  },
  "webcredentials": {
    "apps": ["XX57RJ5UTD.com.bacon.may20"]
  }
}
Notes:
  • The file has no extension and no
    Content-Type
    requirements
    beyond being served as-is. Expo Router static export serves files in
    public/
    verbatim.
  • The
    appclips
    block is what lets a URL on the domain launch the Clip.
  • webcredentials
    is used for sharing credentials between the website, parent app, and the App Clip.
  • activitycontinuation
    is optional and used for sharing the link between mobile and desktop. Must be used with
    Head
    from expo-router — see https://docs.expo.dev/router/advanced/apple-handoff/
  • Notation and route-disabling details: https://sosumi.ai/documentation/xcode/supporting-associated-domains
当iOS获取
https://<your-domain>/.well-known/apple-app-site-association
并找到匹配的
appclips
条目时,App Clip会被调用。
sh
mkdir -p public/.well-known
touch public/.well-known/apple-app-site-association
粘贴
setup-safari
输出的JSON,但需添加
appclips
,对应Clip的完整应用ID(
<TeamID>.<ClipBundleID>
)。
setup-safari
的输出仅包含主应用的配置:
json
{
  "applinks": {
    "details": [
      {
        "appIDs": ["XX57RJ5UTD.com.bacon.may20"],
        "components": [{ "/": "*", "comment": "Matches all routes" }]
      }
    ]
  },
  "appclips": {
    "apps": ["XX57RJ5UTD.com.bacon.may20.clip"]
  },
  "activitycontinuation": {
    "apps": ["XX57RJ5UTD.com.bacon.may20"]
  },
  "webcredentials": {
    "apps": ["XX57RJ5UTD.com.bacon.may20"]
  }
}
注意事项:

6. Add the Smart App Banner meta tag

6. 添加智能应用横幅元标签

Create
src/app/+html.tsx
(Expo Router's HTML shell) and add the tag from
setup-safari
. Create the versioned template if it doesn't exist:
sh
bunx expo customize src/app/+html.tsx
Add the meta tag to the
<head>
:
tsx
import { ScrollViewStyleReset } from "expo-router/html";

export default function Root({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="apple-itunes-app" content="app-id=6771566491" />
        <ScrollViewStyleReset />
      </head>
      <body>{children}</body>
    </html>
  );
}
To make the website show the App Clip card instead of the install card, use:
html
<meta
  name="apple-itunes-app"
  content="app-id=6771566491, app-clip-bundle-id=com.bacon.may20.clip, app-clip-display=card"
/>
创建
src/app/+html.tsx
(Expo Router的HTML外壳)并添加
setup-safari
输出的标签。如果不存在版本化模板,可创建:
sh
bunx expo customize src/app/+html.tsx
<head>
中添加元标签:
tsx
import { ScrollViewStyleReset } from "expo-router/html";

export default function Root({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="apple-itunes-app" content="app-id=6771566491" />
        <ScrollViewStyleReset />
      </head>
      <body>{children}</body>
    </html>
  );
}
若要让网站显示App Clip卡片而非安装卡片,可使用:
html
<meta
  name="apple-itunes-app"
  content="app-id=6771566491, app-clip-bundle-id=com.bacon.may20.clip, app-clip-display=card"
/>

7. Deploy the website

7. 部署网站

The AASA file must be live before iOS will trust the association. Use EAS Hosting:
sh
bunx expo export -p web
eas deploy --prod
This publishes the site (including
/.well-known/apple-app-site-association
) at
https://<slug>.expo.app
. Verify:
sh
curl https://may20.expo.app/.well-known/apple-app-site-association
AASA文件必须在线,iOS才会信任该关联。使用EAS Hosting
sh
bunx expo export -p web
eas deploy --prod
这会将网站(包括
/.well-known/apple-app-site-association
)发布到
https://<slug>.expo.app
。验证:
sh
curl https://may20.expo.app/.well-known/apple-app-site-association

8. Mirror permissions

8. 镜像权限

Inspect the parent app's permissions after prebuild:
sh
npx expo config --type introspect
Look at the
infoPlist
object — mirror the permission keys in the App Clip's
Info.plist
so matching APIs can be used from the Clip.
Set
deploymentTarget: "17.6"
in the Clip's target config — App Clips have a higher minimum size limit in iOS 17.6.
If the app uses push notifications or location services, add to the App Clip's
Info.plist
to request the necessary permissions:
xml
<key>NSAppClip</key>
<dict>
  <key>NSAppClipRequestEphemeralUserNotification</key>
  <false/>
  <key>NSAppClipRequestLocationConfirmation</key>
  <true/>
</dict>
预构建后检查主应用的权限:
sh
npx expo config --type introspect
查看
infoPlist
对象 —— 在App Clip的
Info.plist
中镜像权限键,以便Clip可以使用匹配的API。
在Clip的目标配置中设置
deploymentTarget: "17.6"
—— iOS 17.6中App Clip的最小大小限制更高。
如果应用使用推送通知或定位服务,需在App Clip的
Info.plist
中添加配置以请求必要权限:
xml
<key>NSAppClip</key>
<dict>
  <key>NSAppClipRequestEphemeralUserNotification</key>
  <false/>
  <key>NSAppClipRequestLocationConfirmation</key>
  <true/>
</dict>

9. Build and submit to TestFlight

9. 构建并提交至TestFlight

sh
bunx testflight
This will:
  1. Generate an
    eas.json
    if missing.
  2. Set up credentials for both targets (parent + Clip). Each gets its own provisioning profile but can share a single Distribution Certificate.
  3. Sync capabilities — note
    Enabled: Associated Domains
    for the Clip target.
  4. Build, upload, and schedule a TestFlight submission.
sh
bunx testflight
这会:
  1. 如果缺少
    eas.json
    则生成该文件。
  2. 两个目标(主应用 + Clip)设置凭据。每个目标都有自己的配置文件,但可以共享一个分发证书。
  3. 同步功能 —— 注意Clip目标的
    Enabled: Associated Domains
    (已启用:关联域名)。
  4. 构建、上传并安排TestFlight提交。

10. Configure App Clip metadata

10. 配置App Clip元数据

Pull existing App Store metadata to local:
sh
eas metadata:pull
Add
apple.appClip
to
store.config.json
. Up to 3 invocation URLs can launch the Clip from a web page:
json
{
  "configVersion": 0,
  "apple": {
    "appClip": {
      "defaultExperience": {
        "action": "PLAY",
        "releaseWithAppStoreVersion": true,
        "reviewDetail": {
          "invocationUrls": ["https://may20.expo.app/", null, null]
        },
        "info": {
          "en-US": {
            "subtitle": "Instantly native with Expo",
            "headerImage": "store/apple/app-clip/en-US/asc-app-clip.png"
          }
        }
      }
    }
  }
}
The
headerImage
must be a 1800x1200 PNG with no opacity.
Push back to the store:
sh
eas metadata:push
将现有App Store元数据拉取到本地:
sh
eas metadata:pull
store.config.json
中添加
apple.appClip
。最多可设置3个调用URL,用于从网页启动Clip:
json
{
  "configVersion": 0,
  "apple": {
    "appClip": {
      "defaultExperience": {
        "action": "PLAY",
        "releaseWithAppStoreVersion": true,
        "reviewDetail": {
          "invocationUrls": ["https://may20.expo.app/", null, null]
        },
        "info": {
          "en-US": {
            "subtitle": "Instantly native with Expo",
            "headerImage": "store/apple/app-clip/en-US/asc-app-clip.png"
          }
        }
      }
    }
  }
}
headerImage
必须是1800x1200的PNG图片,且不包含透明度。
将元数据推回商店:
sh
eas metadata:push

What you get

你将获得的内容

  • Parent app target:
    com.bacon.may20
  • App Clip target:
    com.bacon.may20.clip
    , lives in
    targets/clip/
  • AASA hosted at
    https://may20.expo.app/.well-known/apple-app-site-association
  • Smart App Banner meta tag on every web route
  • Every route linked to its native counterpart
  • TestFlight build of the parent app with the Clip embedded
Once Apple invokes the Clip from a URL on the domain, iOS opens
targets/clip/
's entry point which loads the React Native app.
  • 主应用目标:
    com.bacon.may20
  • App Clip目标:
    com.bacon.may20.clip
    ,位于
    targets/clip/
    目录下
  • 托管在
    https://may20.expo.app/.well-known/apple-app-site-association
    的AASA文件
  • 每个网页路由上的智能应用横幅元标签
  • 每个路由都链接到对应的原生组件
  • 包含Clip的主应用TestFlight构建版本
一旦Apple从域名上的URL调用Clip,iOS会打开
targets/clip/
的入口点,加载React Native应用。

Native detection (optional)

原生检测(可选)

To let JS detect when it's running inside an App Clip and present an install prompt for the full app, create a local Expo module (
bunx create-expo-module --local
) that exposes
navigator.appClip.prompt()
.
See ./references/native-module.md for the Swift module, TypeScript interface, and usage.
为了让JS检测是否在App Clip中运行,并显示主应用的安装提示,可创建本地Expo模块(
bunx create-expo-module --local
),暴露
navigator.appClip.prompt()
方法。
详见./references/native-module.md中的Swift模块、TypeScript接口及使用方法。

References

参考资料

  • ./references/native-module.md — Local Expo module to detect App Clip context and present the SKOverlay install prompt
  • ./references/native-module.md —— 用于检测App Clip上下文并显示SKOverlay安装提示的本地Expo模块