# FLUTTER - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: zh Generated on: 2026-07-24T13:01:53.323Z Total files: 44 --- # File: sdk-installation-flutter --- --- title: "安装与配置 Flutter SDK" description: "在 Flutter 上安装 Adapty SDK 的分步指南,适用于基于订阅的应用。" --- Adapty SDK 包含两个核心模块,可无缝集成到您的 Flutter 应用中: - **Core Adapty**:这是 Adapty 正常运行所必需的核心 SDK。 - **AdaptyUI**:如果你使用 [Adapty 付费墙编辑工具](adapty-paywall-builder)(一款无需编写代码即可轻松创建跨平台付费墙的可视化工具),则需要此模块。 :::tip 想看看 Adapty SDK 在真实移动应用中是如何集成的?欢迎查看我们的[示例应用](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example),其中展示了完整的集成配置,包括显示付费墙、完成购买以及其他基础功能。 ::: ## 要求 \{#requirements\} Adapty SDK 支持 iOS 13.0+,但要正常使用付费墙编辑工具创建的付费墙,需要 iOS 15.0+。 Adapty Flutter SDK 4.0——新增了 [Flow Builder](adapty-flow-builder) 支持——将最低要求提升至 **iOS 15.0+**、**Xcode 26+** 以及 **Flutter 3.32.0+**(Dart 3.8.0+)。安装详情请参阅下方的 [Adapty SDK 4.0](#adapty-sdk-40-swift-package-manager)。 :::info Adapty 兼容 Google Play Billing Library 8.x 及以下版本。默认情况下,Adapty 使用 Google Play Billing Library v7.0.0,但如果你想强制使用更高版本,可以手动[添加依赖项](https://developer.android.com/google/play/billing/integrate#dependency)。 ::: :::info 安装 SDK 是 Adapty 配置流程的第 5 步。在应用内购买正常运行之前,您还需要将应用连接到各应用商店,然后在 Adapty 看板中创建产品、付费墙和版位。[快速入门指南](quickstart)涵盖了所有必要步骤。 ::: ## 安装 Adapty SDK \{#install-adapty-sdk\} [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Flutter.svg?style=flat&logo=flutter)](https://github.com/adaptyteam/AdaptySDK-Flutter/releases) :::important 以下步骤安装的是最新稳定版 SDK(3.x)。如果你需要 v4([Flow Builder](adapty-flow-builder) 所需,[快速入门](flutter-quickstart-paywalls)也使用该版本),请直接参考下方的 [Adapty SDK 4.0: Swift Package Manager](#adapty-sdk-40-swift-package-manager)。 ::: 1. 将 Adapty 添加到你的 `pubspec.yaml` 文件中: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: ^ ``` 2. 运行以下命令安装依赖: ```bash showLineNumbers title="Terminal" flutter pub get ``` 3. 在应用中导入 Adapty SDK: ```dart showLineNumbers title="main.dart" import 'package:adapty_flutter/adapty_flutter.dart'; ``` ### Adapty SDK 4.0:Swift Package Manager 将支持 [Flow Builder](adapty-flow-builder) 的 Adapty Flutter SDK 4.0 添加到您的 `pubspec.yaml`: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: 4.0.0 ``` 从 v4 开始,原生 iOS SDK 不再通过 CocoaPods 分发——插件仅通过 **Swift Package Manager** 拉取([CocoaPods 的 spec 仓库将于 2026 年 12 月变为只读](https://blog.cocoapods.org/CocoaPods-Specs-Repo/))。如果您使用的是 Flutter 3.32–3.43,请先启用 Swift Package Manager 支持: ```bash showLineNumbers title="Terminal" flutter config --enable-swift-package-manager ``` Flutter 3.44 及更高版本默认启用 Swift Package Manager,因此无需额外操作。 有关 v4 中的 API 变更,请参阅[迁移指南](migration-to-flutter-sdk-v4)。 ## 激活 Adapty SDK 的 Adapty 模块 \{#activate-adapty-module-of-adapty-sdk\} 在您的应用代码中激活 Adapty SDK。 :::note Adapty SDK 在您的应用中只需激活一次。 ::: 获取您的 **Public SDK Key**: 1. 打开 Adapty 看板,导航至 [**App settings → General**](https://app.adapty.io/settings/general)。 2. 在 **Api keys** 部分,复制 **Public SDK Key**(不是 Secret Key)。 3. 将代码中的 `"YOUR_PUBLIC_SDK_KEY"` 替换为实际值。 或者,使用 [Adapty CLI](developer-cli) 以编程方式获取: ``` npm install -g adapty adapty auth login adapty apps list ``` 或者,直接运行: ``` npx adapty auth login adapty apps list ``` - 请确保使用 **Public SDK key** 初始化 Adapty,**Secret key** 仅用于[服务端 API](getting-started-with-server-side-api)。 - **SDK keys** 对每个应用都是唯一的,如果您有多个应用,请确保选择正确的那个。 ```dart showLineNumbers title="main.dart" void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { _initializeAdapty(); super.initState(); } Future _initializeAdapty() async { try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY'), ); } catch (e) { // handle the error } } Widget build(BuildContext context) { return Text("Hello"); } } ``` :::important 在调用任何其他 Adapty SDK 方法之前,请等待 `activate` 完成。完整调用顺序请参阅 [Flutter SDK 调用顺序](flutter-sdk-call-order)。 ::: 现在在你的应用中配置付费墙: - 如果你使用 [Adapty 付费墙编辑工具](adapty-paywall-builder),请先[激活 Adapty SDK 的 AdaptyUI 模块](#activate-adaptyui-module-of-adapty-sdk),然后按照[付费墙编辑工具快速入门](flutter-quickstart-paywalls)操作。 - 如果你自行构建付费墙 UI,请参阅[自定义付费墙快速入门](flutter-quickstart-manual)。 ## 激活 Adapty SDK 的 AdaptyUI 模块 \{#activate-adaptyui-module-of-adapty-sdk\} 如果你计划使用[付费墙编辑工具](adapty-paywall-builder),并且已经[安装了 AdaptyUI 模块](sdk-installation-flutter#install-adapty-sdk),还需要激活 AdaptyUI: :::note 无论 AdaptyUI 是否已激活,与 AdaptyUI 相关的依赖项都会链接到你的应用中。 ::: :::important 在代码中,必须先激活 Adapty 核心模块,再激活 AdaptyUI。 ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withActivateUI(true), // This automatically activates AdaptyUI ); ``` ## 可选配置 \{#optional-setup\} ### 日志记录 \{#logging\} #### 配置日志系统 \{#set-up-the-logging-system\} Adapty 会记录错误及其他重要信息,帮助你了解运行状况。目前支持以下日志级别: | 级别 | 描述 | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------ | | `AdaptyLogLevel.error` | 仅记录错误日志 | | `AdaptyLogLevel.warn` | 记录错误日志,以及 SDK 中不会导致严重错误但值得关注的消息。 | | `AdaptyLogLevel.info` | 记录错误、警告及各类信息消息。默认值 | | `AdaptyLogLevel.verbose` | 记录调试时可能有用的额外信息,例如函数调用、API 请求等。 | | `AdaptyLogLevel.debug` | 记录调试信息。 | 您可以在配置 Adapty 之前在应用中设置日志级别: ```dart showLineNumbers title="main.dart" // Set log level before activation. // 'verbose' is recommended for development and the first production release await Adapty().setLogLevel(AdaptyLogLevel.verbose); // Or set it during configuration await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withLogLevel(AdaptyLogLevel.verbose), ); ``` ### 数据政策 \{#data-policies\} Adapty 不存储用户的个人数据,除非你明确发送,但你可以实施额外的数据安全策略以符合应用商店或国家/地区的规定。 #### 禁用 IP 地址收集与共享 \{#disable-ip-address-collection-and-sharing\} 在激活 Adapty 模块时,将 `ipAddressCollectionDisabled` 设置为 `true` 即可禁用用户 IP 地址的收集与共享。默认值为 `false`。 使用此参数可以增强用户隐私保护、遵守区域性数据保护法规(如 GDPR 或 CCPA),或在您的应用不需要基于 IP 的功能时减少不必要的数据收集。 ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withIpAddressCollectionDisabled(true), ); ``` #### 禁用广告 ID 的收集与共享 \{#disable-advertising-id-collection-and-sharing\} 激活 Adapty 模块时,将 `appleIdfaCollectionDisabled`(iOS)或 `googleAdvertisingIdCollectionDisabled`(Android)设置为 `true` 可禁用广告标识符的收集。默认值为 `false`。 如需遵守 App Store/Play Store 政策、避免触发应用追踪透明度提示,或者您的应用不需要基于广告 ID 的广告归因或分析功能,请使用此参数。 ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withAppleIdfaCollectionDisabled(true) // iOS ..withGoogleAdvertisingIdCollectionDisabled(true), // Android ); ``` #### 为 AdaptyUI 设置媒体缓存配置 \{#set-up-media-cache-configuration-for-adaptyui\} 该模块会随 Adapty SDK 自动激活。如果你不使用付费墙编辑工具,想要停用 AdaptyUI 模块,请在激活时传入 `withActivateUI(false)`。 默认情况下,AdaptyUI 会缓存媒体文件(如图片和视频)以提升性能、减少网络流量。你可以通过提供自定义配置来调整缓存设置。 使用 `withMediaCacheConfiguration` 可覆盖默认缓存限制。该方法为可选项——如果不调用,将使用默认值(磁盘大小 100MB,内存数量不限)。但一旦创建了配置对象,其所有参数均为必填项。 ```dart showLineNumbers title="main.dart" final mediaCacheConfig = AdaptyUIMediaCacheConfiguration( memoryStorageTotalCostLimit: 200 * 1024 * 1024, // 200 MB memoryStorageCountLimit: 2147483647, // max int value diskStorageSizeLimit: 200 * 1024 * 1024, // 200 MB ); await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withMediaCacheConfiguration(mediaCacheConfig), ); ``` **参数:** | 参数 | 是否必填 | 描述 | |-------------------------|----------|-----------------------------------------------------------------------------| | memoryStorageTotalCostLimit | 必填 | 内存缓存总大小,单位为字节。默认值为 100 MB。 | | memoryStorageCountLimit | 必填 | 内存存储的条目数量限制。默认值为 int 最大值。 | | diskStorageSizeLimit | 必填 | 磁盘文件大小限制,单位为字节。默认值为 100 MB。 | ### 启用本地访问等级(Android)\{#enable-local-access-levels-android\} 默认情况下,[本地访问等级](local-access-levels) 在 iOS 上已启用,在 Android 上处于禁用状态。如需在 Android 上也启用此功能,请将 `withGoogleLocalAccessLevelAllowed` 设置为 `true`: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleLocalAccessLevelAllowed(true), ); ``` ### 备份恢复时清除数据 \{#clear-data-on-backup-restore\} 将 `appleClearDataOnBackup` 设置为 `true` 后,SDK 会检测应用是否从 iCloud 备份中恢复,并删除所有本地存储的 SDK 数据,包括已缓存的用户画像信息、产品详情和付费墙。随后 SDK 将以全新状态重新初始化。默认值为 `false`。 :::note 仅删除本地 SDK 缓存。Apple 的交易历史记录和 Adapty 服务器上的用户数据不受影响。 ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withAppleClearDataOnBackup(true) // default – false ); ``` ## 故障排查 \{#troubleshooting\} #### Android 备份规则(Auto Backup 配置)\{#android-backup-rules-auto-backup-configuration\} 部分 SDK(包括 Adapty)会自带 Android 自动备份配置。如果多个 SDK 都定义了备份规则,Android 清单合并工具可能会报错,提示 `android:fullBackupContent`、`android:dataExtractionRules` 或 `android:allowBackup` 相关问题。 常见错误示例:`Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note 以下更改应在你的 Android 平台目录(通常位于项目的 `android/` 文件夹)中进行。 ::: 要解决此问题,你需要: - 告知清单合并工具使用应用自身的备份相关属性值。 - 创建备份规则文件,将 Adapty 的规则与其他 SDK 的规则合并。 #### 1. 在清单中添加 `tools` 命名空间 \{#1-add-the-tools-namespace-to-your-manifest\} 在 `AndroidManifest.xml` 文件中,确保根标签 `` 包含 tools: ```xml ... ``` #### 2. 在 `` 中覆盖备份属性 \{#2-override-backup-attributes-in-application\} 在同一个 `AndroidManifest.xml` 文件中,更新 `` 标签,使应用提供最终属性值,并告知清单合并工具替换库中的值: ```xml ... ``` 如果某个 SDK 也设置了 `android:allowBackup`,请将其一并加入 `tools:replace`: ```xml tools:replace="android:allowBackup,android:fullBackupContent,android:dataExtractionRules" ``` #### 3. 创建合并后的备份规则文件 \{#3-create-merged-backup-rules-files\} 在 Android 项目的 `res/xml/` 目录下创建 XML 文件,将 Adapty 的规则与其他 SDK 的规则合并。Android 根据系统版本使用不同的备份规则格式,同时创建两个文件可确保应用所支持的所有 Android 版本都能正常兼容。 :::note 以下示例以 AppsFlyer 作为第三方 SDK 的示例。请替换或添加你应用中实际使用的其他 SDK 的规则。 ::: **Android 12 及更高版本**(使用新的数据提取规则格式): ```xml title="sample_data_extraction_rules.xml" ``` **Android 11 及更低版本**(使用旧版完整备份内容格式): ```xml title="sample_backup_rules.xml" #### 在 Android 上从其他应用返回后购买失败 \{#purchases-fail-after-returning-from-another-app-in-android\} 如果启动购买流程的 Activity 使用了非默认的 `launchMode`,当用户从 Google Play、银行应用或浏览器返回时,Android 可能会错误地重建或复用该 Activity,导致购买结果丢失或被视为已取消。 为确保购买正常进行,请为启动购买流程的 Activity 仅使用 `standard` 或 `singleTop` 启动模式,避免使用其他任何模式。 在您的 `AndroidManifest.xml` 中,确保启动购买流程的 Activity 设置为 `standard` 或 `singleTop`: ```xml ``` #### 由 Podfile 中 SWIFT_VERSION 覆盖引起的 Swift 6 构建错误 \{#swift-6-build-errors-caused-by-podfile-swift_version-override\} 在为 iOS 构建 Flutter 应用时,您可能会看到 Adapty pod 目标上出现 Swift 6 编译错误。常见症状包括:`AdaptyUIBuilderLogic` 中的 `@Sendable` 不匹配、Adapty 类型缺少 `Sendable` 一致性,或 actor 隔离错误。 The Adapty pods 声明 `s.swift_version = '6.0'` 并要求使用 Swift 6 进行构建。你自己的应用代码可以继续使用 Swift 5——只有 Adapty pod 目标(`Adapty`、`AdaptyUI`、`AdaptyUIBuilder`、`AdaptyLogger`、`AdaptyPlugin`)需要使用 Swift 6 构建。 最常见的原因是 `ios/Podfile` 中存在 `post_install` 钩子,它会为每个 pod 目标重写 `SWIFT_VERSION`: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` **修复方法**:将 Adapty pod targets 排除在覆盖范围之外: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| next if %w[Adapty AdaptyUI AdaptyUIBuilder AdaptyLogger AdaptyPlugin].include?(target.name) target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` 然后在 `ios/` 目录下运行 `pod install` 并重新构建。 如需验证,请打开 `ios/Pods/Pods.xcodeproj`,选择 `Adapty` pod target → **Build Settings** → **Swift Language Version**,确认其值为 **Swift 6**。 --- # File: flutter-quickstart-paywalls --- --- title: "在 Flutter SDK 中使用付费墙编辑工具启用购买功能" description: "使用 Adapty 付费墙编辑工具启用应用内购买的快速入门指南。" --- 要启用应用内购买,你需要了解三个核心概念: - [**产品**](product) – 用户可以购买的任何内容(订阅、消耗型商品、永久授权) - [**流程**](adapty-flow-builder) – 向用户展示产品的界面序列,通过无代码 Flow Builder 构建。SDK 通过 `getFlow` 获取它们。如果你更倾向于在自己的代码中构建 UI,请使用付费墙代替 — 参见[手动实现付费墙](flutter-quickstart-manual)。 - [**版位**](placements) – 在应用中的何处、何时展示流程(如 `main`、`onboarding`、`settings`)。你在看板中将流程关联到版位,然后在代码中通过版位 ID 请求它们。这样可以轻松运行 A/B 测试,并向不同用户展示不同的流程。 Adapty 为您提供三种在应用中启用购买的方式。请根据您的应用需求选择其中一种: | 实现方式 | 复杂度 | 适用场景 | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Flow Builder | ✅ 简单 | 您[在无代码编辑工具中创建完整的、可直接购买的流程](quickstart-paywalls)。Adapty 会自动渲染,并在后台处理所有复杂的购买流程、收据验证和订阅管理。 | | 手动创建付费墙 | 🟡 中等 | 您在应用代码中自行实现付费墙 UI,但仍从 Adapty 获取流程对象,以保持产品方案的灵活性。请参阅[指南](flutter-quickstart-manual)。 | | Observer 模式 | 🔴 困难 | 您已有自己的购买处理基础设施,并希望继续使用。请注意,Observer 模式在 Adapty 中存在一定限制。请参阅[文章](observer-vs-full-mode)。 | :::important **以下步骤说明如何实现在 Adapty Flow Builder 中创建的流程。** 如果您希望自行构建付费墙 UI,请参阅[手动实现付费墙](flutter-quickstart-manual)。 ::: 要在您的应用代码中展示 Adapty Flow Builder 中创建的流程,您只需: 1. **获取流程**:从 Adapty 获取流程。 2. **展示流程,Adapty 将为您处理购买**:在您的应用中显示该视图。 3. **处理按钮操作**:将用户交互与您的应用响应关联起来。例如,当用户点击按钮时打开链接或关闭流程。 ## 开始之前 \{#before-you-start\} 在开始之前,请完成以下步骤: 1. 在 Adapty 看板中将你的应用连接到 [App Store](initial_ios) 和/或 [Google Play](initial-android)。 2. 在 Adapty 中[创建产品](create-product)。 3. [创建流程并向其添加产品](create-paywall)。 4. [创建版位并将流程添加到其中](create-placement)。 5. 在应用代码中[安装并激活 Adapty SDK](sdk-installation-flutter)。本指南使用 Adapty Flutter SDK v4 API。 :::tip 完成这些步骤最快的方式是参照[快速入门指南](quickstart),或使用 [Developer CLI](developer-cli-quickstart) 创建付费墙和版位。 ::: ## 1. 获取流程 \{#1-get-the-flow\} 你的流程与在看板中配置的版位相关联。通过版位,你可以为不同的目标受众运行不同的流程,或进行 [A/B 测试](ab-tests)。 要获取在 Adapty 付费墙编辑工具中创建的流程,你需要: 1. 使用 `getFlow` 方法通过[版位](placements) ID 获取 `flow` 对象,并通过 `hasViewConfiguration` 属性检查该流程是否由编辑工具创建。 2. 使用 `createFlowView` 方法创建流程视图。该视图包含显示流程所需的 UI 元素和样式。 :::important 要获取视图配置,必须在编辑工具中开启 **Show on device** 开关。否则将获取到空的视图配置,流程也不会显示。 ::: ```dart showLineNumbers try { // the requested flow final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); final view = await AdaptyUI().createFlowView( flow: flow, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## 2. 展示流程 \{#display-the-flow\} 获取到 flow 视图后,只需添加几行代码即可将其展示出来。 要展示流程,请对由 `createFlowView` 方法创建的 `view` 调用 `view.present()` 方法。每个 `view` 只能展示一次:关闭后,该视图会从内存中释放。如果需要再次展示流程,请重新调用 `createFlowView` 以创建新的 `view` 实例。 ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip 有关如何展示流程的更多详情,请参阅我们的[指南](flutter-present-paywalls)。 ::: ## 3. 处理按钮操作 \{#handle-button-actions\} 当用户点击流程中的按钮时,Flutter SDK 会自动处理购买、恢复购买、关闭视图和打开 URL 等操作。但其他按钮具有自定义或预定义的 ID,需要在代码中处理相应操作。 要控制或监控流程屏幕上的操作,请实现 `AdaptyUIFlowsEventsObserver` 方法,并在展示任何屏幕之前设置观察者。当用户执行某个操作时,`flowViewDidPerformAction` 会被触发,您的应用需要根据操作 ID 做出相应响应。 三个观察者方法是**必须实现的**:`flowViewDidFinishPurchase`、`flowViewDidFinishRestore` 和 `flowViewDidReceiveError` — 缺少这些方法,代码将无法编译。 :::tip 请参阅我们的指南,了解如何处理按钮[操作](flutter-handle-paywall-actions)和[事件](flutter-handling-events)。 ::: 将观察者实现为一个专用的长生命周期对象,而不是 widget。由于整个应用只有一个全局观察者插槽,将其绑定到 `State` 会导致页面泄漏(SDK 持有对它的强引用),并且在下一个页面注册自身时会被静默替换。使用 `extends` 还会继承 SDK 的默认行为,因此除了三个必需方法外,你只需覆盖自己关心的回调即可。 ```dart showLineNumbers title="Flutter" // A dedicated, long-lived handler for flow events. // It does NOT live inside a Widget/State, so it never leaks and is never // silently replaced when screens are pushed or popped. class FlowEventsHandler extends AdaptyUIFlowsEventsObserver { // A single, app-wide instance — same idiom as Adapty() and AdaptyUI(). static final FlowEventsHandler _instance = FlowEventsHandler._(); factory FlowEventsHandler() => _instance; FlowEventsHandler._(); // This method is called when user performs an action on the flow UI. // Overriding it replaces the default behavior (dismiss on close, open URLs), // so keep those cases if you want to preserve it. @override void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): // close the flow on the Android back button view.dismiss(); break; case OpenUrlAction(:final url, :final openIn): AdaptyUI().openUrl(url, openIn: openIn); break; default: break; } } // Required: decide what happens after a purchase finishes @override void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { if (purchaseResult is! AdaptyPurchaseResultUserCancelled) { view.dismiss(); } } // Required: dismiss the flow once a restore succeeds @override void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { view.dismiss(); } // Required: handle rendering and other view errors @override void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { print('Flow error: $error'); view.dismiss(); } } ``` 在应用启动时**注册一次**处理程序,须在任何流程显示之前完成: ```dart showLineNumbers title="Flutter" AdaptyUI().setFlowsEventsObserver(FlowEventsHandler()); ``` ## 后续步骤 \{#next-steps\} :::tip 有疑问或遇到问题?欢迎访问我们的[支持论坛](https://adapty.featurebase.app/),在那里你可以找到常见问题的解答,也可以提出自己的问题。我们的团队和社区随时为你提供帮助! ::: 您的付费墙已准备好在应用中显示。在 [App Store 沙盒](test-purchases-in-sandbox)或 [Google Play Store](testing-on-android) 中测试您的购买,确保可以从付费墙完成测试购买。 现在,您需要[检查用户的访问等级](flutter-check-subscription-status),以确保您向正确的用户展示付费墙或授予付费功能的访问权限。 ## 完整示例 \{#full-example\} 下面展示了如何将上述所有步骤整合到你的应用中。 ```dart void main() { // Register a single, long-lived observer once, before any flow is shown. // It is intentionally a plain object (NOT a Widget/State): its lifetime is the // whole app, so it never leaks and is never silently replaced when screens are // pushed or popped. AdaptyUI().setFlowsEventsObserver(FlowEventsHandler()); runApp(MaterialApp(home: FlowScreen())); } /// A dedicated handler for AdaptyUI flow events. /// /// It `extends` [AdaptyUIFlowsEventsObserver] (rather than being implemented /// by a `State`), which gives you two things for free: /// * the SDK's sensible defaults for optional callbacks, so besides the three /// required methods you only override what you actually care about; /// * a lifecycle that is independent of the widget tree — there is no strong /// reference back into a `Widget`, so nothing leaks and there is nothing to /// unregister. /// /// Every callback receives the [AdaptyUIFlowView] it relates to, so handling /// flow actions never requires a `BuildContext` or widget state. class FlowEventsHandler extends AdaptyUIFlowsEventsObserver { // A single, app-wide instance — same idiom as Adapty() and AdaptyUI(). static final FlowEventsHandler _instance = FlowEventsHandler._(); factory FlowEventsHandler() => _instance; FlowEventsHandler._(); // Called when the user performs an action on the flow UI. @override void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): // close the flow on the Android back button view.dismiss(); break; case OpenUrlAction(:final url, :final openIn): // Open the URL natively, honoring the dashboard browser setting. AdaptyUI().openUrl(url, openIn: openIn); break; default: break; } } // Required: decide what happens after a purchase finishes. @override void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { if (purchaseResult is! AdaptyPurchaseResultUserCancelled) { view.dismiss(); } } // Required: dismiss the flow once a restore succeeds. @override void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { view.dismiss(); } // Required: handle rendering and other view errors. @override void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { print('Flow error: $error'); view.dismiss(); } } class FlowScreen extends StatefulWidget { const FlowScreen({super.key}); @override State createState() => _FlowScreenState(); } class _FlowScreenState extends State { @override void initState() { super.initState(); _showFlowIfNeeded(); } Future _showFlowIfNeeded() async { try { final flow = await Adapty().getFlow( placementId: 'YOUR_PLACEMENT_ID', ); if (!flow.hasViewConfiguration) return; final view = await AdaptyUI().createFlowView(flow: flow); await view.present(); } catch (_) { // Handle any errors (network, SDK issues, etc.) } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Adapty Flow Example')), body: Center( // Add a button to re-trigger the flow for testing purposes. child: ElevatedButton( onPressed: _showFlowIfNeeded, child: const Text('Show Flow'), ), ), ); } } ``` --- # File: flutter-check-subscription-status --- --- title: "在 Flutter SDK 中检查订阅状态" description: "了解如何在 Flutter 应用中使用 Adapty 检查订阅状态。" --- 要决定用户是否可以访问付费内容或查看付费墙,您需要检查其用户画像中的[访问等级](access-level)。 本文介绍如何访问用户画像状态,以决定向用户展示什么内容——是显示付费墙还是授予付费功能访问权限。 ## 获取订阅状态 \{#get-subscription-status\} 当您决定是否向用户显示付费墙或付费内容时,需要检查其用户画像中的[访问等级](access-level)。您有两种选择: - 如果需要立即获取最新用户画像数据(例如应用启动时)或希望强制更新,请调用 `getProfile`。 - 设置**自动用户画像更新**,以保留一份本地副本,该副本会在订阅状态发生变化时自动刷新。 ### 获取用户画像 \{#get-profile\} 获取订阅状态最简单的方法是使用 `getProfile` 方法访问用户画像: ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### 监听订阅更新 \{#listen-to-subscription-updates\} 要在应用中自动接收用户画像更新: 1. 使用 `Adapty().didUpdateProfileStream.listen()` 监听用户画像变更——每当用户的订阅状态发生变化时,Adapty 会自动调用此方法。 2. 在此方法被调用时保存更新后的用户画像数据,这样无需额外发起网络请求,即可在应用各处直接使用。 ```dart class SubscriptionManager { AdaptyProfile? _currentProfile; SubscriptionManager() { // Listen for profile updates Adapty().didUpdateProfileStream.listen((profile) { _currentProfile = profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() bool hasAccess() { return _currentProfile?.accessLevels['premium']?.isActive ?? false; } } ``` :::note Adapty 会在应用启动时自动调用用户画像更新流监听器,即使设备处于离线状态,也能提供已缓存的订阅数据。 ::: ## 将用户画像与付费墙逻辑关联 \{#connect-profile-with-paywall-logic\} 当你需要立即决定是否显示付费墙或授予付费功能访问权限时,可以直接检查用户的用户画像。这种方式适用于应用启动、进入高级功能区域或展示特定内容之前等场景。 ```dart Future _checkAccessLevel() async { try { final profile = await Adapty().getProfile(); return profile.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false; } catch (e) { print('Error checking access level: $e'); return false; // Show paywall if access check fails } } Future _initializePaywall() async { await _loadPaywall(); final hasAccess = await _checkAccessLevel(); if (!hasAccess) { // Show paywall if no access } } ``` ## 后续步骤 \{#next-steps\} 现在您已了解如何追踪订阅状态,接下来请学习如何[管理用户画像](flutter-quickstart-identify),以确保用户能够访问其已付费的内容。 --- # File: flutter-quickstart-identify --- --- title: "在 Flutter SDK 中识别用户" description: "在 Flutter 中设置 Adapty 以管理应用内订阅的快速入门指南。" --- :::important 本指南适用于拥有自有认证系统的开发者。你将在此了解如何在 Adapty 中管理用户画像,使其与现有认证系统保持一致。 ::: 如何管理用户购买行为取决于你的应用认证模式: - 如果你的应用不使用后端认证且不存储用户数据,请参阅[匿名用户部分](#anonymous-users)。 - 如果你的应用已有(或将有)后端认证,请参阅[已识别用户部分](#identified-users)。 **核心概念**: - **用户画像**是 SDK 正常运行所必需的实体,由 Adapty 自动创建。 - 用户画像可以是匿名的**(不含 customer user ID)**,也可以是已识别的**(含 customer user ID)**。 - 你提供 **customer user ID** 是为了将 Adapty 中的用户画像与你内部的身份验证系统进行关联对照。 以下是匿名用户与已识别用户的区别: | | 匿名用户 | 已识别用户 | |-------------------------|-----------------------------------|-----------------------------------------------| | **购买管理** | 商店级购买恢复 | 通过客户用户 ID 跨设备保留购买历史记录 | | **用户画像管理** | 每次重装生成新的用户画像 | 跨会话和设备使用同一用户画像 | | **数据持久性** | 匿名用户的数据与应用安装绑定 | 已识别用户的数据在应用重装后仍然保留 | ## 匿名用户 \{#anonymous-users\} 如果你没有后端身份验证,**则无需在应用代码中处理身份验证**: 1. 当 SDK 在应用首次启动时被激活,Adapty 会**为该用户创建一个新的用户画像**。 2. 当用户在应用内完成任何购买后,该购买记录会**与其 Adapty 用户画像及应用商店账户关联**。 3. 当用户**重新安装**应用或在**新设备**上安装时,Adapty 会**在激活时创建一个新的匿名用户画像**。 4. 如果用户之前已在您的应用中完成过购买,默认情况下,其购买记录会在 SDK 激活时自动从 App Store 同步。 对于匿名用户,每次安装都会创建新的用户画像,但这不是问题——在 Adapty 看板的分析设置中,你可以[配置什么将被视为新安装](general#4-installs-definition-for-analytics)。 对于匿名用户,你需要按**设备 ID** 统计安装量。在这种情况下,设备上的每次应用安装(包括重新安装)都会被计为一次安装。 ## 已识别用户 \{#identified-users\} 在应用中识别用户有两种方式: - [**登录/注册时:**](#during-loginsignup) 如果用户在应用启动后才登录,请在用户完成身份验证时调用 `identify()`,并传入 customer user ID。 - [**SDK 激活时:**](#during-the-sdk-activation) 如果应用启动时已有存储的 customer user ID,可在调用 `activate()` 时直接传入。 :::important 默认情况下,当 Adapty 收到来自某个 Customer User ID 的购买请求,而该 ID 当前与另一个 Customer User ID 关联时,访问等级将被共享,因此两个用户画像都拥有付费访问权限。你可以配置此设置,将付费访问权限从一个用户画像转移到另一个,或完全禁用共享功能。详情请参阅[这篇文章](general#6-sharing-paid-access-between-user-accounts)。 ::: ### 登录/注册时 \{#during-loginsignup\} 如果您是在应用启动后才识别用户身份(例如在用户登录或注册之后),请使用 `identify` 方法来设置其 customer user ID。 - 如果您**之前从未使用过该 customer user ID**,Adapty 会自动将其与当前用户画像关联。 - 如果您**之前已使用该 customer user ID 识别过用户**,Adapty 将切换至与该 customer user ID 关联的用户画像。 :::important 客户用户 ID 对每个用户必须唯一。如果将参数值硬编码,所有用户将被视为同一个人。 ::: 调用其他 SDK 方法之前,请务必 `await` `identify`。并发调用会产生 `#3006 profileWasChanged` 错误或落到匿名用户画像上。详见 [Flutter SDK 的调用顺序](flutter-sdk-call-order)。 ```dart showLineNumbers try { await Adapty().identify(customerUserId); // 每个用户唯一 } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### 在 SDK 激活期间 \{#during-the-sdk-activation\} 如果在激活 SDK 时已知道用户的 customer user ID,可以直接在 `activate` 方法中传入,无需单独调用 `identify`。 如果已知 customer user ID,但在激活之后才进行设置,则意味着在激活时 Adapty 会创建一个新的匿名用户画像,并仅在你调用 `identify` 后才切换到已有的用户画像。 您可以传入已有的 customer user ID(之前使用过的),也可以传入一个全新的。如果传入新的,激活时创建的新用户画像将自动与该 customer user ID 关联。 :::note 默认情况下,创建匿名用户画像不会影响分析看板,因为安装量是基于设备 ID 统计的。 设备 ID 代表应用在设备上的一次安装实例,仅在应用重新安装后才会重新生成。 无论是首次安装还是重复安装,也无论是否使用了已有的客户用户 ID,设备 ID 均不受影响。 创建用户画像(在 SDK 激活或退出登录时)、登录,或在不重新安装应用的情况下升级应用,均不会产生新的安装事件。 如果您希望按唯一用户而非设备来统计安装量,请前往 **App settings** 并配置 [**Installs definition for analytics**](general#4-installs-definition-for-analytics)。 ::: ```dart showLineNumbers" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID) // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. ); } catch (e) { // handle the error } ``` ### 退出用户登录 \{#log-users-out\} 如果您有供用户退出登录的按钮,请使用 `logout` 方法。 :::important 退出用户登录会为该用户创建新的匿名用户画像。 ::: ```dart showLineNumbers try { await Adapty().logout(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` :::info 要将用户重新登录到应用,请使用 `identify` 方法。 ::: ### 允许未登录状态下购买 \{#allow-purchases-without-login\} 如果你的用户在登录前和登录后都可以进行购买,你需要确保他们登录后仍能保留访问权限: 1. 当未登录用户完成购买时,Adapty 会将该购买绑定到其匿名用户画像 ID。 2. 当用户登录账号后,Adapty 会切换到其已识别的用户画像。 - 如果是新的 customer user ID(例如购买发生在注册之前),Adapty 会将该 customer user ID 分配给当前用户画像,所有购买记录均得以保留。 - 如果是已存在的 customer user ID(该 customer user ID 已关联某个用户画像),则需要在切换用户画像后获取实际的访问等级。你可以在识别完成后立即调用 [`getProfile`](flutter-check-subscription-status),也可以[监听用户画像更新](flutter-check-subscription-status)以实现数据自动同步。 ## 下一步 \{#next-steps\} 恭喜!您已经在应用中完成了应用内支付逻辑的接入!祝您的应用变现一切顺利! 要充分发挥 Adapty 的价值,可以探索以下主题: - [**测试**](troubleshooting-test-purchases):确保一切正常运行 - [**用户引导**](flutter-onboardings):通过用户引导吸引用户并提升留存率 - [**集成**](configuration):只需一行代码,即可与营销归因和分析服务集成 - [**设置自定义用户画像属性**](flutter-setting-user-attributes):为用户画像添加自定义属性并创建市场细分,以便针对不同用户启动 A/B 测试或展示不同的付费墙 --- # File: adapty-sdk-integration-skill-flutter --- --- title: "使用 SDK 集成技能将 Adapty 集成到 Flutter 应用" description: "使用 adapty-sdk-integration 技能,借助 AI 编程工具将 Adapty SDK 端到端集成到 Flutter 应用中。" --- :::important 该技能目前处于 Beta 阶段。如果出现卡顿或异常行为,请改用[逐步集成指南](adapty-cursor-flutter)——它会引导你的 AI 工具完成每个阶段,并提供正确的文档。 ::: [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill) 可以端到端地自动完成 Adapty 集成:看板配置、SDK 安装、付费墙设置以及各阶段验证。它会自动检测你的平台,并在每个阶段获取相关的 Adapty 文档。 **支持的工具**:Claude Code、GitHub Copilot CLI、OpenAI Codex、Gemini CLI。 安装时,请选择适合你所用工具的方式。完整列表请参阅 [skill README](https://github.com/adaptyteam/adapty-sdk-integration-skill)。 **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex 或其他工具** — 使用 [skills CLI](https://skills.sh)(注意:通过此方式安装的 skill 不会自动更新): ``` npx skills add adaptyteam/adapty-sdk-integration-skill ``` 也可以克隆仓库,将 `skills/adapty-sdk-integration/` 复制到你所用工具的 skills 目录中。 安装完成后,在项目中运行该 skill: ``` /adapty-sdk-integration ``` skill 会提出几个配置问题,然后逐步引导你完成看板配置、SDK 安装、付费墙设置和验证。 --- # File: adapty-cursor-flutter --- --- title: "借助 AI 将 Adapty 集成到 Flutter 应用中" description: "使用 Cursor、Context7、ChatGPT、Claude 或其他 AI 工具,将 Adapty 集成到 Flutter 应用的分步指南。" --- 本指南将引导你逐步使用 AI 编程工具将 Adapty 集成到 Flutter 应用中——你只需按正确的顺序为它提供 Adapty 文档即可。 For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## 开始前:看板配置 \{#before-you-start-dashboard-setup\} 在编写任何 SDK 代码之前,Adapty 需要进行一些看板配置。您可以使用交互式 LLM 技能,或通过看板手动操作。 ### 技能方式(推荐)\{#skill-approach-recommended\} Adapty CLI 技能让您的 LLM 能够直接设置应用、产品、访问等级、付费墙和版位——无需为每个步骤打开看板。您只需在看板中[连接您的商店](integrate-payments)即可。 ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` 添加技能后,在您的代理中运行 `/adapty-cli`。它将引导您完成每个步骤——包括何时打开看板连接您的商店。 ### 看板配置方式 \{#dashboard-approach\} 如果你更倾向于手动配置,以下是编写代码前需要准备的内容。LLM 无法自动查找看板中的值,需要你手动提供。 1. **连接应用商店**:在 Adapty 看板中,前往 **App settings → General**,将 App Store 和 Google Play 均完成连接(如果你的 Flutter 应用需要同时支持两个平台)。这是购买功能正常运行的必要条件。 [连接应用商店](integrate-payments) 2. **复制公共 SDK 密钥**:在 Adapty 看板中,前往 **App settings → General**,找到 **API keys** 部分。在代码中,这就是你传入 Adapty 配置的字符串。 3. **至少创建一个产品**:在 Adapty 看板中,前往 **Products** 页面。你无需在代码中直接引用产品——Adapty 会通过付费墙将其下发。 [添加产品](quickstart-products) 4. **创建付费墙和版位**:在 Adapty 看板中,在 **Paywalls** 页面创建付费墙,然后在 **Placements** 页面将其分配到版位。在代码中,版位 ID 就是传给 `Adapty().getPaywall()` 的字符串。 [创建付费墙](quickstart-paywalls) 5. **设置访问等级**:在 Adapty 看板的 **Products** 页面中,按产品进行配置。在代码中,通过 `profile.accessLevels['premium']?.isActive` 检查对应字符串。默认的 `premium` 访问等级适用于大多数应用。如果付费用户会根据所购产品获得不同功能的访问权限(例如 `basic` 方案与 `pro` 方案),请在开始编写代码之前[创建额外的访问等级](assigning-access-level-to-a-product)。 :::tip 准备好这五项信息后,就可以开始写代码了。告诉你的 LLM:"我的 Public SDK key 是 X,我的版位 ID 是 Y",这样它就能生成正确的初始化和付费墙获取代码。 ::: ### 准备就绪后再设置 \{#set-up-when-ready\} 以下内容不是开始编码的必要条件,但随着集成的深入,您会需要它们: - **A/B 测试**:在 **Placements** 页面进行配置,无需更改代码。 [A/B 测试](ab-tests) - **更多付费墙和版位**:使用不同的版位 ID 添加更多 `getPaywall` 调用。 - **分析集成**:在 **Integrations** 页面进行配置,具体设置因集成而异。请参阅[分析集成](analytics-integration)和[归因集成](attribution-integration)。 ## 向 LLM 提供 Adapty 文档 \{#feed-adapty-docs-to-your-llm\} ### 使用 Context7(推荐) [Context7](https://context7.com) 是一个 MCP 服务器,可让你的 LLM 直接访问最新的 Adapty 文档。LLM 会根据你的提问自动获取相关文档,无需手动粘贴 URL。 Context7 支持 **Cursor**、**Claude Code**、**Windsurf** 及其他兼容 MCP 的工具。运行以下命令即可完成配置: ``` npx ctx7 setup ``` 该命令会自动检测你的编辑器并配置 Context7 服务器。如需手动配置,请参阅 [Context7 GitHub 仓库](https://github.com/upstash/context7)。 配置完成后,在提示词中引用 Adapty 库: ``` Use the adaptyteam/adapty-docs library to look up how to install the Flutter SDK ``` :::warning 虽然 Context7 无需手动粘贴文档链接,但实现顺序很重要。请按照下方的[实现步骤](#implementation-walkthrough)逐步操作,确保一切正常运行。 ::: ### 使用纯文本文档 \{#use-plain-text-docs\} 您可以以纯文本 Markdown 格式访问任何 Adapty 文档。在 URL 末尾添加 `.md`,或点击文章标题下方的 **Copy for LLM**。例如:[adapty-cursor-flutter.md](https://adapty.io/docs/zh/adapty-cursor-flutter.md)。 下面[实现演练](#implementation-walkthrough)的每个阶段都包含一个"发送给 LLM"的代码块,其中包含可粘贴的 `.md` 链接。 如需一次获取更多文档,请参阅下方的[索引文件和平台特定子集](#plain-text-doc-index-files)。 ## 实现演练 \{#implementation-walkthrough\} 本指南的其余部分按实现顺序介绍 Adapty 集成。每个阶段都包含发送给 LLM 的文档、完成后的预期效果以及常见问题。 ### 规划你的集成方案 \{#plan-your-integration\} 在开始写代码之前,先让 LLM 分析你的项目并制定实现计划。如果你使用的 AI 工具支持规划模式(比如 Cursor 或 Claude Code 的 plan 模式),建议先用它让 LLM 同时读取你的项目结构和 Adapty 文档,再动手写代码。 告诉你的 LLM 你使用的是哪种购买方式——这会影响它需要参考的文档: - [**Adapty 付费墙编辑工具**](adapty-paywall-builder):在 Adapty 的无代码编辑工具中创建付费墙,SDK 会自动渲染。 - [**手动创建的付费墙**](flutter-making-purchases):自行编写付费墙 UI 代码,但仍使用 Adapty 获取产品并处理购买。 - [**观察者模式**](observer-vs-full-mode):保留现有的购买基础设施,仅使用 Adapty 进行数据分析和集成。 不确定该选哪个?请查看[快速入门中的对比表](flutter-quickstart-paywalls)。 ### 安装并配置 SDK \{#install-and-configure-the-sdk\} 使用 `flutter pub add` 添加 Adapty SDK 依赖,并通过你的 Public SDK key 激活。这是一切的基础——没有它,其他功能都无法运行。 **指南:** [安装并配置 Adapty SDK](sdk-installation-flutter) 将以下内容发送给你的 LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/zh/sdk-installation-flutter.md ``` :::tip[Checkpoint] - **预期结果:** 应用可在 iOS 和 Android 上正常构建和运行,调试控制台显示 Adapty 激活日志。 - **常见问题:** 出现"Public API key is missing"→ 检查是否已将占位符替换为 **App settings** 中的真实密钥。 ::: ### 显示付费墙并处理购买 \{#show-paywalls-and-handle-purchases\} 通过版位 ID 获取付费墙、展示付费墙并处理购买事件。具体需要参考哪些指南,取决于你处理购买的方式。 在操作过程中,每完成一步就在沙盒中测试一次购买——不要等到最后再测。设置说明请参阅[在沙盒中测试购买](test-purchases-in-sandbox)。 **指南:** - [使用付费墙启用购买(快速入门)](flutter-quickstart-paywalls) - [获取付费墙编辑工具付费墙及其配置](flutter-get-pb-paywalls) - [展示付费墙](flutter-present-paywalls) - [处理付费墙事件](flutter-handling-events) - [响应按钮操作](flutter-handle-paywall-actions) 请将以下内容发送给您的 LLM: ``` 在编写代码之前,请先阅读这些 Adapty 文档: - https://adapty.io/docs/zh/flutter-quickstart-paywalls.md - https://adapty.io/docs/zh/flutter-get-pb-paywalls.md - https://adapty.io/docs/zh/flutter-present-paywalls.md - https://adapty.io/docs/zh/flutter-handling-events.md - https://adapty.io/docs/zh/flutter-handle-paywall-actions.md ``` :::tip[检查点] - **预期效果:** 付费墙显示已配置的产品,点击产品会触发沙盒购买对话框。 - **常见问题:** 付费墙为空或出现 `getPaywall` 错误 → 检查版位 ID 是否与看板中完全一致,以及该版位是否已分配目标受众。 ::: **指南:** - [在自定义付费墙中启用购买功能(快速入门)](flutter-quickstart-manual) - [获取付费墙和产品](fetch-paywalls-and-products-flutter) - [渲染通过远程配置设计的付费墙](present-remote-config-paywalls-flutter) - [发起购买](flutter-making-purchases) - [恢复购买](flutter-restore-purchase) 将这段内容发送给你的 LLM: ``` 在编写代码之前,请先阅读以下 Adapty 文档: - https://adapty.io/docs/zh/flutter-quickstart-manual.md - https://adapty.io/docs/zh/fetch-paywalls-and-products-flutter.md - https://adapty.io/docs/zh/present-remote-config-paywalls-flutter.md - https://adapty.io/docs/zh/flutter-making-purchases.md - https://adapty.io/docs/zh/flutter-restore-purchase.md ``` :::tip[Checkpoint] - **预期效果:** 你的自定义付费墙能够展示从 Adapty 获取的产品,点击产品会触发沙盒购买弹窗。 - **常见问题:** 产品列表为空 → 请确认看板中已为付费墙分配产品,且版位已设置目标受众。 ::: **指南:** - [Observer 模式概述](observer-vs-full-mode) - [实现 Observer 模式](implement-observer-mode-flutter) - [在 Observer 模式中上报交易](report-transactions-observer-mode-flutter) 将以下内容发送给你的 LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/zh/observer-vs-full-mode.md - https://adapty.io/docs/zh/implement-observer-mode-flutter.md - https://adapty.io/docs/zh/report-transactions-observer-mode-flutter.md ``` :::tip[检查点] - **预期结果:** 使用现有购买流程完成沙盒购买后,该交易应出现在 Adapty 看板的 **Event Feed** 中。 - **注意事项:** 没有事件 → 请确认你已向 Adapty 上报交易,并已为两个应用商店配置了服务器通知。 ::: ### 检查订阅状态 \{#check-subscription-status\} 购买后,检查用户画像中的活跃访问等级,以控制高级内容的访问权限。 **指南:**[检查订阅状态](flutter-check-subscription-status) 发送给您的 LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/zh/flutter-check-subscription-status.md ``` :::tip[检查点] - **预期效果:** 沙盒购买后,`profile.accessLevels['premium']?.isActive` 返回 `true`。 - **注意事项:** 购买后 `accessLevels` 为空 → 检查看板中该产品是否已分配访问等级。 ::: ### 识别用户 \{#identify-users\} 将您的应用用户账户与 Adapty 用户画像关联,确保购买记录在多设备间持久保存。 :::important 如果您的应用无需身份验证,请跳过此步骤。 ::: **指南:** [识别用户](flutter-quickstart-identify) 将以下内容发送给您的 LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/zh/flutter-quickstart-identify.md ``` :::tip[Checkpoint] - **预期结果:** 调用 `Adapty().identify()` 后,看板的 **Profiles** 页面会显示你的自定义用户 ID。 - **注意事项:** 请在激活之后、获取付费墙之前调用 `identify`,以避免匿名用户画像归因问题。 ::: ### 准备发布 \{#prepare-for-release\} 沙盒中的集成测试通过后,请按照发布检查清单逐项确认,确保一切都已准备好投入生产。 **指南:** [发布检查清单](release-checklist) 将以下内容发送给你的 LLM: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/zh/release-checklist.md ``` :::tip[Checkpoint] - **预期结果:** 所有检查项均已确认:商店连接、服务器通知、购买流程、访问等级检查,以及隐私要求。 - **注意事项:** 如果缺少服务器通知 → 请在 **App settings → iOS SDK** 中配置 App Store 服务器通知,在 **App settings → Android SDK** 中配置 Google Play 实时开发者通知。 ::: ## 纯文本文档索引文件 \{#plain-text-doc-index-files\} 如果你需要为 LLM 提供超出单个页面范围的更广泛上下文,我们提供了列出或汇总所有 Adapty 文档的索引文件: - [`llms.txt`](https://adapty.io/docs/zh/llms.txt):列出所有页面及其 `.md` 链接。这是一种[新兴标准](https://llmstxt.org/),旨在让 LLM 能够访问网站内容。请注意,对于某些 AI 代理(如 ChatGPT),你需要先下载 `llms.txt`,然后将其作为文件上传到对话中。 - [`llms-full.txt`](https://adapty.io/docs/zh/llms-full.txt):将整个 Adapty 文档站点合并为单个文件。内容非常庞大——仅在需要完整信息时使用。 - Flutter 专用版本 [`flutter-llms.txt`](https://adapty.io/docs/zh/flutter-llms.txt) 和 [`flutter-llms-full.txt`](https://adapty.io/docs/zh/flutter-llms-full.txt):平台专属子集,相比完整站点可节省 token 用量。 --- # File: flutter-get-pb-paywalls --- --- title: "获取流程与付费墙 - Flutter" description: "在 Flutter 应用中从 Adapty 获取流程和付费墙。" --- 在[设计好您的流程或付费墙编辑工具付费墙](adapty-paywall-builder)之后,您可以在移动应用中展示它。第一步是获取与版位关联的流程或付费墙及其视图配置,具体方法如下所述。 请注意,本主题涉及流程和付费墙编辑工具定制的付费墙。如果您是手动实现付费墙,请参阅[在移动应用中为远程配置付费墙获取付费墙和产品](fetch-paywalls-and-products-flutter)主题。 :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 :::
在开始于您的移动应用中展示流程和付费墙之前(点击展开) 1. 在 Adapty 看板中[创建您的产品](create-product)。 2. 在 Adapty 看板中[创建流程/付费墙并将产品加入其中](create-paywall)。 3. 在 Adapty 看板中[创建版位并将流程/付费墙加入其中](create-placement)。 4. 在您的移动应用中安装 [Adapty SDK](sdk-installation-flutter)。
## 获取流程/付费墙 \{#fetch-flowpaywall\} 如果你已使用流程编辑工具或付费墙编辑工具设计了流程或付费墙,则无需在移动应用代码中手动处理其渲染逻辑来向用户展示。此类流程或付费墙已包含展示内容和展示方式的完整配置。不过,你仍需通过版位获取其 ID 和视图配置,然后在移动应用中进行呈现。 尽早获取流程或付费墙并[创建其视图](flutter-get-pb-paywalls#fetch-the-view-configuration)——最好在展示之前就提前完成。`createFlowView` 方法会加载视图配置,并在后台开始下载和缓存图片。调用时机越早,下载完成的时间就越充裕。等到真正展示流程或付费墙时,其配置和图片往往已经缓存好、随时可用。 使用 `getFlow` 方法获取流程或付费墙: ```dart showLineNumbers try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow/paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` 参数: | 参数 | 是否必填 | 描述 | |---------|--------|-----------| | **placementId** | 必填 | 目标[版位](placements)的标识符。这是你在 Adapty 看板中创建版位时指定的值。 | | **fetchPolicy** | 默认:`.reloadRevalidatingCacheData` |

默认情况下,SDK 会尝试从服务器加载数据,若加载失败则返回缓存数据。我们推荐使用此选项,因为它能确保用户始终获取最新数据。

但如果你认为用户的网络连接不稳定,可以考虑使用 `.returnCacheDataElseLoad`——在缓存存在的情况下直接返回缓存数据。这样用户获取的数据可能不是最新的,但无论网络状况多差,加载速度都会更快。缓存会定期更新,因此在会话期间使用缓存来减少网络请求是安全可靠的。

请注意,缓存在应用重启后依然保留,只有在应用重新安装或手动清除时才会被清空。

Adapty SDK 通过两层机制在本地存储付费墙:上述定期更新的缓存,以及[备用付费墙](fallback-paywalls)。我们还使用 CDN 来加快付费墙的获取速度,并在 CDN 不可用时提供独立的备用服务器。整套机制旨在确保你始终能获取最新版本的付费墙,同时在网络条件较差时也能保证可靠性。

| | **loadTimeout** | 默认:5 秒 |

限制该方法超时时间的 `Duration` 值。若超时,将返回缓存数据或本地备用数据。

请注意,在极少数情况下,该方法的实际超时时间可能略晚于 `loadTimeout` 中指定的时间,因为底层操作可能由多个请求组成。

| 响应参数: | 参数 | 描述 | | :-------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Flow | 一个 `AdaptyFlow` 对象,包含流程的标识符(`instanceIdentity`、`variationId`)、名称、版位、其付费墙实验变体(`paywalls`)以及任何远程配置(`remoteConfigs`)。 | ## 获取视图配置 \{#fetch-the-view-configuration\} :::important 请确保在编辑工具中启用了 **Show on device** 开关。如果未开启此选项,将无法获取视图配置。 ::: 如果版位是在 **Flow Builder** 或 **Paywall Builder** 中设计的,Adapty 会自动为你渲染 UI —— 所获取的 flow 的 `hasViewConfiguration` 属性为 `true`。使用 `createFlowView` 创建视图,然后[展示 flow 或付费墙](flutter-present-paywalls)。如果版位是未使用编辑工具的自定义付费墙(`hasViewConfiguration` 为 `false`),请改为[将其作为远程配置付费墙处理](present-remote-config-paywalls-flutter)。 :::warning `createFlowView` 方法的返回结果只能呈现一次。如需再次呈现,请重新调用 `createFlowView` 方法。 ::: ```dart showLineNumbers try { final view = await AdaptyUI().createFlowView(flow: flow); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` 参数: | 参数 | 是否必填 | 描述 | | :------------------- | :------- | :----------------------------------------------------------- | | **flow** | 必填 | 一个 `AdaptyFlow` 对象,用于获取所需流程/付费墙的视图。 | | **customTags** | 选填 | 定义自定义标签及其对应值的映射。自定义标签作为内容中的占位符,在流程/付费墙中动态替换为指定字符串,实现个性化内容。详情请参阅[付费墙编辑工具中的自定义标签](custom-tags-in-paywall-builder)。 | | **preloadProducts** | 选填 | 启用后可优化产品在屏幕上的显示时机。设为 `true` 时,AdaptyUI 将自动预加载所需产品。默认值:`false`。 | | **loadTimeout** | 选填 | 一个 `Duration`,用于限制视图配置的加载超时时间。若超时,将使用缓存数据或本地备用内容。 | :::note 如果您使用多种语言,请了解如何添加[流程本地化](add-paywall-locale-in-adapty-paywall-builder),以及如何正确使用语言区域代码([点击此处了解](flutter-localizations-and-locale-codes))。 ::: 完成视图设置后,[展示流程/付费墙](flutter-present-paywalls)。 ## 获取默认目标受众的流程或付费墙以加快加载速度 \{#get-a-flow-or-paywall-for-a-default-audience-to-fetch-it-faster\} 通常情况下,流程和付费墙的加载几乎是即时完成的,无需担心速度问题。但如果你配置了大量目标受众和版位,且用户的网络连接较弱,加载流程或付费墙可能会比预期慢。在这种情况下,你可能希望先展示默认流程或付费墙,以保证用户体验的流畅性,而不是什么都不显示。 为了解决这个问题,你可以使用 `getFlowForDefaultAudience` 方法,该方法会获取指定版位中针对 **All Users** 目标受众的流程或付费墙。但需要特别注意的是,推荐的做法是通过 `getFlow` 方法来获取流程或付费墙,详情请参阅上方的[获取流程/付费墙](#fetch-flowpaywall)章节。 :::warning 为什么我们推荐使用 `getFlow` `getFlowForDefaultAudience` 方法存在一些明显的缺点: - **潜在的向后兼容性问题**:如果你需要为不同的应用版本(当前版本和未来版本)展示不同的付费墙,可能会面临挑战。你要么针对当前(旧版)版本设计兼容的付费墙,要么接受旧版用户可能遇到付费墙无法渲染的问题。 - **目标定向失效**:所有用户都将看到针对 **All Users** 目标受众设计的同一个付费墙,这意味着你将失去个性化定向能力(包括基于国家、营销归因或自定义属性的定向)。 如果你愿意接受这些弊端以换取更快的流程或付费墙加载速度,请按如下方式使用 `getFlowForDefaultAudience` 方法。否则,请继续使用[上文](#fetch-flowpaywall)介绍的 `getFlow`。 ::: ```dart showLineNumbers try { final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow/paywall } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` | 参数 | 是否必填 | 描述 | |---------|--------|-----------| | **placementId** | 必填 | [版位](placements) 的标识符。这是您在 Adapty 看板中创建版位时所指定的值。 | | **fetchPolicy** | 默认值:`.reloadRevalidatingCacheData` |

默认情况下,SDK 会尝试从服务器加载数据,若加载失败则返回缓存数据。我们推荐使用此方案,因为它能确保用户始终获取最新数据。

但如果您认为用户的网络环境不稳定,可以考虑使用 `.returnCacheDataElseLoad`——当缓存数据存在时直接返回缓存。这样一来,用户获取的数据可能不是最新的,但无论网络状况如何,加载速度都会更快。缓存会定期更新,因此在会话期间使用缓存来减少网络请求是安全的。

请注意,重启应用后缓存依然保留,只有在重新安装应用或手动清理时才会被清除。

| ## 自定义资源 \{#customize-assets\} 要自定义流程/付费墙中的图片和视频,请实现自定义资源。 主图和视频有预定义的 ID:`hero_image` 和 `hero_video`。在自定义资源包中,通过这些 ID 来定位对应元素并自定义其行为。 对于其他图片和视频,您需要在 Adapty 看板中[设置自定义 ID](custom-media)。 例如,您可以: - 向部分用户展示不同的图片或视频。 - 在远程主图加载时,先显示本地预览图。 - 在播放视频前先显示预览图。 以下是如何通过简单字典提供自定义资源的示例: ```dart final customAssets = { // Show a local image using a custom ID 'custom_image': AdaptyCustomAsset.localImageAsset( assetId: 'assets/images/image_name.png', ), // Show a local video with a preview image 'hero_video': AdaptyCustomAsset.localVideoAsset( assetId: 'assets/videos/custom_video.mp4', ), }; try { final view = await AdaptyUI().createFlowView( flow: flow, customAssets: customAssets, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note 如果找不到某个资源,流程/付费墙将回退到其默认外观。 ::: ## 设置开发者自定义计时器 \{#set-up-developer-defined-timers\} 要在移动应用中使用自定义计时器,请将 `customTimers` 映射传递给 `createFlowView` 方法。映射中每个键为计时器 ID,对应的值为定义计时器结束时间的 `DateTime` 对象。示例如下: ```dart showLineNumbers try { final view = await AdaptyUI().createFlowView( flow: flow, customTimers: { 'CUSTOM_TIMER_6H': DateTime.now().add(const Duration(seconds: 3600 * 6)), 'CUSTOM_TIMER_NY': DateTime(2027, 1, 1), // New Year 2027 }, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` 在此示例中,`CUSTOM_TIMER_NY` 和 `CUSTOM_TIMER_6H` 是你在 Adapty 看板中设置的开发者自定义计时器的 **Timer ID**。`customTimers` 映射可确保你的应用动态地为每个计时器更新正确的值。例如: - `CUSTOM_TIMER_NY`:距计时器结束时间(如元旦)的剩余时间。 - `CUSTOM_TIMER_6H`:用户打开流程后,6 小时倒计时的剩余时间。
在 [Adapty 看板中使用新版付费墙编辑工具完成付费墙的视觉设计](adapty-paywall-builder)后,你可以在移动应用中展示它。第一步是获取与版位关联的付费墙及其视图配置,具体方法如下。 :::warning 新版付费墙编辑工具需要 Flutter SDK 3.3.0 或更高版本。 ::: 请注意,本主题适用于使用付费墙编辑工具自定义的付费墙。如果您是手动实现付费墙,请参阅[在移动应用中获取远程配置付费墙的付费墙和产品](fetch-paywalls-and-products-flutter)主题。 :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 :::
在移动应用中展示付费墙之前(点击展开) 1. 在 Adapty 看板中[创建产品](create-product)。 2. 在 Adapty 看板中[创建付费墙并将产品添加到其中](create-paywall)。 3. 在 Adapty 看板中[创建版位并将付费墙添加到其中](create-placement)。 4. 在您的移动应用中安装 [Adapty SDK](sdk-installation-flutter)。
## 获取使用付费墙编辑工具设计的付费墙 \{#fetch-paywall-designed-with-paywall-builder\} 如果你已经[使用付费墙编辑工具设计了付费墙](adapty-paywall-builder),就无需在移动应用代码中手动处理渲染逻辑来向用户展示它。这类付费墙同时包含展示内容和展示方式。不过,你仍需通过版位获取其 ID 和视图配置,然后在移动应用中将其呈现出来。 为确保最佳性能,请尽早获取付费墙及其[视图配置](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder),以便在向用户展示之前有足够的时间完成图片下载。 使用 `getPaywall` 方法获取付费墙: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 参数: | 参数 | 是否必填 | 描述 | |---------|--------|-----------| | **placementId** | 必填 | 目标[版位](placements)的标识符。这是你在 Adapty 看板中创建版位时指定的值。 | | **locale** |

可选

默认值:`en`

|

[付费墙本地化](add-paywall-locale-in-adapty-paywall-builder)的标识符。该参数应为语言代码,由一个或两个子标签组成,子标签之间用连字符(**-**)分隔。第一个子标签表示语言,第二个子标签表示地区。

示例:`en` 表示英语,`pt-br` 表示巴西葡萄牙语。

有关语言区域代码及推荐用法,请参阅[本地化与语言区域代码](flutter-localizations-and-locale-codes)。

| | **fetchPolicy** | 默认值:`.reloadRevalidatingCacheData` |

默认情况下,SDK 会尝试从服务器加载数据,若加载失败则返回缓存数据。我们推荐使用此方式,因为它能确保用户始终获取最新数据。

但如果你认为用户的网络环境不稳定,可以考虑使用 `.returnCacheDataElseLoad`,在缓存存在时直接返回缓存数据。这种情况下,用户获取的数据可能不是最新的,但无论网络状况如何,加载速度都会更快。缓存会定期更新,因此在会话期间使用缓存以减少网络请求是安全的。

请注意,缓存在应用重启后仍然保留,只有在重新安装应用或手动清除时才会被清空。

Adapty SDK 在本地以两层方式存储付费墙:上述定期更新的缓存,以及[备用付费墙](fallback-paywalls)。我们还使用 CDN 加快付费墙的获取速度,并在 CDN 不可用时提供独立的备用服务器。这套机制旨在确保你始终获取最新版本的付费墙,同时在网络条件较差的情况下也能保证可靠性。

| | **loadTimeout** | 默认值:5 秒 |

该值限制此方法的超时时间。若超时,将返回缓存数据或本地备用数据。

请注意,在极少数情况下,此方法的实际超时时间可能略晚于 `loadTimeout` 中指定的时间,因为底层操作可能包含多个请求。

对于 Android:你可以使用扩展函数创建 `TimeInterval`(例如 `5.seconds`,其中 `.seconds` 来自 `import com.adapty.utils.seconds`),或使用 `TimeInterval.seconds(5)`。如需不设限制,请使用 `TimeInterval.INFINITE`。

| 响应参数: | 参数 | 描述 | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | 一个 [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) 对象,包含产品 ID 列表、付费墙标识符、远程配置及其他若干属性。 | ## 获取使用付费墙编辑工具设计的付费墙视图配置 \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important 请确保在付费墙编辑工具中开启 **Show on device** 开关。如果未启用此选项,将无法获取视图配置。 ::: 获取付费墙后,检查其是否包含 `ViewConfiguration`,这表明该付费墙是使用付费墙编辑工具创建的。这将帮助你确定如何展示该付费墙。如果存在 `ViewConfiguration`,则将其作为付费墙编辑工具付费墙处理;如果不存在,请[将其作为远程配置付费墙处理](present-remote-config-paywalls-flutter)。 ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` 获取视图后,[展示付费墙](flutter-present-paywalls)。 ## 为默认目标受众获取付费墙以加快加载速度 \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\} 通常情况下,付费墙的获取几乎是即时完成的,无需担心速度问题。但如果你配置了大量目标受众和付费墙,且用户的网络连接较差,获取付费墙可能会比预期耗时更长。在这种情况下,你可能希望优先展示默认付费墙,以保证流畅的用户体验,而不是让用户看到空白页面。 要解决这个问题,你可以使用 `getPaywallForDefaultAudience` 方法,它会获取指定版位中 **All Users** 目标受众对应的付费墙。但需要特别注意的是,推荐的做法是通过 `getPaywall` 方法来获取付费墙,详见上方的[获取付费墙信息](flutter-get-pb-paywalls#fetch-paywall-designed-with-paywall-builder)章节。 :::warning 为什么我们推荐使用 `getPaywall` `getPaywallForDefaultAudience` 方法存在以下几个明显的缺点: - **潜在的向后兼容问题**:如果你需要为不同的应用版本(当前版本和未来版本)展示不同的付费墙,可能会遇到挑战。你要么将付费墙设计为同时兼容当前(旧版)版本,要么接受使用当前(旧版)版本的用户可能遇到付费墙无法渲染的问题。 - **失去定向能力**:所有用户都将看到专为 **All Users** 目标受众设计的同一个付费墙,这意味着你将失去个性化定向功能(包括基于国家、营销归因或自定义属性的定向)。 如果你愿意接受这些不足,以换取更快的付费墙加载速度,可按如下方式使用 `getPaywallForDefaultAudience` 方法。否则,请继续使用[上文](#fetch-paywall-designed-with-paywall-builder)介绍的 `getPaywall` 方法。 ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` :::note `getPaywallForDefaultAudience` 方法从 Flutter SDK 3.2.0 版本起可用。 ::: | 参数 | 是否必填 | 描述 | |---------|--------|-----------| | **placementId** | 必填 | [版位](placements)的标识符。这是你在 Adapty 看板中创建版位时指定的值。 | | **locale** |

可选

默认值:`en`

|

[付费墙本地化](add-remote-config-locale)的标识符。该参数应为语言代码,由一个或多个子标签组成,子标签之间用短横线(**-**)分隔。第一个子标签表示语言,第二个子标签表示地区。

示例:`en` 表示英语,`pt-br` 表示巴西葡萄牙语。

有关语言区域代码及推荐使用方式,请参阅[本地化与语言区域代码](localizations-and-locale-codes)。

| | **fetchPolicy** | 默认值:`.reloadRevalidatingCacheData` |

默认情况下,SDK 会尝试从服务器加载数据,若失败则返回缓存数据。我们推荐使用此方式,因为它能确保用户始终获取最新数据。

不过,如果你认为用户的网络状况不稳定,可以考虑使用 `.returnCacheDataElseLoad`——在缓存数据存在时直接返回缓存。这种方式下用户获取的数据可能不是最新的,但无论网络状况如何,加载速度都会更快。缓存会定期更新,因此在会话期间使用缓存来避免网络请求是安全的。

请注意,缓存在应用重启后依然保留,只有在应用重新安装或手动清除时才会被清空。

| ## 自定义资源 \{#customize-assets\} 要自定义付费墙中的图片和视频,请实现自定义资源。 主图和主视频有预定义的 ID:`hero_image` 和 `hero_video`。在自定义资源包中,你可以通过这些 ID 定位对应元素并自定义其行为。 对于其他图片和视频,你需要在 Adapty 看板中[设置自定义 ID](custom-media)。 例如,你可以: - 为部分用户显示不同的图片或视频。 - 在远程主图加载时,先显示本地预览图。 - 在播放视频前显示预览图片。 :::important 要使用此功能,请将 Adapty Flutter SDK 更新至 3.8.0 或更高版本。 ::: 以下是如何通过简单字典提供自定义资源的示例: ```dart final customAssets = { // Show a local image using a custom ID 'custom_image': AdaptyCustomAsset.localImageAsset( assetId: 'assets/images/image_name.png', ), // Show a local video with a preview image 'hero_video': AdaptyCustomAsset.localVideoAsset( assetId: 'assets/videos/custom_video.mp4', ), }; try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customAssets: customAssets, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note 如果找不到相应素材,付费墙将回退到其默认外观。 ::: ## 设置自定义计时器 \{#set-up-developer-defined-timers\} 要在移动应用中使用自定义计时器,请将 `customTimers` 映射传递给 `createPaywallView` 方法。映射中的每个键是计时器 ID,对应的值是一个 `DateTime` 对象,用于定义计时器的结束时间。示例如下: ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customTimers: { 'CUSTOM_TIMER_6H': DateTime.now().add(const Duration(seconds: 3600 * 6)), 'CUSTOM_TIMER_NY': DateTime(2025, 1, 1), // New Year 2025 }, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` 在此示例中,`CUSTOM_TIMER_NY` 和 `CUSTOM_TIMER_6H` 是您在 Adapty 看板中设置的开发者自定义计时器的 **Timer ID**。`customTimers` 映射确保您的应用为每个计时器动态更新正确的值。例如: - `CUSTOM_TIMER_NY`:距计时器结束时间(如元旦)的剩余时长。 - `CUSTOM_TIMER_6H`:从用户打开付费墙开始计算的 6 小时倒计时剩余时长。
--- # File: flutter-present-paywalls --- --- title: "展示流程与付费墙 - Flutter" description: "使用 Adapty 的变现功能在 Flutter 应用中展示流程和付费墙。" --- 如果你已经使用流程编辑工具(Flow Builder)或付费墙编辑工具(Paywall Builder)设计了流程或付费墙,则无需在移动应用代码中手动实现渲染逻辑来向用户展示它。这类流程或付费墙本身已经包含了展示内容与展示方式的完整定义。 :::warning 本指南适用于流程和付费墙编辑工具构建的付费墙。如需展示**远程配置付费墙**,请参阅[渲染远程配置设计的付费墙](present-remote-config-paywalls-flutter)。 ::: Adapty Flutter SDK 提供两种展示流程和付费墙的方式: - **独立页面** - **嵌入式组件** ## 作为独立屏幕展示 \{#present-as-standalone-screen\} 要将流程或付费墙作为独立屏幕显示,请对通过 [`createFlowView`](flutter-get-pb-paywalls#fetch-the-view-configuration) 方法创建的 `view` 调用 `view.present()` 方法。每个 `view` 只能展示一次:关闭后,该 view 会从内存中释放。如需再次显示流程或付费墙,请重新调用 `createFlowView` 创建新的 `view` 实例。 ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 ::: ### 关闭流程或付费墙 \{#dismiss-the-flow-or-paywall\} 当需要以编程方式关闭流程或付费墙时,请使用 `dismiss()` 方法: ```dart showLineNumbers title="Flutter" try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note 关闭视图会将其从内存中释放——已关闭的视图无法再次呈现。请改用 `createFlowView` 创建新的视图。 ::: ### 显示对话框 \{#show-dialog\} 在 Android 上展示流程或付费墙视图时,请使用此方法替代原生的弹窗对话框。在 Android 上,普通弹窗会出现在视图背后,用户无法看到。此方法可确保对话框在所有平台上都能正确显示在流程或付费墙的上方。 ```dart showLineNumbers title="Flutter" try { final action = await view.showDialog( title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', ); if (action == AdaptyUIDialogActionType.secondary) { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (e) { // handle error } ``` ### 配置 iOS 展示样式 \{#configure-ios-presentation-style\} 通过向 `present()` 方法传递 `iosPresentationStyle` 参数,可以配置流程或付费墙在 iOS 上的展示方式。该参数接受 `AdaptyUIIOSPresentationStyle.fullScreen`(默认值)或 `AdaptyUIIOSPresentationStyle.pageSheet`。 ```dart showLineNumbers try { await view.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## 嵌入到 widget 层级中 \{#embed-in-widget-hierarchy\} 如需将流程或付费墙嵌入到现有的 widget 树中,可以直接在 Flutter widget 层级里使用 `AdaptyUIFlowPlatformView` widget。 ```dart showLineNumbers title="Flutter" AdaptyUIFlowPlatformView( flow: flow, // The flow object you fetched onDidAppear: (view) { }, onDidDisappear: (view) { }, onDidPerformAction: (view, action) { }, onDidSelectProduct: (view, productId) { }, onDidStartPurchase: (view, product) { }, onDidFinishPurchase: (view, product, purchaseResult) { }, onDidFailPurchase: (view, product, error) { }, onDidStartRestore: (view) { }, onDidFinishRestore: (view, profile) { }, onDidFailRestore: (view, error) { }, onDidReceiveError: (view, error) { }, onDidFailLoadingProducts: (view, error) { }, onDidFinishWebPaymentNavigation: (view, product, error) { }, ) ``` :::note 对于 Android 平台视图,请确保你的 `MainActivity` 继承自 `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: 如果你已使用付费墙编辑工具自定义了付费墙,则无需在移动应用代码中手动处理其渲染逻辑来向用户展示它。此类付费墙已包含展示内容及展示方式的完整配置。 :::warning 本指南仅适用于**新版付费墙编辑工具**创建的付费墙,需要 SDK v3.2.0 或更高版本。不同版本付费墙编辑工具设计的付费墙及远程配置付费墙的展示流程各有不同。 - 如需展示**远程配置付费墙**,请参阅[展示远程配置设计的付费墙](present-remote-config-paywalls-flutter)。 ::: Adapty Flutter SDK 提供两种付费墙展示方式: - **独立页面** - **嵌入式组件** ## 作为独立页面展示 \{#present-as-standalone-screen\} 要将付费墙作为独立页面展示,请在由 [`createPaywallView`](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) 方法创建的 `view` 上调用 `view.present()` 方法。每个 `view` 只能使用一次。如果需要再次展示付费墙,请重新调用 `createPaywallView` 创建新的 `view` 实例。 :::warning 重复使用同一个 `view` 而不重新创建,可能会导致 `AdaptyUIError.viewAlreadyPresented` 错误。 ::: ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 ::: ### 关闭付费墙 \{#dismiss-the-paywall\} 当你需要以编程方式关闭付费墙时,请使用 `dismiss()` 方法: ```dart showLineNumbers title="Flutter" try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### 显示对话框 \{#show-dialog\} 在 Android 上展示付费墙视图时,请使用此方法代替原生的提示对话框。在 Android 上,普通提示框会出现在付费墙视图的后面,导致用户看不到它。此方法可确保对话框在所有平台上都能正确显示在付费墙的上方。 ```dart showLineNumbers title="Flutter" try { final action = await view.showDialog( title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', ); if (action == AdaptyUIDialogActionType.secondary) { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (e) { // handle error } ``` ### 配置 iOS 呈现样式 \{#configure-ios-presentation-style\} 通过向 `present()` 方法传递 `iosPresentationStyle` 参数,可以配置付费墙在 iOS 上的呈现方式。该参数接受 `AdaptyUIIOSPresentationStyle.fullScreen`(默认值)或 `AdaptyUIIOSPresentationStyle.pageSheet` 两个值。 ```dart showLineNumbers try { await view.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## 嵌入到组件层级中 \{#embed-in-widget-hierarchy\} 如需将付费墙嵌入到现有的组件树中,可以直接在 Flutter 组件层级里使用 `AdaptyUIPaywallPlatformView` 组件。 ```dart showLineNumbers title="Flutter" AdaptyUIPaywallPlatformView( paywall: paywall, // The paywall object you fetched onDidAppear: (view) { }, onDidDisappear: (view) { }, onDidPerformAction: (view, action) { }, onDidSelectProduct: (view, productId) { }, onDidStartPurchase: (view, product) { }, onDidFinishPurchase: (view, product, purchaseResult) { }, onDidFailPurchase: (view, product, error) { }, onDidStartRestore: (view) { }, onDidFinishRestore: (view, profile) { }, onDidFailRestore: (view, error) { }, onDidFailRendering: (view, error) { }, onDidFailLoadingProducts: (view, error) { }, onDidFinishWebPaymentNavigation: (view, product, error) { }, ) ``` :::note 要使 Android 平台视图正常工作,请确保你的 `MainActivity` 继承自 `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: --- # File: flutter-handle-paywall-actions --- --- title: "在 Flutter SDK 中响应按钮操作" description: "使用 Adapty 在 Flutter 中处理付费墙按钮操作,提升应用变现效果。" --- 如果你正在使用 Adapty 编辑工具构建流程或付费墙,务必正确设置按钮: 1. 在[编辑工具中添加按钮](paywall-buttons),并为其分配已有操作或创建自定义操作 ID。 2. 在应用代码中编写逻辑,处理每个已分配的操作。 本指南介绍如何在代码中处理自定义操作和已有操作。 :::warning **关闭视图和打开 URL 由默认的 `flowViewDidPerformAction` 实现自动处理**,SDK 本身负责处理购买和恢复操作。其他所有按钮动作,例如登录或打开另一个流程,则需要在应用代码中实现相应的响应逻辑。请注意,对*已完成*购买和恢复操作的响应发生在必要的观察者回调中——详见[处理流程和付费墙事件](flutter-handling-events)。 ::: ## 关闭流程和付费墙 \{#close-flows-and-paywalls\} 要添加一个关闭流程或付费墙的按钮,请在编辑工具中添加一个按钮,并为其分配 **Close** 操作。无需编写任何代码:默认的 `flowViewDidPerformAction` 实现会在收到 `CloseAction` 时自动关闭视图。 :::info Android 系统的**返回**按钮不再默认关闭视图。它会以 `AndroidSystemBackAction` 的形式传递给 `flowViewDidPerformAction`——如果你希望返回按钮能够关闭流程或付费墙,请自行处理该事件。 ::: 如需自定义行为,可覆写 `flowViewDidPerformAction`——例如,像 v3 中那样,在用户点击 Android 系统返回按钮时关闭视图: ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; case OpenUrlAction(:final url, :final openIn): AdaptyUI().openUrl(url, openIn: openIn); break; default: break; } } ``` :::warning 覆盖 `flowViewDidPerformAction` 会完全替换默认实现——如果你希望保留默认的关闭和打开 URL 行为,请保留 `CloseAction` 和 `OpenUrlAction` 的处理逻辑。 ::: ## 从流程和付费墙中打开 URL \{#open-urls-from-flows-and-paywalls\} :::tip 如果您想添加一组链接(例如使用条款和购买恢复),请在编辑工具中添加 **Link** 元素,并以与具有 **Open URL** 操作的按钮相同的方式处理它。 ::: 要添加一个打开链接的按钮(例如**使用条款**或**隐私政策**),请在编辑工具中添加一个按钮,为其分配 **Open URL** 操作,然后输入您想要打开的 URL。 无需编写代码:`flowViewDidPerformAction` 的默认实现会通过 `AdaptyUI().openUrl` 以原生方式打开 URL,并遵循看板中设置的应用内浏览器或外部浏览器偏好。 默认行为已能满足大多数场景的需求。如果你仍希望自行处理 URL 的打开方式,可以覆盖该处理器: ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): view.dismiss(); break; case OpenUrlAction(url: final url): // Open the URL in whatever way fits your app break; default: break; } } ``` ## 登录应用 \{#log-into-the-app\} 要添加一个让用户登录应用的按钮: 1. 在编辑工具中,添加一个按钮并为其分配 **Login** 操作。 2. 在应用代码中,实现一个 `login` 操作的处理器,用于识别当前用户。 ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'login'): // Navigate to your login screen in whatever way fits your app break; default: break; } } ``` ## 处理自定义操作 \{#handle-custom-actions\} 要添加一个处理其他操作的按钮: 1. 在编辑工具中,添加一个按钮,为其分配 **Custom** 操作,并为其设置一个 ID。 2. 在应用代码中,为你创建的操作 ID 实现对应的处理逻辑。 例如,如果你有另一组订阅优惠或一次性购买,可以添加一个按钮来展示另一个流程或付费墙: ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'openNewPaywall'): // Display another flow or paywall break; default: break; } } ``` 如果您正在使用 Adapty 付费墙编辑工具构建付费墙,正确设置按钮至关重要: 1. 在[付费墙编辑工具中添加按钮](paywall-buttons),并为其分配一个已有的操作,或创建一个自定义操作 ID。 2. 在应用代码中编写逻辑,处理您分配的每个操作。 本指南介绍如何在代码中处理自定义操作和预设操作。 :::warning **只有购买和恢复购买会被自动处理。** 其他所有按钮操作(例如关闭付费墙或打开链接)都需要在应用代码中实现相应的响应逻辑。 ::: ## 关闭付费墙 \{#close-paywalls\} 要添加一个关闭付费墙的按钮: 1. 在付费墙编辑工具中,添加一个按钮并为其分配 **Close** 操作。 2. 在应用代码中,为 `CloseAction` 和 `AndroidSystemBackAction` 操作实现处理程序。 :::info 在 Flutter SDK 中,`CloseAction` 和 `AndroidSystemBackAction` 操作默认会触发关闭付费墙。不过,如有需要,你可以在代码中覆盖此行为。例如,关闭一个付费墙可能会触发打开另一个付费墙。 ::: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; default: break; } } ``` ## 从付费墙打开 URL \{#open-urls-from-paywalls\} :::tip 如果你想添加一组链接(例如使用条款和购买恢复),可以在付费墙编辑工具中添加 **Link** 元素,并像处理带有 **Open URL** 操作的按钮一样进行处理。 ::: 要添加一个从付费墙打开链接的按钮(例如**使用条款**或**隐私政策**): 1. 在付费墙编辑工具中,添加一个按钮,为其分配 **Open URL** 操作,并输入要打开的 URL。 2. 在你的应用代码中,实现一个 `openUrl` 操作的处理器,用于在浏览器中打开接收到的 URL。 ```dart // You have to install url_launcher plugin in order to handle urls: // https://pub.dev/packages/url_launcher void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case OpenUrlAction(url: final url): final Uri uri = Uri.parse(url); launchUrl(uri, mode: LaunchMode.inAppBrowserView); break; default: break; } } ``` ## 登录应用 \{#log-into-the-app\} 要添加一个让用户登录应用的按钮: 1. 在付费墙编辑工具中,添加一个按钮并为其分配 **Login** 动作。 2. 在应用代码中,实现一个用于标识用户的 `login` 动作处理器。 ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'login'): // Navigate to your login screen in whatever way fits your app break; default: break; } } ``` ## 处理自定义操作 \{#handle-custom-actions\} 要添加一个处理其他任意操作的按钮: 1. 在付费墙编辑工具中,添加一个按钮,为其分配 **Custom** 操作,并为其指定一个 ID。 2. 在应用代码中,为你创建的操作 ID 实现对应的处理逻辑。 例如,如果你有另一组订阅方案或一次性购买产品,可以添加一个按钮来展示另一个付费墙: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'openNewPaywall'): // Display another paywall break; default: break; } } ``` --- # File: flutter-handling-events --- --- title: "Flutter - 处理 flow 与付费墙事件" description: "了解如何在 Flutter 中使用 Adapty 处理订阅相关事件,从而有效追踪用户交互。" --- :::important 本指南涵盖购买、恢复、产品选择和渲染的事件处理。关闭视图和打开链接由默认的 `flowViewDidPerformAction` 实现处理——如需覆盖这些行为或处理自定义按钮动作,请参阅[按钮动作处理指南](flutter-handle-paywall-actions)。 ::: 通过编辑工具配置的流程和付费墙无需额外代码即可完成购买和恢复购买操作。但它们会产生一些事件供你的应用响应,包括按钮点击(关闭按钮、URL、产品选择等)以及流程或付费墙上与购买相关的操作通知。请参阅以下内容了解如何响应这些事件。 如需在移动应用中控制或监控流程或付费墙屏幕上发生的过程,请实现 `AdaptyUIFlowsEventsObserver` 方法,并在展示任何屏幕之前设置观察者: ```dart showLineNumbers title="Flutter" AdaptyUI().setFlowsEventsObserver(this); ``` 有三个观察者方法是**必须实现**的——缺少它们类将无法编译:`flowViewDidFinishPurchase`、`flowViewDidFinishRestore` 和 `flowViewDidReceiveError`。其他方法均为可选。若要移除已设置的观察者,请向 `setFlowsEventsObserver` 传入 `null`。 :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 ::: 以下事件示例展示了每个对象上可用的属性,注释中提供了示意性的参考值。 ### 用户生成的事件 \{#user-generated-events\} #### 视图已出现 \{#view-appeared\} 当流程或付费墙视图显示在屏幕上时,将调用此方法。 :::note 在 iOS 上,当用户点击付费墙内的[网页付费墙按钮](web-paywall#step-2a-add-a-web-purchase-button),且网页付费墙在应用内浏览器中打开时,也会调用此方法。 ::: ```dart showLineNumbers title="Flutter" void flowViewDidAppear(AdaptyUIFlowView view) { } ``` #### 视图已消失 \{#view-disappeared\} 当流程或付费墙视图从屏幕上关闭时,将调用此方法。 :::note 在 iOS 上,当从付费墙中在应用内浏览器打开的 [web 付费墙](web-paywall#step-2a-add-a-web-purchase-button) 从屏幕消失时,也会触发此方法。 ::: ```dart showLineNumbers title="Flutter" void flowViewDidDisappear(AdaptyUIFlowView view) { } ``` #### 产品选择 \{#product-selection\} 当某个产品被选中购买(由用户或系统触发)时,将调用此方法: ```dart showLineNumbers title="Flutter" void flowViewDidSelectProduct(AdaptyUIFlowView view, String productId) { } ```
事件示例(点击展开) ```dart void flowViewDidSelectProduct(AdaptyUIFlowView view, String productId) { // productId is a String: productId; // 'premium_monthly' } ```
#### 开始购买 \{#started-purchase\} 当用户发起购买流程时,将调用此方法: ```dart showLineNumbers title="Flutter" void flowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product) { } ```
事件示例(点击展开) ```dart void flowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' product.localizedTitle; // 'Premium Monthly' product.localizedDescription; // 'Premium subscription for 1 month' product.price.amount; // 9.99 (double) product.price.currencyCode; // 'USD' product.price.localizedString; // '$9.99' } ```
#### 完成购买 \{#finished-purchase\} 此方法为**必需**。当购买成功、用户取消购买或购买处于待处理状态时,系统会调用此方法: ```dart showLineNumbers title="Flutter" void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // successful purchase break; case AdaptyPurchaseResultPending(): // purchase is pending break; case AdaptyPurchaseResultUserCancelled(): // user cancelled the purchase break; default: break; } } ```
事件示例(点击展开) ```dart void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) break; case AdaptyPurchaseResultPending(): // no additional data break; case AdaptyPurchaseResultUserCancelled(): // no additional data break; } } ```
:::info 与 v3 不同,此方法没有默认行为——视图不再在购买成功后自动关闭。请自行决定后续操作:继续流程或调用 `view.dismiss()`。有关关闭屏幕的详细信息,请参阅[响应按钮操作](flutter-handle-paywall-actions)。 ::: #### 完成 Web 支付导航 \{#finished-web-payment-navigation\} 此方法在尝试为特定产品打开 [Web 付费墙](web-paywall)后调用,包括导航成功和失败的情况: ```dart showLineNumbers title="Flutter" void flowViewDidFinishWebPaymentNavigation(AdaptyUIFlowView view, AdaptyPaywallProduct? product, AdaptyError? error) { } ``` **参数:** | 参数 | 描述 | |:------------|:---------------------------------------------------------------------------------------------------| | **product** | 打开 Web 付费墙时对应的 `AdaptyPaywallProduct`。可以为 `null`。 | | **error** | 如果 Web 付费墙导航失败,则为 `AdaptyError` 对象;导航成功时为 `null`。 | #### 购买失败 \{#failed-purchase\} 当购买失败时(例如由于支付问题或网络错误),此方法将被调用。对于用户主动取消或待处理的交易,**不会**触发此方法——这些情况由 `flowViewDidFinishPurchase` 处理: ```dart showLineNumbers title="Flutter" void flowViewDidFailPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyError error) { } ``` #### 开始恢复购买 \{#started-restore\} 如果用户发起恢复流程,此方法将被调用: ```dart showLineNumbers title="Flutter" void flowViewDidStartRestore(AdaptyUIFlowView view) { } ``` #### 恢复成功 \{#successful-restore\} 此方法为**必需**。如果购买恢复成功,将会调用此方法: ```dart showLineNumbers title="Flutter" void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { } ```
事件示例(点击展开) ```dart void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) profile.subscriptions['premium_monthly']?.isActive; // true profile.subscriptions['premium_monthly']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) } ```
如果用户已拥有所需的 `accessLevel`,我们建议关闭该页面。请参阅[订阅状态](flutter-listen-subscription-changes)了解如何检查,以及[响应按钮操作](flutter-handle-paywall-actions)了解如何关闭页面。 #### 恢复失败 \{#failed-restore\} 如果恢复购买失败,将触发此方法: ```dart showLineNumbers title="Flutter" void flowViewDidFailRestore(AdaptyUIFlowView view, AdaptyError error) { } ``` ### 数据获取与渲染 \{#data-fetching-and-rendering\} #### 产品加载错误 \{#product-loading-errors\} 如果你在初始化时没有传入产品数组,AdaptyUI 会自动从服务器获取所需对象。如果该操作失败,AdaptyUI 会通过调用以下方法来上报错误: ```dart showLineNumbers title="Flutter" void flowViewDidFailLoadingProducts(AdaptyUIFlowView view, AdaptyError error) { } ``` #### 视图错误 \{#view-errors\} 此方法为**必填项**。它替代了 v3 的 `paywallViewDidFailRendering` 方法:界面渲染过程中发生的错误以及其他视图错误,都会通过调用此方法来上报。实现此方法后,关闭视图的逻辑由你来决定——我们建议在发生此类错误时关闭视图,这也是未设置观察者时 SDK 内置默认行为的处理方式: ```dart showLineNumbers title="Flutter" void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { // log the error and dismiss the broken view view.dismiss(); } ``` 正常情况下不应出现渲染错误,如果遇到,请告知我们。 ### 分析事件 \{#analytics-events\} 可选的 `flowViewDidReceiveAnalyticEvent` 方法用于接收来自流程的自定义分析事件。目前流程尚未向您的代码发送此类事件,因此无需实现该方法。 ### 在观察者模式下处理购买 \{#handle-purchases-in-observer-mode\} 如果你以[观察者模式](implement-observer-mode-flutter)激活了 SDK,并展示了由 Adapty 渲染的流程或付费墙,SDK 不会自动为你发起购买。当用户点击购买或恢复购买按钮时,SDK 会调用你的 `AdaptyUIObserverModeResolver`。完整的配置说明请参阅[在观察者模式下展示流程](flutter-present-flows-in-observer-mode)。 ### 处理系统请求 \{#handle-system-requests\} `AdaptyUISystemRequestsHandler`(通过 `AdaptyUI().setSystemRequestsHandler(...)` 注册)用于处理来自流程的系统请求:操作系统权限提示(如推送通知或相机访问)以及 App Store 评价请求。目前流程尚未触发此类请求,因此无需注册处理程序。 如果你注册了处理器,请注意:`handlePermission` 是该类的必需方法——用你自己的代码请求权限,然后返回 `AdaptyUIPermissionResult.granted()` 或 `AdaptyUIPermissionResult.denied()`;`handleAppReviewRequest` 是可选的。
:::important 本指南介绍购买、恢复、产品选择和付费墙渲染的事件处理。你还必须实现按钮处理(关闭付费墙、打开链接等)。详情请参阅[按钮操作处理指南](flutter-handle-paywall-actions)。 ::: 使用[付费墙编辑工具](adapty-paywall-builder)配置的付费墙无需额外代码即可完成购买和恢复购买操作。但它们会产生一些事件,供你的应用响应。这些事件包括按钮点击(关闭按钮、URL、产品选择等)以及付费墙上与购买相关操作的通知。请参阅以下内容了解如何响应这些事件。 :::warning 本指南仅适用于**新版付费墙编辑工具付费墙**,需要 Adapty SDK v3.0 或更高版本。 ::: 要控制或监控移动应用中付费墙屏幕上发生的事件,请实现 `AdaptyUIPaywallsEventsObserver` 的相关方法,并在展示任何屏幕之前设置观察者: ```dart showLineNumbers title="Flutter" AdaptyUI().setPaywallsEventsObserver(this); ``` :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 ::: 以下事件示例展示了每个对象上可用的属性,注释中提供了示例值。 ### 用户生成的事件 \{#user-generated-events\} #### 付费墙已显示 \{#paywall-appeared\} 当付费墙视图显示在屏幕上时,会调用此方法。 :::note 在 iOS 上,当用户点击付费墙内的[网页付费墙按钮](web-paywall#step-2a-add-a-web-purchase-button),且网页付费墙在应用内浏览器中打开时,也会调用此方法。 ::: ```dart showLineNumbers title="Flutter" void paywallViewDidAppear(AdaptyUIPaywallView view) { } ``` #### 付费墙已关闭 \{#paywall-disappeared\} 当付费墙视图从屏幕上消失时,会调用此方法。 :::note 在 iOS 上,当从付费墙中打开的[网页付费墙](web-paywall#step-2a-add-a-web-purchase-button)在应用内浏览器中消失时,也会触发此方法。 ::: ```dart showLineNumbers title="Flutter" void paywallViewDidDisappear(AdaptyUIPaywallView view) { } ``` #### 商品选择 \{#product-selection\} 当用户或系统选中某个商品进行购买时,会触发此方法: ```dart showLineNumbers title="Flutter" void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) { } ```
事件示例(点击展开) ```dart void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) { // productId is a String: productId; // 'premium_monthly' } ```
#### 开始购买 \{#started-purchase\} 当用户发起购买流程时,将触发此方法: ```dart showLineNumbers title="Flutter" void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { } ```
事件示例(点击展开) ```dart void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' product.localizedTitle; // 'Premium Monthly' product.localizedDescription; // 'Premium subscription for 1 month' product.price.amount; // 9.99 (double) product.price.currencyCode; // 'USD' product.price.localizedString; // '$9.99' } ```
#### 购买完成 \{#finished-purchase\} 当购买成功、用户取消购买或购买处于待处理状态时,将调用此方法: ```dart showLineNumbers title="Flutter" void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // successful purchase break; case AdaptyPurchaseResultPending(): // purchase is pending break; case AdaptyPurchaseResultUserCancelled(): // user cancelled the purchase break; default: break; } } ```
事件示例(点击展开) ```dart void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) break; case AdaptyPurchaseResultPending(): // no additional data break; case AdaptyPurchaseResultUserCancelled(): // no additional data break; } } ```
我们建议在这种情况下关闭该页面。有关关闭付费墙页面的详细信息,请参阅[响应按钮操作](flutter-handle-paywall-actions)。 #### 完成 Web 支付跳转 \{#finished-web-payment-navigation\} 此方法在尝试为特定产品打开 [Web 付费墙](web-paywall)后触发,无论跳转成功还是失败均会调用: ```dart showLineNumbers title="Flutter" void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, AdaptyPaywallProduct? product, AdaptyError? error) { } ``` **参数:** | 参数 | 描述 | |:------------|:---------------------------------------------------------------------------------------------------| | **product** | 打开网页付费墙时对应的 `AdaptyPaywallProduct`。可以为 `null`。 | | **error** | 如果网页付费墙跳转失败,则为 `AdaptyError` 对象;跳转成功则为 `null`。 |
事件示例(点击展开) ```dart void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, AdaptyPaywallProduct? product, AdaptyError? error) { // product — AdaptyPaywallProduct?: product?.vendorProductId; // 'premium_monthly' if (error == null) { // navigation succeeded } else { // error — AdaptyError: error.code; // AdaptyErrorCode.networkFailed (2005) error.message; // 'Network request failed' error.detail; // platform-specific underlying error, or null } } ```
#### 购买失败 \{#failed-purchase\} 当购买失败时(例如因为支付问题或网络错误)会触发此方法。它**不会**在用户主动取消或待处理交易时触发——这些情况由 `paywallViewDidFinishPurchase` 处理: ```dart showLineNumbers title="Flutter" void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { } ```
事件示例(点击展开) ```dart void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' // error — AdaptyError: error.code; // AdaptyErrorCode.productPurchaseFailed (1006) error.message; // 'Product purchase failed.' error.detail; // platform-specific underlying error, or null } ```
#### 开始恢复购买 \{#started-restore\} 当用户发起恢复购买流程时,将触发此方法: ```dart showLineNumbers title="Flutter" void paywallViewDidStartRestore(AdaptyUIPaywallView view) { } ``` #### 恢复成功 \{#successful-restore\} 如果恢复购买成功,将调用此方法: ```dart showLineNumbers title="Flutter" void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { } ```
事件示例(点击展开) ```dart void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) profile.subscriptions['premium_monthly']?.isActive; // true profile.subscriptions['premium_monthly']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) } ```
如果用户已拥有所需的 `accessLevel`,我们建议关闭该页面。请参阅[订阅状态](flutter-listen-subscription-changes)主题了解如何检查,以及[响应按钮操作](flutter-handle-paywall-actions)主题了解如何关闭付费墙页面。 #### 恢复失败 \{#failed-restore\} 如果恢复购买失败,将调用以下方法: ```dart showLineNumbers title="Flutter" void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { } ```
事件示例(点击展开) ```dart void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { // error — AdaptyError: error.code; // AdaptyErrorCode.receiveRestoredTransactionsFailed (1011) error.message; // 'Error occurred in the process of restoring purchases.' error.detail; // platform-specific underlying error, or null } ```
### 数据获取与渲染 \{#data-fetching-and-rendering\} #### 产品加载错误 \{#product-loading-errors\} 如果你在初始化时未传入产品数组,AdaptyUI 会自行从服务器获取所需对象。若此操作失败,AdaptyUI 将通过调用以下方法报告错误: ```dart showLineNumbers title="Flutter" void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { } ```
事件示例(点击展开) ```dart void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { // error — AdaptyError: error.code; // AdaptyErrorCode.productRequestFailed (1002) error.message; // 'Unable to fetch available In-App Purchase products at the moment.' error.detail; // platform-specific underlying error, or null } ```
#### 渲染错误 \{#rendering-errors\} 如果在界面渲染过程中发生错误,系统会通过调用此方法来上报该错误。默认情况下(自 v3.15.2 起),当渲染错误发生时,付费墙会自动关闭,但你可以根据需要覆盖此行为。 ```dart showLineNumbers title="Flutter" void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { // Default behavior: view.dismiss() // Override with custom logic if needed, for example: // - Log the error // - Show an error message to the user } ```
事件示例(点击展开) ```dart void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { // error — AdaptyError: error.code; // AdaptyErrorCode.jsException (4105) error.message; // 'An exception was thrown from JS during AdaptyUI flow execution.' error.detail; // platform-specific underlying error, or null // Default behavior: view.dismiss() } ```
正常情况下不应出现此类错误,如果遇到,请告知我们。
--- # File: flutter-use-fallback-paywalls --- --- title: "Flutter - 使用备用付费墙" description: "处理用户离线或 Adapty 服务器不可用的情况" --- :::warning 备用付费墙需要 Flutter SDK v2.11 或更高版本。 ::: 为了保持流畅的用户体验,请务必为您的流程、[付费墙](paywalls)和[用户引导](onboardings)设置[备用方案](/fallback-paywalls)。这一预防措施可以在网络部分或完全中断时,确保应用仍能正常运行。 * **若应用无法访问 Adapty 服务器:** 应用可以显示备用流程或付费墙,并读取本地的用户引导配置。 * **若应用无法访问互联网:** 应用可以显示备用流程或付费墙。用户引导包含远程内容,需要联网才能正常使用。 :::important 在按照本指南操作之前,请先从 Adapty [下载](/local-fallback-paywalls)备用配置文件。 ::: ## 配置 \{#configuration\} 1. 将备用配置文件添加到项目根目录下应用的 `assets` 目录中。 2. 在获取目标付费墙或用户引导**之前**调用 `.setFallback` 方法。 ```dart showLineNumbers title="Flutter" final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; try { await Adapty().setFallback(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 参数: | 参数 | 描述 | | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **assetId** | 备用配置文件的路径。 | :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 ::: --- # File: flutter-localizations-and-locale-codes --- --- title: "在 Flutter SDK 中使用本地化和语言区域代码" description: "管理应用本地化和语言区域代码,触达全球用户。" --- ## 为什么这很重要 \{#why-this-is-important\} 当 Adapty 为流程选择本地化语言,以及你读取自定义付费墙的远程配置时,语言区域代码就会发挥作用。 语言区域代码较为复杂,在不同平台之间可能存在差异,因此 Adapty 在其支持的所有平台上统一采用一套内部标准。了解这一标准,有助于你预测用户最终会收到哪个本地化版本。 ## Adapty 的语言代码标准 \{#locale-code-standard-at-adapty\} Adapty 的语言代码采用略经修改的 [BCP 47 标准](https://en.wikipedia.org/wiki/IETF_language_tag):每个代码由小写子标签组成,各子标签之间用连字符分隔。示例:`en`(英语)、`pt-br`(葡萄牙语(巴西))、`zh`(简体中文)、`zh-hant`(繁体中文)。 ## 语言区域代码匹配 \{#locale-code-matching\} 当 Adapty 查找与用户语言区域匹配的本地化内容时,会按以下步骤进行: 1. 将语言区域字符串转换为小写,并将所有下划线(`_`)替换为连字符(`-`) 2. Adapty 查找与完整语言区域代码完全匹配的本地化内容 3. 如果未找到匹配项,Adapty 取第一个连字符之前的子字符串(例如 `pt-br` 对应 `pt`),并查找匹配的本地化内容 4. 如果仍未找到匹配项,Adapty 返回默认的 `en` 本地化内容 因此,`'pt_BR'`、`pt-BR` 和 `pt-br` 最终都会解析为同一个本地化内容。 ## 实现本地化 \{#implementing-localizations\} 在 SDK v4 中,获取流程时无需传入语言区域代码。 - **付费墙编辑工具与流程构建器付费墙**:Adapty 会根据设备设置及你在构建器中配置的本地化内容自动解析语言区域。使用 `createFlowView` 渲染流程,无需传入语言区域代码。 - **自定义(远程配置)付费墙**:`getFlow` 会在 `flow.remoteConfigs` 中返回所有已配置的本地化内容。每个条目都包含一个 `locale` 代码及配置内容(`data` 字符串或已解析的 `dictionary`)。请自行实现回退逻辑,选择与用户匹配的条目: ```dart showLineNumbers final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); final config = flow.remoteConfigs.firstWhereOrNull((c) => c.locale == 'en') ?? flow.remoteConfig; // the first remote config, if present // read your values from config?.dictionary ``` 上述语言区域代码匹配规则描述了 Adapty 如何对每个远程配置中存储的 `locale` 代码进行规范化处理。 ## 为什么这很重要 \{#why-this-is-important\} 有几种情况下需要用到语言区域代码——例如,当你尝试为当前应用的本地化版本获取正确的付费墙时。 由于语言区域代码比较复杂,且在不同平台之间可能存在差异,我们为所有支持的平台制定了一套内部标准。正因为这些代码较为复杂,理解你究竟向我们的服务器发送了什么内容以获取正确的本地化版本,以及后续会发生什么,就变得尤为重要——这样你才能始终获得预期的结果。 ## Adapty 语言代码标准 \{#locale-code-standard-at-adapty\} 在语言代码方面,Adapty 采用略经修改的 [BCP 47 标准](https://en.wikipedia.org/wiki/IETF_language_tag):每个代码由小写子标签组成,以连字符分隔。例如:`en`(英语)、`pt-br`(葡萄牙语(巴西))、`zh`(简体中文)、`zh-hant`(繁体中文)。 ## 语言区域代码匹配 \{#locale-code-matching\} 当 Adapty 收到客户端 SDK 发来的语言区域代码请求并开始查找对应的付费墙本地化版本时,流程如下: 1. 将传入的语言区域字符串转换为小写,并将所有下划线(`_`)替换为连字符(`-`) 2. 查找与完整语言区域代码完全匹配的本地化版本 3. 如果未找到匹配项,则截取第一个连字符之前的子字符串(例如 `pt-br` 取 `pt`),再次查找匹配的本地化版本 4. 如果仍未找到匹配项,则返回默认的 `en` 本地化版本 这样,发送 `'pt_BR'` 的 iOS 设备、发送 `pt-BR` 的 Android 设备,以及发送 `pt-br` 的其他设备,都会得到相同的结果。 ## 实现本地化:推荐方式 \{#implementing-localizations-recommended-way\} 如果你正在考虑本地化问题,很可能已经在项目中使用了本地化字符串文件。在这种情况下,我们建议在每个本地化文件中添加一个键值对,用于存储对应语言的 Adapty 语言代码。然后在调用 SDK 时提取该键的值,示例如下: ```dart showLineNumbers // 1. Modify your app_en.arb, app_es.arb, app_pt_br.arb files /* app_en.arb */ "adapty_paywalls_locale": "en", /* app_es.arb */ "adapty_paywalls_locale": "es", /* app_pt_br.arb */ "adapty_paywalls_locale": "pt-br", // 2. Extract and use the locale code final locale = AppLocalizations.of(context)!.adapty_paywalls_locale; // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` 这样,你就能完全掌控每位用户获取到的本地化内容。 ## 实现本地化的另一种方式 \{#implementing-localizations-the-other-way\} 你也可以不为每个本地化显式定义语言区域代码,同样能得到类似(但不完全相同)的结果。这种方式是从平台提供的其他对象中提取语言区域代码,如下所示: ```dart showLineNumbers final locale = Localizations.localeOf(context).languageCode; // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` 不过,出于以下几点原因,我们不推荐这种方式: 1. 在 iOS 上,首选语言和当前语言区域并不相同。如果希望正确选取本地化内容,你需要依赖 Apple 的内置逻辑(使用推荐的本地化字符串文件方案时开箱即用),或者自行重新实现该逻辑。 2. 很难预测 Adapty 服务器实际收到的内容。例如,在 iOS 上,设备可能会返回类似 `ar_OM@numbers='latn'` 这样的语言区域标识,并将其发送至我们的服务器。对于这个请求,你得到的不会是预期的 `ar-om` 本地化内容,而是 `ar`,这很可能不符合预期。 Should you decide to use this approach anyway — make sure you've covered all the relevant use cases. --- # File: flutter-web-paywall --- --- title: "在 Flutter SDK 中实现 web 付费墙" description: "设置 web 付费墙,无需支付 App Store 费用和审核即可获得收入。" --- :::important 在开始之前,请确保您已[在看板中配置了 web 付费墙](web-paywall),并安装了 Adapty SDK 3.6.1 或更高版本。 ::: 如果您使用的是自行开发的付费墙,则需要使用 SDK 方法处理 web 付费墙。`.openWebPaywall` 方法: 1. 生成唯一 URL,使 Adapty 能够将向特定用户展示的付费墙与他们被重定向到的网页关联起来。 2. 跟踪用户何时返回应用,然后以短时间间隔请求 `.getProfile`,以确定用户画像的访问权限是否已更新。 这样,如果付款成功且访问权限已更新,订阅几乎会立即在应用中激活。 ```dart showLineNumbers title="Flutter" try { await Adapty().openWebPaywall(product: ); // The web paywall will be opened } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle other errors } ``` :::note `openWebPaywall` 方法有两个版本: 1. `openWebPaywall(product)` 根据付费墙生成 URL,并将产品数据附加到 URL 中。 2. `openWebPaywall(paywall)` 根据付费墙生成 URL,但不附加产品数据。当 Adapty 付费墙中的产品与网页付费墙中的产品不同时,请使用此版本。 在 SDK v4 中,`paywall` 参数接受 `AdaptyFlowPaywall`——即获取到的 flow 的付费墙变体。在对其进行索引之前,请先检查 `flow.paywalls` 不为空,例如 `flow.paywalls[0]`。 ::: #### 处理错误 \{#handle-errors\} | 错误 | 描述 | 建议操作 | |-----------------------------------------|-----------------------------------|---------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | 付费墙未配置 web 购买 URL | 检查付费墙是否已在 Adapty 看板中正确配置 | | AdaptyError.productWithoutPurchaseUrl | 产品没有 web 购买 URL | 在 Adapty 看板中验证产品配置 | | AdaptyError.failedOpeningWebPaywallUrl | 无法在浏览器中打开 URL | 检查设备设置或提供其他购买方式 | | AdaptyError.failedDecodingWebPaywallUrl | 无法正确编码 URL 中的参数 | 验证 URL 参数是否有效且格式正确 | ## 在应用内浏览器中打开网页付费墙 \{#open-web-paywalls-in-an-in-app-browser\} :::important 从 Adapty SDK v3.15 开始支持在应用内浏览器中打开网页付费墙。 ::: 默认情况下,网页付费墙会在外部浏览器中打开。 为了提供流畅的用户体验,你可以在应用内浏览器中打开网页付费墙。这样,网页购买页面会直接显示在你的应用内,用户无需切换应用即可完成交易。 要启用此功能,请将 `in` 参数设置为 `.inAppBrowser`: ```dart showLineNumbers try { await Adapty().openWebPaywall( product: , openIn: AdaptyWebPresentation.inAppBrowser, ); // The web paywall will be opened in the in-app browser } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle other errors } ``` --- # File: flutter-troubleshoot-paywall-builder --- --- title: "在 Flutter SDK 中排查付费墙编辑工具问题" description: "在 Flutter SDK 中排查付费墙编辑工具问题" --- 本指南帮助您解决在 Flutter SDK 中使用 Adapty 付费墙编辑工具设计付费墙时遇到的常见问题。 ## 获取付费墙配置失败 \{#getting-a-paywall-configuration-fails\} **问题**:`createPaywallView` 方法无法获取付费墙配置。 **原因**:该付费墙未在付费墙编辑工具中启用设备展示。 **解决方案**:在付费墙编辑工具中启用 **Show on device** 开关。 ## 付费墙浏览次数过大 \{#the-paywall-view-number-is-too-big\} **问题**:付费墙浏览次数显示为预期值的两倍。 **原因**:你可能在代码中调用了 `logShowFlow`(Flutter SDK v4+)/ `logShowPaywall`,如果你正在使用付费墙编辑工具或 Flow Builder,这会导致浏览次数重复计算。对于使用这些工具构建的流程和付费墙,分析数据会自动追踪,无需手动调用此方法。 **解决方案**:如果你正在使用付费墙编辑工具或 Flow Builder,请确保代码中没有调用 `logShowFlow`(Flutter SDK v4+)/ `logShowPaywall`。 ## 其他问题 \{#other-issues\} **问题**:您遇到了上述未涵盖的其他付费墙编辑工具相关问题。 **解决方案**:如有需要,请参考[迁移指南](flutter-sdk-migration-guides)将 SDK 升级至最新版本。许多问题已在较新的 SDK 版本中得到修复。 --- # File: flutter-present-flows-in-observer-mode --- --- title: "在 Flutter SDK 的 Observer 模式下展示流程" description: "在 Flutter 应用中以 Observer 模式展示流程和付费墙编辑工具付费墙,同时使用自己的代码处理购买。" --- 如果你使用编辑工具自定义了流程或付费墙,就无需在移动应用代码中手动处理其渲染逻辑来向用户展示它。这类流程或付费墙本身已包含展示内容和展示方式的完整定义。 :::warning 本节仅适用于[观察者模式](observer-vs-full-mode)。如果你不在观察者模式下工作,请参阅[展示流程与付费墙](flutter-present-paywalls)主题。 ::: :::info 此功能需要 Adapty Flutter SDK 4.0 或更高版本——此前仅在 iOS 和 Android 原生 SDK 中提供。请参阅[迁移指南](migration-to-flutter-sdk-v4)进行升级。 :::
开始展示流程前(点击展开) 1. 完成 Adapty 与 [App Store](initial_ios) 以及[与 Google Play](initial-android) 的初始集成。 2. 安装并配置 Adapty SDK,务必将 `observerMode` 参数设置为 `true`。请参阅 [Flutter SDK 安装指南](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk)。 3. 在 Adapty 看板中[创建产品](create-product)。 4. [在编辑工具中配置流程或付费墙](create-paywall),并为其分配产品。 5. [创建版位并将流程或付费墙分配给对应版位](create-placement)。 6. 在移动端应用代码中[获取流程及其配置](flutter-get-pb-paywalls)。
在观察者模式下,SDK 不会代您发起购买。当用户点击 Adapty 渲染的流程或付费墙中的购买或恢复按钮时,SDK 会调用您的 `AdaptyUIObserverModeResolver`——请在其中用您自己的代码执行购买或恢复操作。 1. 实现 `AdaptyUIObserverModeResolver`: ```dart showLineNumbers title="Flutter" class MyObserverModeResolver extends AdaptyUIObserverModeResolver { @override void observerModeDidInitiatePurchase( AdaptyUIFlowView view, AdaptyPaywallProduct product, void Function() onStartPurchase, void Function() onFinishPurchase, ) { onStartPurchase(); // the view shows its loading indicator // make the purchase with your own code, then: onFinishPurchase(); // the view hides the loading indicator } @override void observerModeDidInitiateRestore( AdaptyUIFlowView view, void Function() onStartRestore, void Function() onFinishRestore, ) { onStartRestore(); // restore purchases with your own code, then: onFinishRestore(); } } ``` `observerModeDidInitiatePurchase` 方法会通知你用户已发起购买,`observerModeDidInitiateRestore` 则通知你用户已发起恢复购买。收到通知后,请触发你自定义的购买或恢复流程。 另外,请记得调用以下回调方法,将购买或恢复流程的进展通知给 AdaptyUI。这对于正确的流程行为(例如显示加载动画等)是必要的: | 回调函数 | 描述 | | :----------------- | :------------------------------------------------------------------------------- | | onStartPurchase() | 应调用此回调函数,通知 AdaptyUI 购买已开始。 | | onFinishPurchase() | 应调用此回调函数,通知 AdaptyUI 购买已完成。 | | onStartRestore() | 应调用此回调函数,通知 AdaptyUI 恢复购买已开始。 | | onFinishRestore() | 应调用此回调函数,通知 AdaptyUI 恢复购买已完成。 | 2. 在展示任何界面之前注册解析器: ```dart showLineNumbers title="Flutter" AdaptyUI().setObserverModeResolver(MyObserverModeResolver()); ``` 3. 按常规方式创建并展示流程视图:[获取流程并创建其视图](flutter-get-pb-paywalls),然后[展示它](flutter-present-paywalls)。无需额外参数——一旦注册了解析器,所有由 Adapty 渲染的流程或付费墙都会通过它来处理购买和恢复操作。 :::warning 别忘了[上报交易并将其关联到付费墙](report-transactions-observer-mode-flutter)。否则,Adapty 将无法识别该交易,也无法确定购买来源的付费墙。 ::: --- # File: flutter-quickstart-manual --- --- title: "在 Flutter SDK 的自定义付费墙中启用购买功能" description: "将 Adapty SDK 集成到您的自定义 Flutter 付费墙中,以启用应用内购买。" --- 本指南介绍如何将 Adapty 集成到自定义付费墙中。您可以完全掌控付费墙的实现方式,同时由 Adapty SDK 负责获取产品、处理新购买以及恢复历史购买记录。本指南使用 Adapty Flutter SDK v4 API——如果您使用的是 v3,请参阅[迁移指南](migration-to-flutter-sdk-v4)了解对应的方法名称。 :::important **本指南适用于需要实现自定义付费墙的开发者。** 如果你想以最简便的方式开启购买功能,请使用 [Adapty 付费墙编辑工具](flutter-quickstart-paywalls)。借助付费墙编辑工具,你可以在无代码可视化编辑器中创建付费墙,Adapty 自动处理所有购买逻辑,无需重新发布应用即可测试不同设计方案。 ::: ## 开始之前 \{#before-you-start\} ### 设置产品 \{#set-up-products\} 要启用应用内购买,你需要了解以下三个核心概念: - [**产品**](product) – 用户可以购买的一切内容(订阅、消耗型商品、永久授权) - [**付费墙**](paywalls) – 定义向用户展示哪些产品的配置。在 Adapty 中,付费墙是获取产品的唯一途径,但这种设计让你无需修改应用代码即可调整产品、价格和优惠。在 SDK v4 中,版位的付费墙变体由 **flow** 对象承载——你获取一个 flow,然后查询其中的产品。 - [**版位**](placements) – 应用中展示付费墙的位置和时机(例如 `main`、`onboarding`、`settings`)。你在看板中为版位配置付费墙,然后在代码中通过版位 ID 请求对应内容。这样就能轻松运行 A/B 测试,并向不同用户展示不同的付费墙。 确保你理解这些概念,即使你使用的是自定义付费墙。简单来说,这些不过是你管理应用内销售产品的方式。 要实现自定义付费墙,你需要创建一个**付费墙**并将其添加到一个**版位**中。这样才能获取你的产品。如需了解在看板中具体需要做什么,请参考[这里](quickstart)的快速入门指南。 ### 管理用户 \{#manage-users\} 您可以选择是否使用后端身份验证。 但是,Adapty SDK 对匿名用户和已识别用户的处理方式有所不同。请阅读[身份识别快速入门指南](flutter-quickstart-identify)以了解具体细节,并确保您正确处理用户信息。 ## 步骤 1:获取产品 \{#step-1-get-products\} 要为自定义付费墙获取产品,你需要: 1. 通过将[版位](placements) ID 传入 `getFlow` 方法来获取 `flow` 对象。 2. 使用 `getPaywallProducts` 方法获取该流程的产品数组。 ```dart showLineNumbers Future loadPaywall() async { try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); final products = await Adapty().getPaywallProducts(flow: flow); // Use products to build your custom paywall UI } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## 第二步:处理购买 \{#step-2-accept-purchases\} 当用户在自定义付费墙中点击某个产品时,调用 `makePurchase` 方法并传入所选产品。该方法会处理购买流程并返回更新后的用户画像。 ```dart showLineNumbers Future purchaseProduct(AdaptyPaywallProduct product) async { try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // Purchase successful, profile updated break; case AdaptyPurchaseResultUserCancelled(): // User canceled the purchase break; case AdaptyPurchaseResultPending(): // Purchase is pending (e.g., user will pay offline with cash) break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## 步骤 3:恢复购买 \{#step-3-restore-purchases\} 应用商店要求所有包含订阅的应用为用户提供恢复购买的途径。 当用户点击恢复按钮时,调用 `restorePurchases` 方法。这将把用户的购买历史与 Adapty 同步,并返回更新后的用户画像。 ```dart showLineNumbers Future restorePurchases() async { try { final profile = await Adapty().restorePurchases(); // Restore successful, profile updated } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## 第四步:检查订阅状态 \{#step-4-check-the-subscription-status\} 购买或恢复购买后,检查用户的[访问等级](access-level),以决定是否显示付费墙或解锁付费功能。`makePurchase` 和 `restorePurchases` 方法已经返回更新后的用户画像;如果在应用的其他地方需要获取当前状态,请使用 `getProfile` 方法: ```dart showLineNumbers Future hasPremiumAccess() async { try { final profile = await Adapty().getProfile(); return profile.accessLevels['premium']?.isActive ?? false; } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } return false; } ``` 有关检查和监控订阅状态的更多方法(包括监听实时更新),请参阅[检查订阅状态](flutter-check-subscription-status)。 ## 下一步 \{#next-steps\} :::tip 有疑问或遇到问题?欢迎访问我们的[支持论坛](https://adapty.featurebase.app/),在那里你可以找到常见问题的解答,也可以提出自己的问题。我们的团队和社区随时为你提供帮助! ::: 您的付费墙已准备好在应用中展示。在 [App Store 沙盒](test-purchases-in-sandbox) 或 [Google Play Store](testing-on-android) 中测试您的购买流程,确保能够从付费墙完成测试购买。如需了解生产环境中的完整实现示例,请参考我们示例应用中的 [PurchasesObserver](https://github.com/adaptyteam/AdaptySDK-Flutter/blob/master/example/lib/purchase_observer.dart),其中演示了包含完善的错误处理、UI 观察者及全面 SDK 集成的购买处理逻辑。 --- # File: fetch-paywalls-and-products-flutter --- --- title: "在 Flutter SDK 中获取远程配置付费墙的付费墙和产品" description: "在 Adapty Flutter SDK 中获取付费墙和产品,提升用户变现效果。" --- 在展示远程配置和自定义付费墙之前,你需要先获取相关信息。请注意,本主题涉及远程配置和自定义付费墙。有关获取流程和付费墙编辑工具自定义付费墙的指导,请参阅[获取流程与付费墙](flutter-get-pb-paywalls)。 :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 :::
在你的移动应用中开始获取付费墙和产品之前(点击展开) 1. 在 Adapty 看板中[创建你的产品](create-product)。 2. 在 Adapty 看板中[创建付费墙并将产品添加到付费墙](create-paywall)。 3. 在 Adapty 看板中[创建版位并将付费墙添加到版位](create-placement)。 4. 在您的移动应用中[安装 Adapty SDK](sdk-installation-flutter)。
## 获取流程信息 \{#fetch-flow-information\} 在 Adapty 中,[产品](product)是 App Store 和 Google Play 产品的统一组合。这些跨平台产品被整合到付费墙中,让你能够在移动应用的特定版位中展示它们。 要展示产品,你需要通过 `getFlow` 方法从某个[版位](placements)获取 `AdaptyFlow`。 :::important **不要硬编码产品 ID。** 唯一需要硬编码的是版位 ID。付费墙是远程配置的,因此产品数量和可用优惠随时可能发生变化。你的应用必须动态处理这些变化——如果一个付费墙今天返回两个产品,明天返回三个,则无需修改代码即可全部展示。 ::: ```dart showLineNumbers try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` | 参数 | 是否必填 | 描述 | |---------|--------|-----------| | **placementId** | 必填 | [版位](placements)的标识符。这是您在 Adapty 看板中创建版位时指定的值。 | | **fetchPolicy** | 默认值:`.reloadRevalidatingCacheData` |

默认情况下,SDK 会尝试从服务器加载数据,若加载失败则返回缓存数据。我们推荐使用此方式,因为它能确保用户始终获取最新数据。

不过,如果您认为用户的网络环境不稳定,可以考虑使用 `.returnCacheDataElseLoad`,在缓存数据存在时直接返回缓存。这种情况下,用户获取的数据可能不是最新的,但无论网络状况如何,加载速度都会更快。缓存会定期更新,因此在会话期间使用缓存来减少网络请求是安全的。

请注意,缓存在应用重启后仍会保留,只有在重新安装应用或手动清理时才会被清除。

Adapty SDK 通过两层机制存储付费墙:上述定期更新的缓存,以及[备用付费墙](flutter-use-fallback-paywalls)。我们还使用 CDN 来加快付费墙的获取速度,并在 CDN 不可用时启用独立的备用服务器。整套系统旨在确保您始终能获取最新版本的付费墙,同时在网络连接不稳定的情况下也能保持可靠性。

| | **loadTimeout** | 默认值:5 秒 |

该值限制此方法的超时时间。若超时,将返回缓存数据或本地备用数据。

请注意,在极少数情况下,此方法的实际超时时间可能略晚于 `loadTimeout` 中指定的时间,因为该操作在底层可能由多个不同请求组成。

| :::note 在 v4 中,`getFlow` 不接受 `locale` 参数。对于自定义付费墙,所有可用的本地化内容都会通过流程的远程配置(`flow.remoteConfigs`)返回——从中选取与用户设备或应用设置匹配的那个。详见[本地化与语言代码](flutter-localizations-and-locale-codes)。 ::: 返回参数: | 参数 | 描述 | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flow | 一个 `AdaptyFlow` 对象,包含流程的标识符(`instanceIdentity`、`variationId`)、名称、版位、付费墙实验变体(`paywalls`)以及远程配置(`remoteConfigs`)。 | ## 获取产品 \{#fetch-products\} 获取到 flow 后,你可以查询与其对应的产品数组: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(flow: flow); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` 响应参数: | 参数 | 描述 | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) 对象列表,包含:产品标识符、产品名称、价格、货币、订阅时长及其他几个属性。 | 在实现自定义付费墙设计时,您可能需要访问 [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) 对象中的这些属性。下面列出了最常用的属性,有关所有可用属性的完整详情,请参阅链接文档。 | 属性 | 描述 | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title(标题)** | 要显示产品标题,请使用 `product.localizedTitle`。请注意,本地化基于用户选择的商店国家/地区,而非设备本身的语言区域设置。 | | **Price(价格)** | 要显示本地化版本的价格,请使用 `product.price.localizedString`。该本地化基于设备的语言区域信息。您也可以使用 `product.price.amount` 以数字形式访问价格,该值以本地货币提供。要获取关联的货币符号,请使用 `product.price.currencySymbol`。 | | **Subscription Period(订阅周期)** | 要显示订阅周期(例如周、月、年等),请使用 `product.subscription?.localizedPeriod`。该本地化基于设备的语言区域设置。要以编程方式获取订阅周期,请使用 `product.subscription?.period`。从中可以访问 `unit` 枚举以获取时长(即天、周、月、年或未知)。`numberOfUnits` 值将获取周期单位的数量。例如,对于季度订阅,`unit` 属性中将显示 `AdaptyPeriodUnit.month`,`numberOfUnits` 属性中将显示 `3`。 | | **Introductory Offer(新用户优惠)** | 要显示标记或其他指示符以表明订阅包含新用户优惠,请查看 `product.subscription?.offer?.phases` 属性。这是一个最多可包含两个折扣阶段的列表:免费试用阶段和优惠价格阶段。每个阶段对象包含以下有用属性:
• `paymentMode`:枚举值为 `AdaptyPaymentMode.freeTrial`、`AdaptyPaymentMode.payAsYouGo`、`AdaptyPaymentMode.payUpFront` 和 `AdaptyPaymentMode.unknown`。免费试用为 `AdaptyPaymentMode.freeTrial` 类型。
• `price`:折扣价格(数字形式)。免费试用时此处为 `0`。
• `localizedNumberOfPeriods`:使用设备语言区域本地化的字符串,描述优惠时长。例如,三天试用优惠在此字段中显示为 `3 days`。
• `subscriptionPeriod`:或者,您可以使用此属性获取优惠周期的各项详情。其工作方式与上一节描述的订阅周期相同。
• `localizedSubscriptionPeriod`:针对用户语言区域格式化的折扣订阅周期。 | ## 使用默认目标受众流程加快流程获取速度 \{#speed-up-flow-fetching-with-default-audience-flow\} 通常情况下,流程获取几乎是即时完成的,无需担心速度问题。但如果你的版位和目标受众数量较多,且用户网络连接较差,流程获取可能会比预期慢。在这种情况下,你可能希望展示一个默认流程,以确保用户体验流畅,而不是让用户看到空白页面。 要解决这个问题,你可以使用 `getFlowForDefaultAudience` 方法,该方法会获取指定版位中**所有用户**目标受众的流程。但请务必了解,推荐的做法是通过 `getFlow` 方法来获取流程,详情请参阅上方的[获取流程信息](fetch-paywalls-and-products-flutter#fetch-flow-information)部分。 :::warning 为什么我们推荐使用 `getFlow` `getFlowForDefaultAudience` 方法存在以下几个明显的缺陷: - **潜在的向后兼容性问题**:如果你需要针对不同的应用版本(当前版本和未来版本)展示不同的付费墙,可能会遇到挑战。你要么设计出同时兼容当前(旧版)版本的付费墙,要么接受使用当前(旧版)版本的用户可能遇到付费墙无法渲染的问题。 - **失去精准定向能力**:所有用户都将看到为 **All Users** 目标受众设计的同一个付费墙,这意味着你将失去个性化定向能力(包括基于国家/地区、营销归因或自定义属性的定向)。 如果您愿意接受这些缺点以换取更快的流程获取速度,请按以下方式使用 `getFlowForDefaultAudience` 方法。否则,请继续使用[上文](fetch-paywalls-and-products-flutter#fetch-flow-information)介绍的 `getFlow`。 ::: ```dart showLineNumbers try { final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` | 参数 | 是否必填 | 描述 | |---------|--------|-----------| | **placementId** | 必填 | [版位](placements)的标识符。这是您在 Adapty 看板中创建版位时所指定的值。 | | **fetchPolicy** | 默认值:`.reloadRevalidatingCacheData` |

默认情况下,SDK 会尝试从服务器加载数据,若失败则返回缓存数据。我们推荐使用此方式,因为它能确保用户始终获取最新数据。

但如果您认为用户的网络环境不稳定,可以考虑使用 `.returnCacheDataElseLoad`——在缓存存在时优先返回缓存数据。这样用户获取的数据可能不是最新的,但无论网络状况如何,加载速度都会更快。缓存会定期更新,因此在会话期间使用缓存以避免网络请求是安全的。

请注意,重启应用后缓存依然保留,只有在重新安装应用或手动清除时才会被清空。

|
在展示远程配置和自定义付费墙之前,你需要先获取相关信息。请注意,本主题涉及远程配置和自定义付费墙。有关获取付费墙编辑工具自定义付费墙的指南,请参阅[获取付费墙编辑工具付费墙及其配置](flutter-get-pb-paywalls)。 :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 :::
在移动应用中开始获取付费墙和产品之前(点击展开) 1. 在 Adapty 看板中[创建你的产品](create-product)。 2. 在 Adapty 看板中[创建付费墙并将产品添加到付费墙](create-paywall)。 3. 在 Adapty 看板中[创建版位并将付费墙添加到版位](create-placement)。 4. 在您的移动应用中[安装 Adapty SDK](sdk-installation-flutter)。
## 获取付费墙信息 \{#fetch-paywall-information\} 在 Adapty 中,[产品](product)是来自 App Store 和 Google Play 的产品组合。这些跨平台产品被整合到付费墙中,使您能够在特定的移动应用版位中展示它们。 要显示产品,您需要使用 `getPaywall` 方法从某个[版位](placements)获取[付费墙](paywalls)。 :::important **不要硬编码产品 ID。** 您唯一需要硬编码的 ID 是版位 ID。付费墙是远程配置的,因此产品数量和可用优惠随时可能发生变化。您的应用必须动态处理这些变化——如果付费墙今天返回两个产品,明天返回三个,则无需修改代码即可显示所有产品。 ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` | 参数 | 是否必填 | 描述 | |---------|--------|-----------| | **placementId** | 必填 | [版位](placements)的标识符。这是您在 Adapty 看板中创建版位时指定的值。 | | **locale** |

可选

默认值:`en`

|

[付费墙本地化](add-remote-config-locale)的标识符。该参数应为由一个或多个以减号(**-**)分隔的子标签组成的语言代码。第一个子标签表示语言,第二个表示地区。

示例:`en` 表示英语,`pt-br` 表示巴西葡萄牙语。

有关语言代码及其推荐用法的更多信息,请参阅[本地化与语言代码](flutter-localizations-and-locale-codes)。

| | **fetchPolicy** | 默认值:`.reloadRevalidatingCacheData` |

默认情况下,SDK 将尝试从服务器加载数据,如果失败则返回缓存数据。我们推荐此方式,因为它确保用户始终获得最新数据。

但是,如果您认为用户网络不稳定,可以考虑使用 `.returnCacheDataElseLoad`,在缓存存在时返回缓存数据。在这种情况下,用户可能无法获取最新数据,但无论网络状况多差,加载速度都会更快。缓存会定期更新,因此在会话期间使用缓存以避免网络请求是安全的。

请注意,重启应用后缓存保持不变,只有在卸载应用或手动清理时才会被清除。

Adapty SDK 在两层中存储付费墙:上述定期更新的缓存和[备用付费墙](flutter-use-fallback-paywalls)。我们还使用 CDN 加快付费墙获取速度,并在 CDN 不可达时使用独立的备用服务器。该系统旨在确保您始终获得最新版本的付费墙,同时在网络连接不佳的情况下也能保证可靠性。

| | **loadTimeout** | 默认值:5 秒 |

该值限制此方法的超时时间。如果达到超时,将返回缓存数据或本地备用数据。

请注意,在极少数情况下,此方法的超时时间可能略晚于 `loadTimeout` 中指定的时间,因为该操作可能在内部由不同请求组成。

| 不要硬编码产品 ID!由于付费墙是远程配置的,可用产品、产品数量以及特殊优惠(如免费试用)可能随时变化。请确保您的代码能够动态处理这些情况。 例如,如果您最初获取了 2 个产品,您的应用应显示这 2 个产品。但是,如果您后来获取了 3 个产品,您的应用应显示全部 3 个,而无需修改任何代码。唯一需要硬编码的是版位 ID。 响应参数: | 参数 | 描述 | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | 一个 [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) 对象,包含:产品 ID 列表、付费墙标识符、远程配置及其他几个属性。 | ## 获取产品 \{#fetch-products\} 获取付费墙后,您可以查询与之对应的产品数组: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(paywall: paywall); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 响应参数: | 参数 | 描述 | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) 对象列表,包含:产品标识符、产品名称、价格、货币、订阅时长及其他多个属性。 | 在实现自定义付费墙设计时,你可能需要访问 [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) 对象中的这些属性。以下列出了最常用的属性,完整属性列表请参阅上方链接文档。 | 属性 | 描述 | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | 使用 `product.localizedTitle` 显示产品名称。请注意,本地化基于用户在商店中选择的国家/地区,而非设备本身的语言环境。 | | **Price** | 使用 `product.price.localizedString` 显示本地化价格,该本地化基于设备的语言环境信息。也可以通过 `product.price.amount` 以数字形式获取价格,其值以本地货币计。如需获取对应的货币符号,请使用 `product.price.currencySymbol`。 | | **Subscription Period** | 使用 `product.subscription?.localizedPeriod` 显示订阅周期(如周、月、年等),该本地化基于设备语言环境。如需以编程方式获取订阅周期,请使用 `product.subscription?.period`,通过 `unit` 枚举可获取周期长度(即 day、week、month、year 或 unknown),`numberOfUnits` 则表示周期单位的数量。例如,对于季度订阅,`unit` 属性值为 `AdaptyPeriodUnit.month`,`numberOfUnits` 属性值为 `3`。 | | **Introductory Offer** | 如需显示徽章或其他标识以表明订阅包含新用户优惠,请查看 `product.subscription?.offer?.phases` 属性。这是一个列表,最多包含两个折扣阶段:免费试用阶段和新用户价格阶段。每个阶段对象包含以下实用属性:
• `paymentMode`:枚举类型,可选值为 `AdaptyPaymentMode.freeTrial`、`AdaptyPaymentMode.payAsYouGo`、`AdaptyPaymentMode.payUpFront` 和 `AdaptyPaymentMode.unknown`。免费试用对应 `AdaptyPaymentMode.freeTrial` 类型。
• `price`:折扣价格(数字形式)。免费试用时该值为 `0`。
• `localizedNumberOfPeriods`:根据设备语言环境本地化的字符串,描述优惠时长。例如,三天试用优惠在此字段显示为 `3 days`。
• `subscriptionPeriod`:也可通过此属性获取优惠周期的具体信息,其使用方式与上一节描述的订阅周期一致。
• `localizedSubscriptionPeriod`:按用户语言环境格式化的折扣订阅周期字符串。 | ## 使用默认目标受众付费墙加速付费墙获取 \{#speed-up-paywall-fetching-with-default-audience-paywall\} 通常情况下,付费墙几乎可以立即获取,因此您无需担心加速此过程。但是,在您拥有大量目标受众和付费墙,且用户网络连接较弱的情况下,获取付费墙可能需要比预期更长的时间。在这种情况下,您可能希望显示默认付费墙,以确保流畅的用户体验,而不是完全不显示付费墙。 为解决此问题,您可以使用 `getPaywallForDefaultAudience` 方法,该方法为 **All Users** 目标受众获取指定版位的付费墙。但是,请务必了解,推荐的方式是通过 `getPaywall` 方法获取付费墙,详见上方的[获取付费墙信息](fetch-paywalls-and-products-flutter#fetch-paywall-information)部分。 :::warning 为什么我们推荐使用 `getPaywall` `getPaywallForDefaultAudience` 方法存在一些重要缺点: - **潜在的向后兼容性问题**:如果您需要为不同的应用版本(当前版本和未来版本)显示不同的付费墙,可能会面临挑战。您要么必须设计支持当前(旧版)版本的付费墙,要么接受使用当前(旧版)版本的用户可能遇到付费墙无法渲染的问题。 - **失去精准定向**:所有用户都将看到为 **All Users** 目标受众设计的同一付费墙,这意味着您将失去个性化定向能力(包括基于国家/地区、营销归因或您自定义属性的定向)。 如果你愿意接受上述缺点以换取更快的付费墙获取速度,请按如下方式使用 `getPaywallForDefaultAudience` 方法。否则,请继续使用[上文](fetch-paywalls-and-products-flutter#fetch-paywall-information)介绍的 `getPaywall`。 ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` :::note `getPaywallForDefaultAudience` 方法从 Flutter SDK 3.2.0 版本开始支持。 ::: | 参数 | 是否必填 | 描述 | |---------|--------|-----------| | **placementId** | 必填 | [版位](placements)的标识符。这是您在 Adapty 看板中创建版位时指定的值。 | | **locale** |

可选

默认值:`en`

|

[付费墙本地化](add-remote-config-locale)的标识符。该参数应为由一个或多个以减号(**-**)分隔的子标签组成的语言代码。第一个子标签表示语言,第二个表示地区。

示例:`en` 表示英语,`pt-br` 表示巴西葡萄牙语。

有关语言代码及其推荐用法的更多信息,请参阅[本地化与语言代码](flutter-localizations-and-locale-codes)。

| | **fetchPolicy** | 默认值:`.reloadRevalidatingCacheData` |

默认情况下,SDK 将尝试从服务器加载数据,如果失败则返回缓存数据。我们推荐此方式,因为它确保用户始终获得最新数据。

但是,如果您认为用户网络不稳定,可以考虑使用 `.returnCacheDataElseLoad`,在缓存存在时返回缓存数据。在这种情况下,用户可能无法获取最新数据,但无论网络状况多差,加载速度都会更快。缓存会定期更新,因此在会话期间使用缓存以避免网络请求是安全的。

请注意,重启应用后缓存保持不变,只有在卸载应用或手动清理时才会被清除。

|
--- # File: present-remote-config-paywalls-flutter --- --- title: "在 Flutter SDK 中渲染远程配置设计的付费墙" description: "了解如何在 Adapty Flutter SDK 中展示远程配置付费墙,以个性化用户体验。" --- 如果您已使用远程配置自定义了付费墙,则需要在移动应用的代码中实现渲染逻辑,以便向用户展示该付费墙。由于远程配置提供了灵活性以满足您的需求,您可以完全掌控付费墙视图所包含的内容及其呈现方式。我们提供了一个获取远程配置的方法,让您能够自主展示通过远程配置设置的自定义付费墙。 ## 获取付费墙远程配置并展示 \{#get-paywall-remote-config-and-present-it\} 在 v4 中,流程包含一个 `remoteConfigs` 列表——每个已配置的本地化对应一条远程配置。选取与用户语言区域匹配的条目并提取所需的值。请参阅[本地化与语言区域代码](flutter-localizations-and-locale-codes),了解如何选择正确的本地化。 ```dart showLineNumbers try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); // one entry per configured localization; fall back to the first one final config = flow.remoteConfigs.firstWhereOrNull((c) => c.locale == 'en') ?? flow.remoteConfig; final String? headerText = config?.dictionary?['header_text'] as String?; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` 至此,获取所有必要数据后,就可以将其渲染并组合成一个美观的页面了。请确保设计能够适配各种手机屏幕尺寸和方向,在不同设备上提供流畅且友好的用户体验。 :::warning 请务必按照以下说明记录付费墙查看事件,以便 Adapty 分析系统能够收集漏斗和 A/B 测试所需的数据。 ::: 展示付费墙后,请继续设置购买流程。当用户发起购买时,只需使用付费墙中的产品调用 `.makePurchase()`。有关 `.makePurchase()` 方法的详细信息,请参阅[发起购买](flutter-making-purchases)。 我们建议[创建一个备用付费墙(即备用付费墙)](flutter-use-fallback-paywalls)。当用户没有网络连接或缓存不可用时,该备用付费墙将自动展示,确保在上述情况下依然能为用户提供流畅的体验。 ## 追踪付费墙展示事件 \{#track-paywall-view-events\} Adapty 帮助你衡量付费墙的表现。购买数据会自动收集,但付费墙的展示事件需要你手动记录,因为只有你知道用户何时看到了付费墙。 要记录付费墙展示事件,只需调用 `.logShowFlow(flow: flow)`,该事件将反映在漏斗和 A/B 测试的付费墙数据指标中。 :::important 如果你展示的是通过[编辑工具](adapty-paywall-builder)创建的流程或付费墙,则无需调用 `.logShowFlow(flow: flow)`。 ::: ```dart showLineNumbers try { await Adapty().logShowFlow(flow: flow); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` 请求参数: | 参数 | 是否必填 | 描述 | | :---------- | :------- |:----------------------------------------------------------------------| | **flow** | 必填 | 一个 `AdaptyFlow` 对象。 | 如果你通过远程配置自定义了付费墙,则需要在移动应用代码中自行实现渲染逻辑,才能将其展示给用户。由于远程配置的灵活性完全由你掌控,付费墙视图的内容和样式也由你决定。我们提供了一个获取远程配置的方法,让你能够自主展示通过远程配置搭建的自定义付费墙。 ## 获取付费墙远程配置并展示它 \{#get-paywall-remote-config-and-present-it\} 要获取付费墙的远程配置,请访问 `remoteConfig` 属性并提取所需的值。 ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID"); final String? headerText = paywall.remoteConfig?.dictionary?['header_text'] as String?; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 至此,一旦获取到所有必要的值,就可以将它们渲染并组合成视觉效果出色的页面了。请确保设计能够适配各种手机屏幕尺寸和方向,在不同设备上提供流畅、易用的体验。 :::warning 请务必按照以下说明记录付费墙查看事件,以便 Adapty 分析系统能够采集漏斗和 A/B 测试所需的数据。 ::: 展示付费墙之后,接下来需要配置购买流程。当用户发起购买时,直接调用 `.makePurchase()` 并传入付费墙中的产品即可。有关 `.makePurchase()` 方法的详细说明,请参阅[发起购买](flutter-making-purchases)。 建议[创建备用付费墙(即备用付费墙)](flutter-use-fallback-paywalls)。当用户没有网络连接或无可用缓存时,将展示该备用付费墙,从而保证在这些情况下依然有流畅的体验。 ## 追踪付费墙展示事件 \{#track-paywall-view-events\} Adapty 可帮助您衡量付费墙的表现。虽然我们会自动收集购买数据,但付费墙展示事件需要您手动记录,因为只有您才知道用户何时看到了付费墙。 要记录付费墙展示事件,只需调用 `.logShowPaywall(paywall)`,该事件将反映在漏斗分析和 A/B 测试的付费墙数据图表中。 :::important 如果您展示的是通过[付费墙编辑工具](adapty-paywall-builder)创建的付费墙,则无需调用 `.logShowPaywall(paywall)`。 ::: ```dart showLineNumbers try { final result = await Adapty().logShowPaywall(paywall: paywall); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 请求参数: | 参数 | 是否必填 | 描述 | | :---------- | :------- |:----------------------------------------------------------------------| | **paywall** | 必填 | 一个 [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) 对象。 | --- # File: flutter-making-purchases --- --- title: "在 Flutter SDK 中进行应用内购买" description: "使用 Adapty 处理应用内购买和订阅的指南。" --- 在移动应用中展示付费墙,是为用户提供高级内容或服务访问权限的关键步骤。不过,如果你使用[付费墙编辑工具](adapty-paywall-builder)来自定义付费墙,仅展示付费墙本身就足以支持购买流程。 如果您不使用付费墙编辑工具,则必须使用名为 `.makePurchase()` 的独立方法来完成购买并解锁所需内容。该方法是用户与付费墙交互并完成所需交易的入口。 如果您的付费墙为用户正在购买的产品设置了有效的促销活动,Adapty 将在购买时自动应用该优惠。 :::warning 请注意,新用户优惠仅在您使用付费墙编辑工具配置的付费墙时才会自动应用。 在其他情况下,您需要[在 iOS 上验证用户的新用户优惠资格](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios)。跳过此步骤可能导致您的应用在发布时被拒绝,还可能对本应享受新用户优惠的用户收取全价。 ::: 请确保您已完成[初始配置](quickstart),不要跳过任何步骤。否则,我们将无法验证购买。 ## 发起购买 \{#make-purchase\} :::note **使用[付费墙编辑工具](adapty-paywall-builder)?** 购买流程会自动处理——你可以跳过此步骤。 **需要分步指引?** 请查看[快速入门指南](flutter-implement-paywalls-manually),其中包含完整上下文的端到端实现说明。 ::: ```dart showLineNumbers try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): if (profile.accessLevels['premium']?.isActive ?? false) { // Grant access to the paid features } break; case AdaptyPurchaseResultPending(): break; case AdaptyPurchaseResultUserCancelled(): break; default: break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` 请求参数: | 参数 | 是否必填 | 描述 | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | 必填 | 从付费墙中获取的 [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) 对象。 | 响应参数: | 参数 | 描述 | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

请求成功后,响应中会包含此对象。[AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) 对象提供了用户在应用内的访问等级、订阅及一次性购买的完整信息。

请检查访问等级状态,以确认用户是否具备所需的应用访问权限。

| :::warning **注意:** 如果你使用的 Apple StoreKit 版本低于 v2.0,且 Adapty SDK 版本低于 v2.9.0,则需要提供 [Apple App Store 共享密钥](app-store-connection-configuration#step-5-enter-app-store-shared-secret)。此方法目前已被 Apple 弃用。 ::: ## 购买时更改订阅 \{#change-subscription-when-making-a-purchase\} 当用户选择新订阅而非续订当前订阅时,具体行为取决于所使用的应用商店: - 对于 App Store,订阅会在同一订阅组内自动更新。如果用户购买了某个订阅组的订阅,而此时已有另一个订阅组的有效订阅,则两个订阅将同时处于激活状态。 - 对于 Google Play,订阅不会自动更新。您需要按照以下说明在移动应用代码中手动处理切换逻辑。 在 Android 中,如需将订阅替换为另一个订阅,请在调用 `.makePurchase()` 方法时传入额外参数: ```dart showLineNumbers try { final subscriptionUpdateParams = AdaptyAndroidSubscriptionUpdateParameters( 'OLD_PRODUCT_ID', AdaptyAndroidSubscriptionUpdateReplacementMode.immediateWithTimeProration, ); final result = await Adapty().makePurchase( product: product, parameters: AdaptyPurchaseParameters( subscriptionUpdateParams: subscriptionUpdateParams, ), ); // successful cross-grade } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` 附加请求参数: | 参数 | 是否必填 | 描述 | | :--------------------------- | :------- |:--------------------------------------------------------------------------------------------------------| | **parameters** | required | 一个 `AdaptyPurchaseParameters` 对象,其 `subscriptionUpdateParams` 字段需设置为 [`AdaptyAndroidSubscriptionUpdateParameters`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyAndroidSubscriptionUpdateParameters-class.html) 对象。 | 您可以在 Google 开发者文档中了解更多关于订阅和替换模式的信息: - [关于替换模式](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Google 关于替换模式的建议](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - 替换模式 [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE())。注意:此方法仅适用于订阅升级,不支持降级。 - 替换模式 [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED())。注意:实际的订阅变更仅在当前订阅计费周期结束后才会生效。 ## 在 iOS 中兑换优惠码 \{#redeem-offer-codes-in-ios\}
关于优惠码 优惠码允许您向特定用户提供折扣或免费试用。与自动应用的常规优惠不同,优惠码通过应用外部渠道发放——例如电子邮件营销、社交媒体或印刷材料。用户可以通过在 App Store 中输入代码、访问兑换链接或在应用内对话框中进行兑换。 要设置优惠码,请在 App Store Connect 中打开某个订阅,然后进入其 **Offer Codes** 部分。您可以创建[三种类型](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes)的优惠码: - **Free** — 订阅在设定时长内免费,下次续订恢复原价。 - **Pay as you go** — 用户在设定时长内每个计费周期按折扣价付费,之后订阅恢复原价续订。 - **Pay up front** — 用户一次性按折扣价支付整个优惠期费用,之后订阅恢复原价续订。 您无需将优惠码添加到 Adapty。Apple 会在优惠期内为每笔交易打上优惠码类别标签,包括首次兑换和后续所有折扣续订。Adapty 检测到该标签后,会将每笔交易以 `offer_code` 优惠类别记录。优惠期结束、订阅以原价续订后,该标签将不再存在。您可以在 [Adapty 看板](controls-filters-grouping-compare-proceeds) 中按 **Offer Code** 优惠类型筛选分析数据。 #### 营收差异排查 如果您发现某笔优惠码交易在 Adapty 中以产品原价而非折扣价显示,请在 App Store Connect 中核实以下内容: - 优惠码已为用户可兑换的所有地区正确配置了定价。 - 已为用户所在的特定国家或地区设置了优惠价格。Apple 在交易中发送的是地区价格。如果该优惠未配置地区价格,Apple 可能会发送产品原价。 您可以在 [Adapty 看板](controls-filters-grouping-compare-proceeds) 中通过 **Offer Code** 优惠类型和 **Offer Discount Type** 筛选器来筛选和核实优惠码交易。 #### 旧版促销码(已弃用) :::warning Apple 于 2026 年 3 月弃用了应用内购买的促销码。优惠码以更强大的功能取而代之:可配置资格条件、设置到期日期,每季度最多可生成 100 万个代码。如果您之前使用促销码进行应用内购买,请在 App Store Connect 中迁移至优惠码。 ::: 旧版促销码(每个应用每个版本上限 100 个)可免费授予订阅访问权限。与优惠码不同,Apple 不会在促销码交易中包含折扣信息——它在收据中发送的是产品原价。因此,Adapty 以原价记录这些交易,导致 Adapty 分析数据与 App Store Connect 之间出现营收差异。 如果您看到历史交易以原价显示但本应免费,这些很可能来自旧版促销码。由于这些代码现已被弃用,请迁移至优惠码以确保营收数据的准确性。
在应用中展示兑换码页面: ```dart showLineNumbers try { await Adapty().presentCodeRedemptionSheet(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` :::danger 根据我们的观察,部分应用的优惠码兑换页面可能无法稳定运行。建议直接将用户跳转至 App Store。 要执行此操作,您需要打开以下格式的 URL: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ### 管理预付费方案(Android) \{#manage-prepaid-plans-android\} 如果您的应用用户可以购买[预付费方案](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans)(例如,购买数月的非续订订阅),您可以为预付费方案启用[待处理交易](https://developer.android.com/google/play/billing/subscriptions#pending)。 ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleEnablePendingPrepaidPlans(true), ); ``` --- # File: flutter-restore-purchase --- --- title: "在 Flutter SDK 的移动应用中恢复购买" description: "了解如何在 Adapty 中恢复购买,以确保无缝的用户体验。" --- 在 iOS 和 Android 上恢复购买是一项功能,允许用户重新访问之前购买的内容(例如订阅或应用内购买),而无需再次付费。此功能对于那些可能已卸载并重新安装应用,或切换到新设备并希望访问之前购买内容的用户尤为有用。 :::note 在使用[付费墙编辑工具](adapty-paywall-builder)构建的付费墙中,购买会自动恢复,无需您编写额外代码。如果您属于这种情况,可以跳过此步骤。 ::: 如果您未使用[付费墙编辑工具](adapty-paywall-builder)自定义付费墙,请调用 `.restorePurchases()` 方法来恢复购买: ```dart showLineNumbers try { final profile = await Adapty().restorePurchases(); if (profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false) { // successful access restore } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 响应参数: | 参数 | 描述 | |---------|-----------| | **Profile** |

一个 [`AdaptyProfile`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) 对象。该模型包含访问等级、订阅和非订阅购买的相关信息。

请检查**访问等级状态**以确定用户是否有权访问应用。

| :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 ::: --- # File: implement-observer-mode-flutter --- --- title: "在 Flutter SDK 中实现观察者模式" description: "在 Adapty 中实现观察者模式,以在 Flutter SDK 中跟踪用户订阅事件。" --- 如果你已有自己的购买基础设施,暂时还不打算完全切换到 Adapty,可以了解一下[观察者模式](observer-vs-full-mode)。在基础形态下,观察者模式提供了高级分析功能,并能与归因和分析系统无缝集成。 如果这满足您的需求,只需: 1. 在配置 Adapty SDK 时,将 `observerMode` 参数设置为 `true` 以启用该功能。请参阅 [Flutter](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk) 的设置说明。 2. 将现有购买基础设施中的[交易上报](report-transactions-observer-mode-flutter)给 Adapty。 ## 观察者模式设置 \{#observer-mode-setup\} 如果你自己处理购买和订阅状态,并使用 Adapty 发送订阅事件和分析数据,请开启观察者模式。 :::important 在观察者模式下运行时,Adapty SDK 不会关闭任何交易,请确保你自己处理。 ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withObserverMode(true) // Enable observer mode ..withLogLevel(AdaptyLogLevel.verbose), ); ``` 参数: | 参数 | 描述 | | --------------------------- | ------------------------------------------------------------ | | observerMode | 一个布尔值,用于控制[观察者模式](observer-vs-full-mode)。默认值为 `false`。 | ## 在 Observer 模式下使用 Adapty 付费墙 \{#using-adapty-paywalls-in-observer-mode\} 如果你还想使用 Adapty 的付费墙和 A/B 测试功能,也是可以的——但在 Observer 模式下需要一些额外配置。除了上述步骤之外,你还需要完成以下操作: 1. 按照常规方式展示[远程配置付费墙](present-remote-config-paywalls-flutter)。 3. 将付费墙与购买交易[进行关联](report-transactions-observer-mode-flutter)。 :::tip 在 SDK v4 中,你也可以在 Observer 模式下展示 Adapty 渲染的流程和付费墙:注册一个 `AdaptyUIObserverModeResolver`,当用户点击对应按钮时,用你自己的代码执行购买或恢复操作。详见[在 Observer 模式下展示流程](flutter-present-flows-in-observer-mode)。 ::: --- # File: report-transactions-observer-mode-flutter --- --- title: "在 Flutter SDK 的观察者模式下上报交易" description: "在 Adapty 观察者模式下上报购买交易,用于 Flutter SDK 的用户洞察和收入跟踪。" --- 在观察者模式下,Adapty SDK 无法自动跟踪通过您现有购买系统完成的购买。您需要从应用商店上报交易。在发布应用之前,务必完成此设置,否则会导致分析数据出错。 使用 `reportTransaction` 显式上报每笔交易,以便 Adapty 识别。 :::warning **请勿跳过交易上报!** 如果您不调用 `reportTransaction`,Adapty 将无法识别该交易,它不会出现在分析数据中,也不会被发送至集成渠道。 ::: 如果您使用 Adapty 付费墙,请在上报交易时携带 `variationId`。这会将购买与触发它的付费墙关联起来,从而确保付费墙分析数据的准确性。 ```dart showLineNumbers try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` 参数: | 参数 | 是否必填 | 描述 | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | 必填 |
  • iOS:交易的标识符。
  • Android:购买的字符串标识符 `purchase.getOrderId`,其中 purchase 是计费库 [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) 类的实例。
| | variationId | 可选 | 实验变体的字符串标识符。您可以通过 [AdaptyPaywall](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) 对象的 `variationId` 属性获取。 |
在观察者模式下,Adapty SDK 无法自动跟踪通过您现有购买系统完成的购买。您需要从应用商店上报或恢复交易。在发布应用之前,务必完成此设置,否则会导致分析数据出错。 在两个平台上均使用 `reportTransaction` 显式上报每笔交易,并在 Android 上额外调用 `restorePurchases`,以确保 Adapty 能够识别该交易。 :::warning **请勿跳过交易上报和购买恢复!** 如果您不调用这些方法,Adapty 将无法识别该交易,它不会出现在分析数据中,也不会被发送至集成渠道。 ::: 如果您使用 Adapty 付费墙,请在上报交易时携带 `variationId`。这会将购买与触发它的付费墙关联起来,从而确保付费墙分析数据的准确性。 ```dart showLineNumbers // every time when calling transaction.finish() if (Platform.isAndroid) { try { await Adapty().restorePurchases(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } } try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` 参数: | 参数 | 是否必填 | 描述 | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | 必填 |
  • iOS,StoreKit 1:[SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) 对象。
  • iOS,StoreKit 2:[Transaction](https://developer.apple.com/documentation/storekit/transaction) 对象。
  • Android:字符串标识符(purchase.getOrderId,其中 purchase 是计费库 [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) 类的实例)。
| | variationId | 可选 | 实验变体的字符串标识符。您可以通过 [AdaptyPaywall](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) 对象的 `variationId` 属性获取。 |
**上报交易** - 3.1.x 及以下版本会自动监听 App Store 中的交易,因此无需手动上报。 - 3.2 版本不支持观察者模式。 **上报交易** 使用 `restorePurchases` 在观察者模式下向 Adapty 上报交易,详情请参阅[在移动代码中恢复购买](flutter-restore-purchase)页面。 :::warning **请勿跳过交易上报!** 如果您不调用 `restorePurchases`,Adapty 将无法识别该交易,它不会出现在分析数据中,也不会被发送至集成渠道。 ::: **将付费墙与交易关联** Adapty SDK 无法确定购买的来源,因为这部分由您自行处理。因此,如果您打算在观察者模式下使用付费墙和/或 A/B 测试,则需要在移动应用代码中将来自应用商店的交易与对应的付费墙关联起来。在发布应用之前,务必正确完成此设置,否则会导致分析数据出错。 ```dart final transactionId = transaction.transactionIdentifier final variationId = paywall.variationId try { await Adapty().setVariationId('transactionId', variationId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ```
--- # File: flutter-troubleshoot-purchases --- --- title: "排查 Flutter SDK 中的购买问题" description: "排查 Flutter SDK 中的购买问题" --- 本指南帮助您解决在 Flutter SDK 中手动实现购买时常见的问题。 ## makePurchase 调用成功,但用户画像未更新 \{#makepurchase-is-called-successfully-but-the-profile-is-not-being-updated\} **问题**:`makePurchase` 方法成功完成,但用户的用户画像和订阅状态在 Adapty 中未更新。 **原因**:这通常表明 Google Play Store 设置不完整或存在配置问题。 **解决方案**:请确保您已完成所有 [Google Play 设置步骤](initial-android)。 ## makePurchase 被调用两次 \{#makepurchase-is-invoked-twice\} **问题**:`makePurchase` 方法针对同一购买被多次调用。 **原因**:这通常发生在由于 UI 状态管理问题或用户快速交互导致购买流程被多次触发时。 **解决方案**:请确保您已完成所有 [Google Play 设置步骤](initial-android)。 ## 观察者模式下的 AdaptyError.cantMakePayments \{#adaptyerrorcantmakepayments-in-observer-mode\} **问题**:在观察者模式下使用 `makePurchase` 时出现 `AdaptyError.cantMakePayments`。 **原因**:在观察者模式下,您应在自己的代码中处理购买,而不是使用 Adapty 的 `makePurchase` 方法。 **解决方案**:如果您使用 `makePurchase` 进行购买,请关闭观察者模式。您需要二选一:要么使用 `makePurchase`,要么在观察者模式下自行处理购买。详情请参阅[实现观察者模式](implement-observer-mode-flutter)。 ## Adapty 错误:(code: 103, message: Play Market request failed on purchases updated: responseCode=3, debugMessage=Billing Unavailable, detail: null) \{#adapty-error-code-103-message-play-market-request-failed-on-purchases-updated-responsecode3-debugmessagebilling-unavailable-detail-null\} **问题**:您收到来自 Google Play Store 的结算不可用错误。 **原因**:此错误与 Adapty 无关,是 Google Play 结算库的错误,表示该设备上结算服务不可用。 **解决方案**:此错误与 Adapty 无关。您可以在 Play Store 文档中查看并了解更多信息:[处理 BillingResult 响应代码](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) | Play Billing | Android Developers。 ## 未找到 makePurchasesCompletionHandlers \{#not-found-makepurchasescompletionhandlers\} **问题**:您遇到了找不到 `makePurchasesCompletionHandlers` 的问题。 **原因**:这通常与沙盒测试问题有关。 **解决方案**:创建一个新的沙盒用户并重试。这通常可以解决与沙盒相关的购买完成处理程序问题。 ## 其他问题 \{#other-issues\} **问题**:您遇到了上述未涵盖的其他购买相关问题。 **解决方案**:如有需要,请参考[迁移指南](flutter-sdk-migration-guides)将 SDK 升级至最新版本。许多问题已在较新版本的 SDK 中得到修复。 --- # File: flutter-identifying-users --- --- title: "在 Flutter SDK 中识别用户" description: "在 Adapty 中识别用户,以改善个性化订阅体验。" --- Adapty 会为每位用户创建一个内部 Profile ID。但如果您有自己的身份验证系统,则应设置您自己的 Customer User ID。您可以在 [用户画像](profiles-crm) 部分通过 Customer User ID 查找用户,也可以在 [服务端 API](getting-started-with-server-side-api) 中使用它,该 ID 还会发送到所有集成渠道。 ### 在配置时设置 Customer User ID \{#setting-customer-user-id-on-configuration\} 如果在配置时已有用户 ID,只需将其作为 `customerUserId` 参数传递给 `.activate()` 方法: ```dart showLineNumbers title="Dart" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID) ); } catch (e) { // handle the error } ``` :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 ::: ### 在配置后设置客户用户 ID \{#setting-customer-user-id-after-configuration\} 如果在 SDK 配置时没有用户 ID,可以随时通过 `.identify()` 方法进行设置。最常见的使用场景是用户注册或登录后,从匿名用户切换为已认证用户时。 ```dart showLineNumbers try { await Adapty().identify(customerUserId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 请求参数: - **Customer User ID**(必填):字符串类型的用户标识符。 :::warning 重新提交重要的用户数据 在某些情况下,例如用户重新登录账户时,Adapty 的服务器可能已经存储了该用户的信息。在这种情况下,Adapty SDK 会自动切换到新用户。如果你之前向匿名用户传递了任何数据(例如自定义属性或来自第三方网络的归因数据),则需要为已识别的用户重新提交这些数据。 还需要注意的是,识别用户后应重新请求所有付费墙和产品,因为新用户的数据可能有所不同。 ::: ### 登出与登录 \{#logging-out-and-logging-in\} 您可以随时调用 `.logout()` 方法将用户登出: ```dart showLineNumbers try { await Adapty().logout(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` 之后,您可以使用 `.identify()` 方法将用户重新登录。 ## 分配 `appAccountToken`(iOS)\{#assign-appaccounttoken-ios\} [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) 是一个 **UUID**,用于将 App Store 交易与你的内部用户身份关联起来。 StoreKit 会将此令牌与每笔交易绑定,方便你的后端将 App Store 数据与用户匹配。 为每位用户生成一个稳定的 UUID,并在同一账号的不同设备上复用它。 这样可以确保购买记录和 App Store 通知始终正确关联。 您可以通过两种方式设置令牌——在 SDK 激活时或在识别用户时。 :::important 您必须始终将 `appAccountToken` 与 `customerUserId` 一起传递。 如果仅传递令牌,该令牌将不会包含在交易中。 ::: ```dart showLineNumbers // 在配置时: try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID, iosAppAccountToken: "YOUR_APP_ACCOUNT_TOKEN") ); } catch (e) { // 处理错误 } // 或在识别用户时 try { await Adapty().identify(customerUserId, iosAppAccountToken: "YOUR_APP_ACCOUNT_TOKEN"); } on AdaptyError catch (adaptyError) { // 处理错误 } catch (e) { } ``` ### 设置混淆账户 ID(Android)\{#set-obfuscated-account-ids-android\} Google Play 在某些使用场景下要求提供混淆账户 ID,以增强用户隐私和安全性。这些 ID 帮助 Google Play 识别购买行为,同时保持用户信息匿名,对于防欺诈和数据分析尤为重要。 如果您的应用处理敏感用户数据,或需要遵守特定隐私法规,则可能需要设置这些 ID。混淆 ID 使 Google Play 能够在不暴露真实用户标识符的情况下追踪购买记录。 ```dart showLineNumbers // During configuration: try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID, androidObfuscatedAccountId: "OBFUSCATED_ACCOUNT_ID") ); } catch (e) { // handle the error } // Or when identifying users try { await Adapty().identify(customerUserId, androidObfuscatedAccountId: "OBFUSCATED_ACCOUNT_ID"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ## 跨设备用户识别 \{#detect-users-across-devices\} 当 SDK 激活时,它会自动从 StoreKit (iOS) 或 Google Play Billing (Android) 读取用户现有的权益,并将其同步到 Adapty 后端。活跃订阅无需应用调用 `restorePurchases`,即可出现在 Adapty 用户画像中。 **不会**自动发生的是:识别新设备上的用户画像与原设备上的用户画像属于同一用户。Adapty 通过 Customer User ID 匹配用户画像,因此身份连续性取决于您使用什么作为 CUID。 **Adapty 跨设备可检测的内容** | 您的配置 | Adapty 检测到的内容 | 您需要做什么 | | --- | --- | --- | | Customer User ID = `device_id`(无应用登录) | 新设备获得不同的 CUID,因此拥有不同的用户画像。订阅通过 **Access level updated** 事件同步到新用户画像,但 `subscription_started` 不会触发——新用户画像被视为原始购买的继承者。基于 `subscription_started` 的分析将少计回归用户。 | 使用稳定的账户 ID 作为 Customer User ID,以便回归用户能跨设备匹配到现有用户画像。 | | Customer User ID = 稳定账户 ID(每台设备均需登录) | SDK 在 `activate()` 时自动同步订阅,`identify()` 通过 CUID 匹配现有用户画像。 | 无需额外配置——身份和订阅均可自动解析。 | | Apple Family Sharing 继承者 | 家庭成员仅通过 **Access level updated** 事件接收订阅——`subscription_started` 不会触发。 | 监听 **Access level updated**。完整的事件矩阵请参见 [Apple Family Sharing](apple-family-sharing)。 | | 同一 Apple/Google 账户,不同应用内用户 | 最先记录购买的用户画像成为父级。后续用户画像通过继承链查看订阅,并触发一次 **Access level updated** 事件。 | 要求用户登录,然后选择适合您业务模型的[共享模式](sharing-paid-access-between-user-accounts)。 | **在新设备上恢复购买** 在付费墙上提供一个用户可主动触发的"恢复购买"按钮。Apple App Review(指南 3.1.1)要求提供此按钮,且当自动同步遗漏边缘情况时,它也可作为备用方案。该按钮应调用 SDK 中的 `restorePurchases`。 正常使用时,首次启动时无需通过代码调用 `restorePurchases`——SDK 已在 `activate()` 时执行了等效操作。仅在需要强制刷新收据检查时才使用代码调用,例如在 `activate()` 完成后调试访问等级缺失问题时。 --- # File: flutter-setting-user-attributes --- --- title: "在 Flutter SDK 中设置用户属性" description: "了解如何在 Adapty 中设置用户属性以实现更好的目标受众细分。" --- 您可以为应用用户设置可选属性,例如电子邮件、电话号码等。然后,您可以使用这些属性创建用户[市场细分](segments),或直接在 CRM 中查看它们。 ### 设置用户属性 \{#setting-user-attributes\} 要设置用户属性,请调用 `.updateProfile()` 方法: ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setEmail("email@email.com") ..setPhoneNumber("+18888888888") ..setFirstName('John') ..setLastName('Appleseed') ..setGender(AdaptyProfileGender.other) ..setBirthday(DateTime(1970, 1, 3)); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 请注意,您之前通过 `updateProfile` 方法设置的属性不会被重置。 :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 ::: ### 允许的键列表 \{#the-allowed-keys-list\} `AdaptyProfileParameters.Builder` 允许的键 `` 及其对应的值 `` 如下所示: | 键 | 值 | |---|-----| |

email

phoneNumber

firstName

lastName

| String | | gender | 枚举类型,允许的值为:`female`、`male`、`other` | | birthday | Date | ### 自定义用户属性 \{#custom-user-attributes\} 你可以设置自己的自定义用户属性,这些属性通常与应用的使用情况相关。例如,健身类应用可以记录每周的锻炼次数,语言学习类应用可以记录用户的知识水平等。你可以在市场细分中使用这些属性来创建精准的付费墙和优惠活动,也可以在分析中利用它们来找出哪些产品数据图表对收入影响最大。 ```dart showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..setCustomStringAttribute('value1', 'key1') ..setCustomDoubleAttribute(1.0, 'key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 要删除已有的键,请使用 `.withRemoved(customAttributeForKey:)` 方法: ```dart showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..removeCustomAttribute('key1') ..removeCustomAttribute('key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 有时你需要查看之前已设置了哪些自定义属性。为此,请使用 `AdaptyProfile` 对象的 `customAttributes` 字段。 :::warning 请注意,`customAttributes` 的值可能已过时,因为用户属性可以随时从不同设备发送,服务器上的属性可能在上次同步后已被修改。 ::: ### 限制 \{#limits\} - 每个用户最多 30 个自定义属性 - 键名最长为 30 个字符。键名可以包含字母数字字符以及以下任意字符:`_` `-` `.` - 值可以是字符串或浮点数,最多 50 个字符。 --- # File: flutter-listen-subscription-changes --- --- title: "在 Flutter SDK 中查看订阅状态" description: "在 Adapty 中追踪和管理用户订阅状态,提升 Flutter 应用的用户留存率。" --- 借助 Adapty,订阅状态管理变得轻而易举。你无需在代码中手动填入产品 ID,只需检查用户是否拥有有效的[访问等级](access-level),即可确认其订阅状态。
开始检查订阅状态前的准备工作(点击展开) - iOS 请配置 [App Store Server Notifications](enable-app-store-server-notifications) - Android 请配置 [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn)
## 访问等级与 AdaptyProfile 对象 \{#access-level-and-the-adaptyprofile-object\} 访问等级是 [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) 对象的属性。我们建议在应用启动时获取用户画像,例如在[识别用户](flutter-identifying-users#setting-customer-user-id-on-configuration)时,之后在发生变化时及时更新。这样,你就可以直接使用已有的用户画像对象,而无需反复请求。 要接收用户画像更新通知,请按照下方[监听用户画像更新(包括访问等级)](flutter-listen-subscription-changes)部分的说明监听用户画像变更。 :::tip 想看看 Adapty SDK 在移动应用中的实际集成示例吗?欢迎查看我们的[示例应用](sample-apps),其中演示了完整的集成流程,包括展示付费墙、完成购买以及其他基本功能。 ::: ## 从服务器获取访问等级 \{#retrieving-the-access-level-from-the-server\} 使用 `.getProfile()` 方法从服务器获取访问等级: ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 响应参数: | 参数 | 描述 | | --------- | ------------------------------------------------------------ | | Profile |

一个 [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) 对象。通常,你只需检查用户画像的访问等级状态,即可判断用户是否拥有应用的高级访问权限。

`.getProfile` 方法始终尝试请求 API,因此返回的结果是最新的。如果由于某些原因(例如无网络连接)导致 Adapty SDK 无法从服务器获取信息,则会返回缓存中的数据。还需注意,Adapty SDK 会定期更新 `AdaptyProfile` 缓存,以尽可能保持数据的实时性。

| `.getProfile()` 方法可以获取用户画像,从中你可以了解访问等级的状态。一个应用可以拥有多个访问等级。例如,如果你有一个新闻应用,并对不同主题独立销售订阅,可以创建"sports"和"science"两个访问等级。但大多数情况下,你只需要一个访问等级,此时直接使用默认的"premium"访问等级即可。 以下是检查默认"premium"访问等级的示例: ```dart showLineNumbers try { final profile = await Adapty().getProfile(); if (profile?.accessLevels['premium']?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### 监听订阅状态更新 \{#listening-for-subscription-status-updates\} 每当用户的订阅发生变化时,Adapty 都会触发一个事件。 要接收来自 Adapty 的消息,您需要进行一些额外配置: ```dart showLineNumbers Adapty().didUpdateProfileStream.listen((profile) { // handle any changes to subscription state }); ``` Adapty 也会在应用启动时触发一个事件,此时将传递缓存中的订阅状态。 ### 订阅状态缓存 \{#subscription-status-cache\} Adapty SDK 内置了缓存机制,用于存储用户画像的订阅状态。这意味着即使服务器无法访问,也可以从缓存数据中获取用户画像的订阅状态信息。 但需要注意的是,无法直接从缓存中请求数据。SDK 每隔一分钟会定期向服务器查询,检查用户画像是否有任何更新或变更。如果存在修改(例如新的交易记录或其他更新),这些内容将同步到缓存数据中,以保持其与服务器的一致性。 --- # File: flutter-deal-with-att --- --- title: "在 Flutter SDK 中处理 ATT" description: "开始在 Flutter 上使用 Adapty,简化订阅设置与管理。" --- 如果您的应用使用了 AppTrackingTransparency 框架,并向用户展示应用跟踪授权请求,则应将[授权状态](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/)发送给 Adapty。 ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setAppTrackingTransparencyStatus(AdaptyIOSAppTrackingTransparencyStatus.authorized); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` :::warning 我们强烈建议您在该值发生变化时尽早发送,只有这样,数据才能及时传递到您所配置的集成渠道。 ::: --- # File: kids-mode-flutter --- --- title: "Flutter SDK 中的儿童模式" description: "轻松启用儿童模式,遵守 Apple 和 Google 政策。Flutter SDK 中不收集 IDFA、GAID 或广告数据。" --- 如果您的 Flutter 应用面向儿童,则必须遵守 [Apple](https://developer.apple.com/kids/) 和 [Google](https://support.google.com/googleplay/android-developer/answer/9893335) 的相关政策。如果您正在使用 Adapty SDK,只需几个简单步骤即可将其配置为符合这些政策,并顺利通过应用商店审核。 ## 需要做什么?\{#whats-required\} 你需要配置 Adapty SDK 以禁止收集以下信息: - [IDFA(广告主标识符)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers)(iOS) - [Android 广告 ID(AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248)(Android) - [IP 地址](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) 此外,我们建议谨慎使用 customer user ID。以 `` 格式设置的 User ID 与使用邮箱地址一样,会被认定为收集个人数据。在儿童模式下,最佳做法是使用随机化或匿名化的标识符(例如哈希 ID 或设备生成的 UUID),以确保合规。 ## 启用儿童模式 \{#enabling-kids-mode\} ### 在 Adapty 看板中进行更新 \{#updates-in-the-adapty-dashboard\} 在 Adapty 看板中,您需要禁用 IP 地址收集。为此,请前往 [App settings](https://app.adapty.io/settings/general),然后在 **Collect users' IP address** 下点击 **Disable IP address collection**。 ### 更新您的移动应用代码 \{#updates-in-your-mobile-app-code\} 为了遵守相关政策,请禁用用户 IDFA(iOS)、GAID/AAID(Android)以及 IP 地址的收集。 **Android:更新 SDK 配置** ```dart showLineNumbers title="Dart" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') // highlight-start ..withGoogleAdvertisingIdCollectionDisabled(true) // set to `true` ..withIpAddressCollectionDisabled(true), // set to `true` // highlight-end ); } catch (e) { // handle the error } ``` **iOS:在 SDK v4 中启用儿童模式** :::important 在 SDK v4 中,原生 iOS SDK 通过 Swift Package Manager 安装,儿童模式通过 `KidsMode` Swift 包 trait 启用,该 trait 会在编译时移除所有 IDFA、AdSupport 和 AppTrackingTransparency 相关代码。此功能需要 **Xcode 26** 或更高版本。 ::: 在 SDK v4 中,请在 `pubspec.yaml` 中使用 `adapty_flutter_kids` 包替代 `adapty_flutter`。这是该插件的儿童模式变体,具有相同的公共 API 和相同的版本号——唯一的区别是其原生 iOS SDK 使用 `KidsMode` trait 构建: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter_kids: 4.0.0 ``` 您的 Dart 代码保持不变——只需将 import 更新为新的包名: ```dart showLineNumbers title="Dart" ``` **iOS:通过 CocoaPods 启用儿童模式(SDK v3)** 1. 更新您的 Podfile: - 如果您**没有** `post_install` 部分,请添加下面的完整代码块。 - 如果您**已有** `post_install` 部分,请将高亮行合并进去。 ```ruby showLineNumbers title="Podfile" def adapty_enable_kids_mode(installer) installer.pods_project.targets.each do |target| next unless target.name == 'Adapty' target.build_configurations.each do |config| flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)' flags = flags.join(' ') if flags.is_a?(Array) config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -DADAPTY_KIDS_MODE" end target.frameworks_build_phase.files.dup.each do |bf| target.frameworks_build_phase.remove_build_file(bf) if bf.display_name.to_s.include?('AdSupport') end end installer.pods_project.save Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', '**', '*.xcconfig')).each do |xc| File.write(xc, File.read(xc).gsub(/\s*-framework\s+"?AdSupport"?/, '')) end end post_install do |installer| # ... keep your existing post_install body (Flutter adds one automatically) ... adapty_enable_kids_mode(installer) # <-- enable Adapty Kids Mode end ``` 2. 运行以下命令以应用更改 ```sh showLineNumbers title="Shell" pod install ``` --- # File: flutter-get-onboardings --- --- title: "在 Flutter SDK 中获取用户引导" description: "了解如何在 Adapty 的 Flutter SDK 中获取用户引导。" --- :::warning **用户引导功能在 SDK v4 中已被废弃,并将在未来版本中移除。** 该功能不再接受修复或改进。请改用 [flows](flutter-get-pb-paywalls):与运行在 WebView 中的用户引导不同,flows 直接在设备上原生渲染,带来更流畅的动画效果、一致的原生外观体验、更快的加载速度,且无需依赖 WebView 运行时。请参阅[获取 flows 和付费墙](flutter-get-pb-paywalls)和[展示 flows 和付费墙](flutter-present-paywalls)快速上手。 ::: 在 Adapty 看板中[使用编辑工具设计好用户引导的视觉部分](design-onboarding)后,您可以在 Flutter 应用中展示它。此过程的第一步是获取与版位关联的用户引导及其视图配置,具体如下所述。 开始之前,请确保: 1. 已安装 [Adapty Flutter SDK](sdk-installation-flutter) 3.8.0 或更高版本。 2. 已[创建用户引导](create-onboarding)。 3. 已将用户引导添加到[版位](placements)。 ## 获取用户引导 \{#fetch-onboarding\} 当您使用我们的无代码编辑工具创建[用户引导](onboardings)时,它会以容器形式存储,包含您的应用需要获取并展示的配置信息。该容器管理整个体验——显示哪些内容、如何呈现,以及如何处理用户交互(如测验答案或表单输入)。容器还会自动追踪分析事件,因此您无需单独实现视图追踪。 为获得最佳性能,请尽早获取用户引导配置,以便在向用户展示之前有足够时间下载图片。 要获取用户引导,请使用 `getOnboarding` 方法: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboarding(placementId: "YOUR_PLACEMENT_ID"); } on AdaptyError catch (e) { //handle error } catch (e) { //handle error } ``` 然后,调用 `createOnboardingView` 方法获取将要展示的视图。 :::warning `createOnboardingView` 方法的结果只能使用一次。如果需要再次使用,请重新调用 `createOnboardingView` 方法。在不重新创建的情况下调用两次可能会导致 `AdaptyUIError.viewAlreadyPresented` 错误。 ::: ```dart showLineNumbers try { final onboardingView = await Adapty().createOnboardingView(onboarding: onboarding); } on AdaptyError catch (e) { //handle error } catch (e) { //handle error } ``` 参数: | 参数 | 是否必填 | 描述 | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | 必填 | 所需[版位](placements)的标识符。这是您在 Adapty 看板中创建版位时指定的值。 | | **locale** |

可选

默认值:`en`

|

用户引导本地化的标识符。该参数应为由一个或两个子标签组成的语言代码,子标签之间用减号(**-**)分隔。第一个子标签表示语言,第二个表示地区。

示例:`en` 表示英语,`pt-br` 表示巴西葡萄牙语。

| | **fetchPolicy** | 默认值:`.reloadRevalidatingCacheData` |

默认情况下,SDK 会尝试从服务器加载数据,若失败则返回缓存数据。我们推荐此选项,因为它能确保用户始终获得最新数据。

但是,如果您认为用户的网络连接不稳定,可以考虑使用 `.returnCacheDataElseLoad`,在缓存数据存在时直接返回缓存。在这种情况下,用户可能无法获得最新数据,但无论网络状况如何,都能体验更快的加载速度。缓存会定期更新,因此在会话期间使用缓存以避免网络请求是安全的。

请注意,缓存在重启应用后依然保留,仅在重新安装应用或手动清理时才会清除。

Adapty SDK 在本地以两层方式存储用户引导:上述定期更新的缓存层和备用用户引导层。我们还使用 CDN 加速用户引导的获取,并在 CDN 不可访问时使用独立备用服务器。该系统旨在确保您始终获得最新版本的用户引导,同时在网络连接稀缺的情况下也能保证可靠性。

| | **loadTimeout** | 默认值:5 秒 |

该值限制此方法的超时时间。如果达到超时,将返回缓存数据或本地备用数据。

请注意,在极少数情况下,此方法的实际超时可能略晚于 `loadTimeout` 中指定的时间,因为该操作在底层可能由多个不同请求组成。

| 响应参数: | 参数 | 描述 | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | 一个 [`AdaptyOnboarding`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyOnboarding-class.html) 对象,包含:用户引导标识符和配置、远程配置以及其他几个属性。 | ## 使用默认目标受众用户引导加速获取 \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} 通常,用户引导几乎可以即时获取,因此您无需担心加速此过程。但是,当您有大量目标受众和用户引导,且用户网络连接较弱时,获取用户引导可能比预期耗时更长。在这种情况下,您可能希望展示默认用户引导以确保流畅的用户体验,而不是不显示任何内容。 为解决这一问题,您可以使用 `getOnboardingForDefaultAudience` 方法,该方法会获取指定版位中针对**所有用户**目标受众的用户引导。但请务必了解,推荐的方式仍是通过 `getOnboarding` 方法获取用户引导,详见上方的[获取用户引导](#fetch-onboarding)部分。 :::warning 请考虑使用 `getOnboarding` 而非 `getOnboardingForDefaultAudience`,因为后者有以下重要限制: - **兼容性问题**:在支持多个应用版本时可能产生问题,需要设计向后兼容的界面,否则旧版本可能显示异常。 - **无个性化**:仅显示"所有用户"目标受众的内容,无法基于国家、归因或自定义属性进行定向。 如果在您的使用场景中更快的获取速度优于上述缺点,请按以下方式使用 `getOnboardingForDefaultAudience`。否则,请按[上方](#fetch-onboarding)所述使用 `getOnboarding`。 ::: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboardingForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` 参数: | 参数 | 是否必填 | 描述 | |-----------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | 必填 | 所需[版位](placements)的标识符。这是您在 Adapty 看板中创建版位时指定的值。 | | **locale** |

可选

默认值:`en`

|

用户引导本地化的标识符。该参数应为由一个或两个子标签组成的语言代码,子标签之间用减号(**-**)分隔。第一个子标签表示语言,第二个表示地区。

示例:`en` 表示英语,`pt-br` 表示巴西葡萄牙语。

| | **fetchPolicy** | 默认值:`.reloadRevalidatingCacheData` |

默认情况下,SDK 会尝试从服务器加载数据,若失败则返回缓存数据。我们推荐此选项,因为它能确保用户始终获得最新数据。

但是,如果您认为用户的网络连接不稳定,可以考虑使用 `.returnCacheDataElseLoad`,在缓存数据存在时直接返回缓存。在这种情况下,用户可能无法获得最新数据,但无论网络状况如何,都能体验更快的加载速度。缓存会定期更新,因此在会话期间使用缓存以避免网络请求是安全的。

请注意,缓存在重启应用后依然保留,仅在重新安装应用或手动清理时才会清除。

Adapty SDK 在本地以两层方式存储用户引导:上述定期更新的缓存层和备用用户引导层。我们还使用 CDN 加速用户引导的获取,并在 CDN 不可访问时使用独立备用服务器。该系统旨在确保您始终获得最新版本的用户引导,同时在网络连接稀缺的情况下也能保证可靠性。

| --- # File: flutter-present-onboardings --- --- title: "在 Flutter SDK 中展示用户引导" description: "了解如何有效展示用户引导以提升转化率。" --- :::warning **用户引导功能已在 SDK v4 中弃用,并将在未来版本中移除。** 该功能不再接受修复或改进。请改用 [flows](flutter-get-pb-paywalls):与在 WebView 中运行的用户引导不同,flows 直接在设备上原生渲染,带来更流畅的动画、一致的原生外观体验、更快的加载速度,且无需依赖 WebView 运行时。请参阅 [获取 flows 与付费墙](flutter-get-pb-paywalls) 和 [展示 flows 与付费墙](flutter-present-paywalls) 快速上手。 ::: 如果您已使用编辑工具自定义了用户引导,则无需在 Flutter 应用代码中手动处理渲染逻辑来向用户展示它。这类用户引导已包含展示内容与展示方式的完整配置。 开始之前,请确认以下事项: 1. 您已安装 [Adapty Flutter SDK](sdk-installation-flutter) 3.8.0 或更高版本。 2. 您已[创建用户引导](create-onboarding)。 3. 您已将用户引导添加到[版位](placements)。 Adapty Flutter SDK 提供两种展示用户引导的方式: - **独立页面** - **嵌入式组件** ## 以独立屏幕呈现 \{#present-as-standalone-screen\} 要将用户引导以独立屏幕的形式展示,请对 `createOnboardingView` 方法创建的 `onboardingView` 调用 `onboardingView.present()` 方法。每个 `view` 只能使用一次。如果需要再次展示用户引导,请重新调用 `createOnboardingView` 创建一个新的 `onboardingView` 实例。 :::warning 重复使用同一个 `onboardingView` 而不重新创建,可能会导致 `AdaptyUIError.viewAlreadyPresented` 错误。 ::: ```dart showLineNumbers title="Flutter" try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### 关闭用户引导 \{#dismiss-the-onboarding\} 当需要以编程方式关闭用户引导时,请使用 `dismiss()` 方法: ```dart showLineNumbers title="Flutter" try { await onboardingView.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### 配置 iOS 展示样式 \{#configure-ios-presentation-style\} 通过向 `present()` 方法传入 `iosPresentationStyle` 参数,可配置用户引导在 iOS 上的展示方式。该参数接受 `AdaptyUIIOSPresentationStyle.fullScreen`(默认值)或 `AdaptyUIIOSPresentationStyle.pageSheet` 值。 ```dart showLineNumbers try { await onboardingView.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## 嵌入到 Widget 层级中 \{#embed-in-widget-hierarchy\} 若要将用户引导嵌入到现有的 Widget 树中,可直接在 Flutter Widget 层级里使用 `AdaptyUIOnboardingPlatformView` 组件。 ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, // The onboarding object you fetched onDidFinishLoading: (meta) { }, onDidFailWithError: (error) { }, onCloseAction: (meta, actionId) { }, onPaywallAction: (meta, actionId) { }, onCustomAction: (meta, actionId) { }, onStateUpdatedAction: (meta, elementId, params) { }, onAnalyticsEvent: (meta, event) { }, ) ``` :::note 要使 Android 平台视图正常工作,请确保你的 `MainActivity` 继承自 `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: ## 用户引导加载中 \{#loader-during-onboarding\} 展示用户引导时,你可能会注意到在启动画面和用户引导之间有一个短暂的加载界面,这是底层视图初始化时产生的。你可以根据自己的需求,用不同的方式来处理这个问题。 #### 使用 onDidFinishLoading 控制启动画面 \{#control-splash-screen-using-ondidfinishloading\} :::note 该方式仅在将用户引导作为 widget 嵌入时可用,不支持以独立页面方式展示。 ::: 推荐的跨平台方案是:保持启动屏或自定义遮罩可见,直到用户引导完全加载后,再手动将其隐藏。 使用嵌入式 widget 时,在其上方叠加自定义 widget,并在 `onDidFinishLoading` 触发时隐藏遮罩: ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Hide your custom splash screen or overlay here }, // ... other callbacks ) ``` ### 自定义原生加载界面 \{#customize-native-loader\} :::important 此方式与平台相关,需要维护原生 UI 代码。除非您的应用已维护独立的原生层,否则不建议使用。 ::: 如果需要自定义默认加载界面本身,可以使用平台专属布局进行替换。此方式需要分别针对 Android 和 iOS 进行实现: - **iOS**:将 `AdaptyOnboardingPlaceholderView.xib` 添加到您的 Xcode 项目中 - **Android**:在 `res/layout` 中创建 `adapty_onboarding_placeholder_view.xml` 并在其中定义占位视图 ## 自定义用户引导中的链接打开方式 \{#customize-how-links-open-in-onboardings\} :::important 自定义用户引导中链接的打开方式需要 Adapty SDK v3.15.1 及以上版本。 ::: 默认情况下,用户引导中的链接会在应用内浏览器中打开。这样可以在应用内直接展示网页,用户无需切换应用,体验更流畅。 如果你希望在外部浏览器中打开链接,可以将 `externalUrlsPresentation` 参数设置为 `AdaptyWebPresentation.externalBrowser` 来自定义此行为: ```dart showLineNumbers title="Flutter" final onboardingView = await AdaptyUI().createOnboardingView( onboarding: onboarding, externalUrlsPresentation: AdaptyWebPresentation.externalBrowser, // default – AdaptyWebPresentation.inAppBrowser ); try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, externalUrlsPresentation: AdaptyWebPresentation.externalBrowser, // default – AdaptyWebPresentation.inAppBrowser onDidFinishLoading: (meta) { }, onDidFailWithError: (error) { }, onCloseAction: (meta, actionId) { }, onPaywallAction: (meta, actionId) { }, onCustomAction: (meta, actionId) { }, onStateUpdatedAction: (meta, elementId, params) { }, onAnalyticsEvent: (meta, event) { }, ) ``` ## 禁用安全区域内边距(Android) \{#disable-safe-area-paddings-android\} 默认情况下,在 Android 设备上,用户引导视图会自动应用安全区域内边距,以避免与状态栏、导航栏等系统 UI 元素重叠。如果你想禁用此行为并完全自定义布局,可以在应用中添加一个布尔值资源: 1. 进入 `android/app/src/main/res/values` 目录。如果没有 `bools.xml` 文件,请新建一个。 2. 添加以下资源: ```xml false ``` 请注意,这些更改会全局应用于你应用中的所有用户引导。 --- # File: flutter-handling-onboarding-events --- --- title: "在 Flutter SDK 中处理用户引导事件" description: "使用 Adapty 在 Flutter 中处理用户引导相关事件。" --- :::warning **用户引导功能在 SDK v4 中已弃用,并将在未来版本中移除。** 该功能不再接受修复或改进。请改用 [flows](flutter-get-pb-paywalls):与在 WebView 中运行的用户引导不同,flows 直接在设备上原生渲染,带来更流畅的动画效果、一致的原生外观、更快的加载速度,以及无 WebView 运行时依赖。请参阅[获取 flows 与付费墙](flutter-get-pb-paywalls)和[展示 flows 与付费墙](flutter-present-paywalls)快速上手。 ::: 使用编辑工具配置的用户引导会生成应用可以响应的事件。处理这些事件的方式取决于你使用的展示方式: - **全屏展示**:需要设置一个全局事件观察者,用于处理所有用户引导视图的事件 - **嵌入式 Widget**:通过 Widget 中的内联回调参数直接处理事件 开始之前,请确保: 1. 你已安装 [Adapty Flutter SDK](sdk-installation-flutter) 3.8.0 或更高版本。 2. 你已[创建用户引导](create-onboarding)。 3. 你已将用户引导添加到[版位](placements)。 ## 全屏展示事件 \{#full-screen-presentation-events\} ### 设置事件观察者 \{#set-up-event-observer\} 要处理全屏用户引导的事件,请实现 `AdaptyUIOnboardingsEventsObserver` 并在展示前进行设置: ```dart showLineNumbers title="Flutter" AdaptyUI().setOnboardingsEventsObserver(this); try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### 处理事件 \{#handle-events\} 在您的观察者中实现以下方法: ```dart showLineNumbers title="Flutter" void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { // Onboarding finished loading } void onboardingViewDidFailWithError( AdaptyUIOnboardingView view, AdaptyError error, ) { // Handle loading errors } void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle close action view.dismiss(); } void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle custom actions } void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle user input updates } void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { // Track analytics events } ``` ## 嵌入式 Widget 事件 \{#embedded-widget-events\} 使用 `AdaptyUIOnboardingPlatformView` 时,你可以直接通过 Widget 内联回调参数处理事件。注意,事件会同时发送给 Widget 回调和全局观察者(如果已设置),但全局观察者是可选的: ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Onboarding finished loading }, onDidFailWithError: (error) { // Handle loading errors }, onCloseAction: (meta, actionId) { // Handle close action }, onPaywallAction: (meta, actionId) { _openPaywall(actionId); }, onCustomAction: (meta, actionId) { // Handle custom actions }, onStateUpdatedAction: (meta, elementId, params) { // Handle user input updates }, onAnalyticsEvent: (meta, event) { // Track analytics events }, ) ``` ## 事件类型 \{#event-types\} 以下章节介绍了您可以处理的各类事件,无论您使用哪种展示方式。 ### 处理自定义操作 \{#handle-custom-actions\} 在编辑工具中,你可以为按钮添加 **custom** 操作并为其分配一个 ID。 之后,您可以在代码中使用该 ID,并将其作为自定义操作来处理。例如,当用户点击自定义按钮(如 **Login** 或 **Allow notifications**)时,委托方法 `onboardingController` 会以 `.custom(id:)` 的形式触发,`actionId` 参数即为编辑工具中设置的 **Action ID**。您可以自定义任意 ID,例如 "allowNotifications"。 ```dart // Full-screen presentation void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { switch (actionId) { case 'login': _login(); break; case 'allow_notifications': _allowNotifications(); break; } } // Embedded widget onCustomAction: (meta, actionId) { _handleCustomAction(actionId); } ```
事件示例(点击展开) ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ```
### 用户引导加载完成 \{#finishing-loading-onboarding\} 当用户引导加载完成时,将触发以下事件: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { print('Onboarding loaded: ${meta.onboardingId}'); } // Embedded widget onDidFinishLoading: (meta) { print('Onboarding loaded: ${meta.onboardingId}'); } ```
事件示例(点击展开) ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ```
### 关闭用户引导 \{#closing-onboarding\} 当用户点击分配了 **Close** 操作的按钮时,用户引导即视为已关闭。 :::important 请注意,你需要自行处理用户关闭用户引导后的逻辑。例如,你需要停止显示用户引导界面本身。 ::: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { await view.dismiss(); } // Embedded widget onCloseAction: (meta, actionId) { Navigator.of(context).pop(); } ```
事件示例(点击展开) ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
### 打开付费墙 \{#opening-a-paywall\} :::tip 如果希望在用户引导内部打开付费墙,请处理此事件。如果希望在付费墙关闭后再打开另一个付费墙,有一种更直接的方式——处理关闭操作并直接打开付费墙,无需依赖事件数据。 ::: 在用户引导中使用付费墙最顺畅的方式,是将操作 ID 设置为与付费墙版位 ID 相同: :::note 请注意,在 iOS 上,同一时间只能显示一个视图(付费墙或用户引导)。如果在用户引导上方呈现付费墙,则无法以编程方式控制后台的用户引导。尝试关闭用户引导时,实际上会关闭付费墙,导致用户引导仍然可见。为避免此问题,请始终在呈现付费墙之前先关闭用户引导视图。 ::: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } Future _openPaywall(String actionId) async { // Implement your paywall opening logic here } // Embedded widget onPaywallAction: (meta, actionId) { _openPaywall(actionId); } ```
事件示例(点击展开) ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ```
### 跟踪导航 \{#tracking-navigation\} 在用户引导流程中,各种导航相关事件发生时,你都会收到对应的分析事件: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { trackEvent(event.type, meta.onboardingId); } // Embedded widget onAnalyticsEvent: (meta, event) { trackEvent(event.type, meta.onboardingId); } ``` `event` 对象可以是以下类型之一: | 类型 | 描述 | |------------|-------------| | `onboardingStarted` | 用户引导加载完成时触发 | | `screenPresented` | 任意屏幕显示时触发 | | `screenCompleted` | 屏幕完成时触发。包含可选的 `elementId`(已完成元素的标识符)和可选的 `reply`(用户的响应)。当用户执行任何退出屏幕的操作时触发。| | `secondScreenPresented` | 第二个屏幕显示时触发 | | `userEmailCollected` | 通过输入框收集到用户邮箱时触发 | | `onboardingCompleted` | 当用户到达 ID 为 `final` 的屏幕时触发。如需使用此事件,请[将 `final` ID 分配给最后一个屏幕](design-onboarding)。| | `unknown` | 用于任何无法识别的事件类型。包含 `name`(未知事件的名称)和 `meta`(附加元数据)| 每个事件都包含 `meta` 元信息,具体字段如下: | 字段 | 描述 | |------------|-------------| | `onboardingId` | 用户引导流程的唯一标识符 | | `screenClientId` | 当前页面的标识符 | | `screenIndex` | 当前页面在流程中的位置 | | `screensTotal` | 流程中的总页面数 |
事件示例(点击展开) ```javascript // onboardingStarted { "name": "onboarding_started", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } // screenPresented { "name": "screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 4 } } // screenCompleted { "name": "screen_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 }, "params": { "element_id": "profile_form", "reply": "success" } } // secondScreenPresented { "name": "second_screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // userEmailCollected { "name": "user_email_collected", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // onboardingCompleted { "name": "onboarding_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
--- # File: flutter-onboarding-input --- --- title: "在 Flutter SDK 中处理用户引导数据" description: "使用 Adapty SDK 在 Flutter 应用中保存和使用用户引导数据。" --- :::warning **用户引导功能在 SDK v4 中已弃用,将在未来版本中移除。** 该功能不再接受修复或改进。请改用 [flows](flutter-get-pb-paywalls):与运行在 WebView 中的用户引导不同,flows 直接在设备上原生渲染,带来更流畅的动画效果、一致的原生外观、更快的加载速度,且无需依赖 WebView 运行时。请参阅 [获取 flows 和付费墙](flutter-get-pb-paywalls) 及 [展示 flows 和付费墙](flutter-present-paywalls) 以开始使用。 ::: 当用户回答测验问题或在输入字段中输入数据时,`onStateUpdatedAction` 方法将被调用。您可以在代码中保存或处理字段类型。 例如: ```dart // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Process data } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Process data } ``` 有关动作格式,请参阅[此处](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyUIOnboardingPlatformView/onStateUpdatedAction.html)。
每种参数类型的属性结构(点击展开) ```dart void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // elementId is a String: elementId; // 'preference_selector' // meta — AdaptyUIOnboardingMeta: meta.onboardingId; // 'onboarding_123' meta.screenClientId; // 'preferences_screen' meta.screenIndex; // 1 meta.screensTotal; // 3 // params is one of the AdaptyOnboardingsStateUpdatedParams subclasses: switch (params) { case AdaptyOnboardingsSelectParams(:final id, :final value, :final label): // a single selected option id; // 'option_1' value; // 'premium' label; // 'Premium Plan' break; case AdaptyOnboardingsMultiSelectParams(:final params): // a list of selected options, each an AdaptyOnboardingsSelectParams params; // [(id: 'interest_1', value: 'sports', label: 'Sports'), (id: 'interest_2', value: 'music', label: 'Music')] break; case AdaptyOnboardingsInputParams(:final input): switch (input) { case AdaptyOnboardingsTextInput(:final value): value; // 'John Doe' break; case AdaptyOnboardingsEmailInput(:final value): value; // 'user@example.com' break; case AdaptyOnboardingsNumberInput(:final value): value; // 25.0 (a double) break; } break; case AdaptyOnboardingsDatePickerParams(:final day, :final month, :final year): day; // 15 month; // 6 year; // 1990 break; } } ```
## 使用场景 \{#use-cases\} ### 用户画像数据填充 \{#enrich-user-profiles-with-data\} 如果你希望立即将用户输入的数据与用户画像关联起来,避免重复询问相同信息,可以在处理操作时通过[更新用户画像](flutter-setting-user-attributes)的方式写入这些数据。 例如,你让用户在 ID 为 `name` 的文本框中输入姓名,并希望将该值设置为用户的名字;同时,你还让用户在 `email` 字段中输入邮箱地址。在你的应用代码中,实现方式如下: ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` ### 根据答案自定义付费墙 \{#customize-paywalls-based-on-answers\} 在用户引导中使用问卷,你可以根据用户完成用户引导后的答案来自定义展示给他们的付费墙。 例如,你可以询问用户的运动经验,并向不同的用户群体展示不同的 CTA 和产品。 1. 在用户引导编辑器中[添加问卷](onboarding-quizzes),并为各选项分配有意义的 ID。 2. 根据 ID 处理问卷响应,并为用户[设置自定义属性](flutter-setting-user-attributes)。 ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` 3. 为每个自定义属性值[创建市场细分](segments)。 4. 创建一个[版位](placements),并为你创建的每个市场细分添加[目标受众](audience)。 5. 在应用代码中为该版位[展示付费墙](flutter-paywalls)。如果你的用户引导中有一个打开付费墙的按钮,请将付费墙代码作为[此按钮操作的响应](flutter-handling-onboarding-events#opening-a-paywall)来实现。 --- # File: flutter-sdk-call-order --- --- title: "Flutter SDK 中的调用顺序" description: "按正确顺序调用 Adapty SDK 方法,避免丢失高级访问权限、归因缺失以及间歇性 #2002 错误。" --- `Adapty().activate()` 必须在调用任何其他 Adapty SDK 方法之前完成。在其完成之前,SDK 没有任何状态。在 `activate()` 之前或与其并行发出的任何调用都会因 [`#2002 notActivated`](error-handling-on-flutter-react-native-unity#custom-network-codes) 错误而失败。 如果你的应用需要用户认证,并在启动后才能获取到 customer user ID,请在获取到后调用 `Adapty().identify()`。在 `identify` 完成之前,不要调用任何用户操作相关的方法。与其并发执行的调用要么会以 [`#3006 profileWasChanged`](error-handling-on-flutter-react-native-unity#custom-network-codes) 失败,要么会作用于激活时创建的匿名用户画像。一旦出现这种情况,归因数据、`appsflyer_id` 等 MMP ID 以及安装归属不一定会迁移到已识别的用户画像上。如果你的应用不需要用户认证,则跳过 `identify`,直接使用匿名用户画像即可。 MMP 和分析类 SDK(AppsFlyer、Adjust、Branch、PostHog)遵循同样的规则。请先初始化它们,等待其 UID 回调后再调用 `Adapty().activate`。否则,MMP ID 会落在一个短暂的匿名用户画像上,不一定能转移到已识别的用户画像中。有关 AppsFlyer 的具体说明,请参阅 [AppsFlyer](appsflyer)。 ## 正确的操作顺序 \{#the-correct-order\} 您的路径取决于两件事:何时获取到用户 ID,以及是否使用了 MMP 或分析 SDK。 - **步骤 2 和 5**:每个应用都必须执行。先激活 SDK,再调用 SDK 方法。 - **步骤 1 和 3**:仅在集成 MMP 或分析 SDK(AppsFlyer、Adjust、Branch、PostHog)时需要。 - **步骤 4**:仅在应用需要用户认证,且在启动后才能获取用户 ID 时需要。 如果在应用启动时已知客户用户 ID,可直接在 `activate()` 中传入(步骤 2a)。这样不会创建匿名用户画像,因此无需执行步骤 4。 | 步骤 | 调用 | 时机 | 说明 | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | 初始化您的 MMP 或分析 SDK(AppsFlyer、Adjust、PostHog、Branch) | 应用启动时,最先执行 | 等待 MMP 的 UID 回调,例如 `getAppsFlyerUID`。 | | 2a | `Adapty().activate(configuration: ...)` 并在配置中设置 `withCustomerUserId` | 应用启动时,在步骤 1 之后执行,前提是您已有 customer user ID | 推荐方式。不会创建匿名用户画像。 | | 2b | `Adapty().activate(configuration: ...)` 不设置 `withCustomerUserId` | 应用启动时,在步骤 1 之后执行,前提是您没有 customer user ID(或从不收集) | Adapty 会创建匿名用户画像。 | | 3 | 为每个 MMP 调用 `Adapty().setIntegrationIdentifier(key: ..., value: ...)` | 在步骤 2 之后、任何用户操作调用之前执行 | 必须执行,以确保 MMP ID 关联到正确的用户画像。 | | 4 | `await Adapty().identify(customerUserId)` | 在步骤 3 之后(若无 MMP 则在步骤 2 之后)、步骤 5 之前执行——仅适用于走路径 2b 且需要身份验证的情况 | 务必使用 `await`。在 `identify` 执行期间并发调用会产生 `#3006 profileWasChanged` 错误。 | | 5 | `getPaywall`(SDK v4 中为 `getFlow`)、`getPaywallProducts`、`restorePurchases`、`makePurchase`、`updateAttribution`、`updateProfile` | 若调用了 `identify`,则在步骤 4 之后执行;否则在步骤 3 之后(若无 MMP 则在步骤 2 之后)执行 | 这些调用需要一个稳定的用户画像。 | :::important 跳过这些步骤会导致回归用户丢失高级访问权限、用户画像中缺少 `appsflyer_id`,以及付费墙针对错误目标受众返回内容。 ::: ## Web2app 与 Web 漏斗安装 \{#web2app-and-web-funnel-installs\} 如果用户在 Web 结账页面(Stripe、Paddle)完成购买后再安装原生应用,设备首次调用 `activate()` 时会创建一个新的匿名用户画像,该画像不会与 Web 端的用户画像关联。如果你能在应用启动前(通过认证流程或安装来源追踪)获取到 customer user ID,可以直接将其传入 `activate()`。否则,在你调用 `identify("YOUR_USER_ID")` 并执行 `restorePurchases` 之前,设备端将无法看到 Web 端的购买记录。 关于每次 Web 结账时需要传递的元数据,请参阅: - [Stripe](stripe) - [Paddle](paddle) --- # File: flutter-optimize-paywall-fetching --- --- title: "在 Flutter SDK 中优化付费墙加载" description: "可靠地获取 Adapty 付费墙:Flutter 中的时机选择、缓存策略与备用方案。" --- 在 Flutter 中可靠地获取付费墙需要做到三点:快速渲染、返回针对目标受众的付费墙,以及在网络较慢时优雅降级。以下规则涵盖了实现这一目标所需的时机选择、缓存策略和备用方案。 :::tip 以下规则假设 `Adapty().activate()` 和 `Adapty().identify()` 已完成。请参阅 [Flutter SDK 的调用顺序](flutter-sdk-call-order)。 ::: 以下建议使用 v3 方法名称。在 SDK v4 中,`getPaywall` 已重命名为 `getFlow`,获取策略类型更名为 `AdaptyFlowFetchPolicy`——所有规则均适用,不受影响。 ## 规则与注意事项 \{#rules-and-pitfalls\} | 应该这样做 | 不要这样做 | 原因 | |---|---|---| | 只在即将展示时获取对应的版位。 | 在启动时并发预取所有版位。 | 批量预取会阻塞主线程,导致启动时出现黑屏。 | | 在归因数据有机会解析之后再调用 `getPaywall`——例如,在 `activate` 之后等待 1-2 秒,或等待 `didUpdateProfileStream` 触发后再调用。 | 在 `runApp` 之前的 `main()` 中调用 `getPaywall`。 | 此时归因数据尚未到位。付费墙会按默认目标受众进行解析,静默绕过市场细分和 ASA 个性化设置。 | | 为每个版位设置 `loadTimeout` 并配置[备用付费墙](fallback-paywalls)。 | 无限等待 `getPaywall` 返回。 | 没有超时设置,网络状况较差的用户会看到空白屏幕,直到网络恢复——或者直接关闭应用。 | 有关 `fetchPolicy` 和 `loadTimeout` 参数的说明,请参阅[获取付费墙和产品](fetch-paywalls-and-products-flutter);有关如何选择合适版位的信息,请参阅[版位](placements)。 ## 针对弱网环境进行优化 \{#tune-for-poor-connectivity\} 对于网络连接持续较差的市场(农村地区、交通途中、受路由问题影响的地区): - 除首次请求外,所有获取操作均设置 `fetchPolicy: AdaptyPaywallFetchPolicy.returnCacheDataElseLoad`。 - 在 Adapty 看板中为每个版位配置[备用付费墙](fallback-paywalls)。 - 将 `loadTimeout` 设置为 3–5 秒,并在超时触发时接受备用付费墙。 - 不要将付费墙的显示挂起在 `getProfile()` 上。独立调用 `getPaywall`,避免因获取用户画像缓慢而阻塞 UI。 --- # File: flutter-show-aa-targeted-paywall --- --- title: "在 Flutter SDK 中首次启动时显示 AA 定向付费墙" description: "在 Flutter 中首次启动时短暂等待 Apple Ads 归因数据后再显示付费墙,超时则回退到默认目标受众。使用 AdaptyProfile.appliedAttributionSources。" --- Apple Ads (AA) 归因数据在 `Adapty().activate()` 调用后会异步到达。首次启动时,归因数据通常尚未就位,因此如果你立即调用 `getPaywall`,Adapty 会基于默认目标受众处理该请求,导致 Apple Ads 用户无法看到针对 AA 细分设置的付费墙。与其先展示付费墙再替换,不如在显示任何内容前短暂等待 AA 归因数据:若归因在短暂超时内到达,则展示定向付费墙;否则展示默认目标受众的付费墙。`AdaptyProfile.appliedAttributionSources` 可告知你 AA 归因是否已生效。 ## 开始之前 \{#before-you-start\} 您需要: - Adapty Flutter SDK **3.17.0** 或更高版本。 - 在 Adapty 中为应用配置 Apple Ads。请参阅 [Apple Ads](apple-search-ads)。 ## 工作原理 \{#how-it-works\} 调用 `Adapty().activate()` 后,SDK 会在后台向 Apple 请求 Apple Ads 归因数据,并将结果转发至 Adapty 后端。当 AA 成为该用户画像的有效归因来源时,SDK 会向你的 `didUpdateProfileStream` 监听器推送更新后的 `AdaptyProfile`,其 `appliedAttributionSources` 列表中将包含 `AdaptyAttributionSource.appleAds`。 首次启动时,你需要处理以下两种情况: 1. **归因在超时时间内完成。** 调用 `getPaywall` — Adapty 根据 Apple Ads 目标受众解析请求,并返回对应的付费墙。 2. **超时时间先到。** 改为显示默认目标受众的付费墙,避免没有 Apple Ads 归因的用户白白等待。`getPaywallForDefaultAudience` 无需等待分群即可直接返回结果。 `appliedAttributionSources` 可以为空,这意味着: - 该用户画像的 Apple Ads 归因尚未处理完成,或 - 根本没有收到任何归因数据。 无论如何,`getPaywallForDefaultAudience` 都可以安全调用——它会直接返回默认受众的付费墙,不受用户画像状态影响。 :::important 等待仅发生在首次启动时。一旦 Apple Ads 归因数据记录完成,它就会永久保存在用户画像中。在此后的每次启动时,缓存的用户画像已经在 `appliedAttributionSources` 中包含了 `AdaptyAttributionSource.appleAds`,因此归因路径会立即解析,`getPaywall` 无需任何等待即可返回针对 Apple Ads 市场细分的付费墙。 ::: ## 实现 \{#implementation\} 首次启动时,等待 `AdaptyAttributionSource.appleAds` 回调并设置硬超时——如果 Apple Ads 归因数据始终未到达,这些用户仍然需要看到付费墙。 1. **激活 SDK。** 请参阅[安装并配置 Flutter SDK](sdk-installation-flutter)。 2. **订阅用户画像更新**,使用 `Adapty().didUpdateProfileStream.listen(…)`。如果尚未设置监听器,请参阅[监听订阅更新](flutter-check-subscription-status#listen-to-subscription-updates)。 3. **在 `appliedAttributionSources` 中监听 `AdaptyAttributionSource.appleAds`。** 当它出现时,使用 `getPaywall` 加载付费墙 —— Adapty 将返回 AA 细分的实验变体: ```dart final subscription = Adapty().didUpdateProfileStream.listen((profile) async { if (!profile.appliedAttributionSources.contains(AdaptyAttributionSource.appleAds)) return; final paywall = await Adapty().getPaywall(placementId: placementId); // present the segmented paywall, then cancel the subscription and the timer }); ``` `didUpdateProfileStream` 是广播流,不会重播历史事件,因此还需通过 `getProfile()` 单独检查当前用户画像。应用重启后,已存储的归因数据不会再次触发事件。 4. **与订阅同步启动一个 3–5 秒的计时器。** 如果计时器先于 `AdaptyAttributionSource.appleAds` 触发,则改用 `getPaywallForDefaultAudience` 加载默认受众付费墙。优先展示先完成的那个付费墙,并取消另一路请求,避免重复获取。为该版位配置[备用付费墙](flutter-use-fallback-paywalls),确保网络请求失败时用户也不会卡住。 ## 完整示例 \{#complete-example\} 下面的实现方案会让归因与超时同时竞争,并行预取默认受众付费墙,然后返回合适的付费墙。调用方只需等待单个函数,无需在调用处管理监听器或状态标志: - 如果归因在 `timeout` 内完成,则通过 `getPaywall` 返回分段付费墙。 - 如果 `timeout` 先到期,则通过 `getPaywallForDefaultAudience` 返回预取的默认受众付费墙。 ```dart title="apple_ads_paywall.dart" /// Returns the Apple Ads-segmented paywall if attribution is applied within /// [timeout], otherwise the default-audience paywall. Call after Adapty().activate(). Future getPaywallOrDefault({ required String placementId, required Duration timeout, }) { // Prefetch the default-audience paywall right away so the timeout path resolves // without an extra network round-trip. `getPaywallForDefaultAudience` skips the // wait for segmentation data. `..ignore()` keeps an unused prefetch from surfacing // as an unhandled error; the error still reaches the caller if this paywall wins. final defaultPaywall = Adapty().getPaywallForDefaultAudience(placementId: placementId)..ignore(); final completer = Completer(); late final StreamSubscription subscription; late final Timer timer; void resolve(Future paywall) { if (completer.isCompleted) return; timer.cancel(); subscription.cancel(); completer.complete(paywall); } void onProfile(AdaptyProfile profile) { if (profile.appliedAttributionSources.contains(AdaptyAttributionSource.appleAds)) { resolve(Adapty().getPaywall(placementId: placementId)); } } // Attribution path: react to profile updates as attribution is applied. subscription = Adapty().didUpdateProfileStream.listen(onProfile); // The stream is a broadcast stream and doesn't replay, so check the current // profile too — on relaunches attribution is already stored and won't re-emit. Adapty().getProfile().then(onProfile).ignore(); // Timeout path: fall back to the prefetched default-audience paywall. timer = Timer(timeout, () => resolve(defaultPaywall)); return completer.future; } ``` 在启动画面中调用,待其完成后再展示付费墙: ```dart try { final paywall = await getPaywallOrDefault( placementId: 'YOUR_PLACEMENT_ID', timeout: const Duration(seconds: 5), ); // present the paywall } on AdaptyError catch (adaptyError) { // handle the error or show a fallback paywall } catch (e) { // handle the error } ``` 调整 `timeout` 参数,设置用户在付费墙出现前最多等待的时间。大多数用户没有 Apple Ads 归因数据,因此他们会等待完整的超时时长——3 到 5 秒是一个合理的平衡点。如果有归因数据,通常会在应用启动后几秒内到达。 如果你的应用已经在监听 `didUpdateProfileStream`(例如用于[检查订阅状态](flutter-check-subscription-status#listen-to-subscription-updates)),则无需做任何修改。`didUpdateProfileStream` 是一个广播流,支持多个独立监听器互不干扰。 --- # File: flutter-test --- --- title: "在 Flutter SDK 中测试与发布" description: "了解如何在 Flutter 应用中使用 Adapty 检查订阅状态。" --- 如果您已经在 Flutter 应用中集成了 Adapty SDK,您可能需要测试一切是否设置正确,以及购买流程在 iOS 和 Android 平台上是否按预期运行。这包括使用 Apple 沙盒环境和 Google Play 测试环境,对 SDK 集成和实际购买流程进行测试。 ## 测试您的应用 \{#test-your-app\} 如需全面测试应用内购买,请参阅我们的平台专属测试指南:[iOS 测试指南](test-purchases-in-sandbox) 和 [Android 测试指南](testing-on-android)。 ## 准备发布 \{#prepare-for-release\} 在将应用提交到应用商店之前,请按照[发布检查清单](release-checklist)确认以下事项: - 应用商店连接和服务器通知已完成配置 - 购买流程顺畅完成,并已上报至 Adapty - 访问等级解锁与恢复功能正常运行 - 隐私与审核要求均已满足 --- # File: InvalidProductIdentifiers-flutter --- --- title: "Flutter SDK 中 Code-1000 noProductIDsFound 错误的修复方法" description: "解决在 Adapty 中管理订阅时出现的无效产品标识符错误。" --- 1000 错误码 `noProductIDsFound` 表示你在付费墙上请求的产品在 App Store 中均无法购买,尽管它们已列在其中。该错误有时会伴随 `InvalidProductIdentifiers` 警告一同出现。如果只出现警告而没有错误,可以忽略。 如果你遇到了 `noProductIDsFound` 错误,请按以下步骤排查: ## 第一步:检查 Bundle ID \{#step-2-check-bundle-id\} 1. 打开 [App Store Connect](https://appstoreconnect.apple.com/apps)。选择你的应用,进入 **General** → **App Information** 页面。 2. 在 **General Information** 子区块中复制 **Bundle ID**。 3. 在 Adapty 顶部菜单中打开 [**App settings** -> **iOS SDK** 标签页](https://app.adapty.io/settings/ios-sdk),将复制的值粘贴到 **Bundle ID** 字段中。 4. 返回 App Store Connect 中的 **App information** 页面,复制其中的 **Apple ID**。 5. 在 Adapty 看板的 [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) 页面中,将该 ID 粘贴到 **Apple app ID** 字段。 ## 第二步:检查产品 \{#step-3-check-products\} 1. 前往 **App Store Connect**,在左侧菜单中找到 [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions)。 2. 点击订阅组名称,在 **Subscriptions** 部分即可看到你的产品列表。 3. 确认你正在测试的产品状态为 **Ready to Submit**。 4. 将表格中的产品 ID 与 Adapty 看板 [**Products**](https://app.adapty.io/products) 标签页中的 ID 进行对比。如果 ID 不匹配,请复制表格中的产品 ID,并在 Adapty 看板中[创建产品](create-product)。 ## 第三步:检查产品可用性 \{#step-4-check-product-availability\} 1. 返回 **App Store Connect**,打开同一个 **Subscriptions** 页面。 2. 点击订阅组名称查看产品列表。 3. 选择你正在测试的产品。 4. 向下滚动到 **Availability** 部分,确认所有所需的国家和地区均已列出。 ## 第四步:检查产品价格 \{#step-5-check-product-prices\} 1. 再次前往 **App Store Connect** 的 **Monetization** → **Subscriptions** 页面。 2. 点击订阅组名称。 3. 选择你正在测试的产品。 4. 向下滚动到 **Subscription Pricing**,展开 **Current Pricing for New Subscribers** 部分。 5. 确认所有所需价格均已列出。 ## 第五步:检查应用付费状态、银行账户和税务表格是否有效 \{#step-5-check-app-paid-status-bank-account-and-tax-forms-are-active\} 1. 在 [**App Store Connect**](https://appstoreconnect.apple.com/) 首页,点击 **Business**。 2. 选择你的公司名称。 3. 向下滚动,确认 **Paid Apps Agreement**、**Bank Account** 和 **Tax forms** 均显示为 **Active**。 完成以上步骤后,你应该能够解决 `InvalidProductIdentifiers` 警告,并让产品在商店中正常上线。 ## 第六步:如果产品卡住,尝试重新创建 \{#step-6-recreate-the-product-if-its-stuck\} 第 1 至 5 步可能都检查无误——状态为 `Approved`、Bundle ID 匹配、API 密钥有效——但 SDK 仍然返回 `1000 noProductIDsFound`。这种情况下,产品可能卡在了 Apple 的注册表中。Apple 的产品注册表偶尔会出现这样的状态:产品在 App Store Connect 的界面中存在,但 StoreKit 的查询路径无法访问到它。 请在 App Store Connect 中删除该产品,然后用相同的产品 ID 重新创建。重新创建后,最多等待 24 小时以完成同步。 --- # File: cantMakePayments-flutter --- --- title: "修复 Flutter SDK 中的 Code-1003 cantMakePayment 错误" description: "解决在 Adapty 中管理订阅时出现的支付错误。" --- 1003 错误 `cantMakePayments` 表示该设备无法进行应用内购买。 如果你遇到了 `cantMakePayments` 错误,通常是由以下原因之一导致的: - 设备限制:该错误与 Adapty 无关。请参阅下方的修复方法。 - 观察者模式配置:`makePurchase` 方法与观察者模式不能同时使用。请参阅下方相关章节。 ## 问题:设备限制 \{#issue-device-restrictions\} | 问题 | 解决方案 | |---------------------------|---------------------------------------------------------| | 屏幕使用时间限制 | 在 [Screen Time](https://support.apple.com/en-us/102470) 中关闭应用内购买限制 | | 账户被暂停 | 联系 Apple 支持以解决账户问题 | | 地区限制 | 使用受支持地区的 App Store 账户 | ## 问题:同时使用观察者模式和 makePurchase \{#issue-using-both-observer-mode-and-makepurchase\} 如果你使用 `makePurchases` 来处理购买,则无需启用观察者模式。[观察者模式](observer-vs-full-mode) 仅在你自行实现购买逻辑时才需要使用。 因此,如果你正在使用 `makePurchase`,可以安全地从 SDK 激活代码中移除对观察者模式的启用。 --- # File: migration-to-flutter-sdk-v4 --- --- title: "将 Adapty Flutter SDK 迁移至 v. 4.0" description: "通过将付费墙 API 替换为 flow API,迁移至 Adapty Flutter SDK v4.0,兼容 Flow Builder 和 Paywall Builder。" --- Adapty Flutter SDK 4.0 引入了 flow,并相应地对付费墙 API 进行了重命名。新 API 同时兼容全新的 Flow Builder 和现有的 Paywall Builder——无需在 Adapty 看板侧做任何配置变更。 ## 快速参考 \{#quick-reference\} | v3 | v4 | |---|---| | `Adapty().getPaywall(placementId: id)` | `Adapty().getFlow(placementId: id)` | | `Adapty().getPaywallForDefaultAudience(placementId: id)` | `Adapty().getFlowForDefaultAudience(placementId: id)` | | `Adapty().getPaywallProducts(paywall: paywall)` | `Adapty().getPaywallProducts(flow: flow)` | | `Adapty().logShowPaywall(paywall: paywall)` | `Adapty().logShowFlow(flow: flow)` | | `AdaptyPaywall`(类型) | `AdaptyFlow` | | `AdaptyPaywallFetchPolicy`(类型) | `AdaptyFlowFetchPolicy` | | `AdaptyUI().createPaywallView(paywall: paywall)` | `AdaptyUI().createFlowView(flow: flow)` | | `AdaptyUIPaywallView`(类型) | `AdaptyUIFlowView` | | `AdaptyUIPaywallPlatformView`(widget) | `AdaptyUIFlowPlatformView` | | `AdaptyUI().presentPaywallView(view)` / `dismissPaywallView(view)` | `AdaptyUI().presentFlowView(view)` / `dismissFlowView(view)` | | `AdaptyUIPaywallsEventsObserver` | `AdaptyUIFlowsEventsObserver` | | `AdaptyUI().setPaywallsEventsObserver(observer)` | `AdaptyUI().setFlowsEventsObserver(observer)` | | `paywallViewDid*` 回调 | `flowViewDid*` 回调 | | `paywallViewDidFailRendering` | `flowViewDidReceiveError` | `AdaptyPaywallProduct` 保持其名称不变——产品仍属于某个 flow,`getPaywallProducts` 现在接受 `AdaptyFlow` 作为参数。获取 flow 时不再需要传入 `locale`。购买和用户画像相关的 API(`makePurchase`、`restorePurchases`、`getProfile`、`identify` 等)保持不变,视图方法 `present`、`dismiss` 和 `showDialog` 也同样不变。部分默认行为有所改动——详见[默认行为变更](#default-behavior-changes)。 ## 最低版本要求 \{#minimum-versions\} Adapty Flutter SDK 4.0 提高了最低要求: - **iOS 15.0** — 最低 iOS 部署目标,从 iOS 13.0 提升。 - **Xcode 26** 或更高版本 — 原生 iOS SDK 使用 Swift tools 6.2。 - **Flutter 3.32.0**(Dart 3.8.0)或更高版本。 ## 安装 \{#installation\} ### 更新软件包 \{#update-the-package\} 安装哪个软件包取决于你的应用是否使用了儿童模式。 对于大多数应用,在 `pubspec.yaml` 中将 `adapty_flutter` 更新至 v4.0: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: 4.0.0 ``` 如果你的应用使用了儿童模式,请改为指定 `adapty_flutter_kids`: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter_kids: 4.0.0 ``` 此**独立软件包**移除了 IDFA 和广告追踪相关代码,以符合 App Store 的要求。请将 Dart 导入路径更新为 `package:adapty_flutter_kids/adapty_flutter.dart`。除此之外,迁移步骤与常规软件包完全相同。 Kids Mode 还需要你在 Adapty 看板中禁用 IP 地址收集——完整配置步骤请参阅 [Kids Mode](kids-mode-flutter)。 ### iOS:原生 SDK 现在通过 Swift Package Manager 分发 \{#ios-native-sdks-now-come-through-swift-package-manager\} [CocoaPods 的 spec 仓库将于 2026 年 12 月进入只读模式](https://blog.cocoapods.org/CocoaPods-Specs-Repo/),因此从 v4 开始,原生 iOS SDK **不再通过 CocoaPods 分发** — 插件仅通过 **Swift Package Manager** 拉取依赖。 如果你使用的是 Flutter 3.32–3.43,请执行以下命令一次性启用 Swift Package Manager 支持: ```bash flutter config --enable-swift-package-manager ``` Flutter 3.44 及更高版本默认启用 Swift Package Manager,无需额外操作。 ## 获取流程 \{#fetching-flows\} ### getPaywall → getFlow 返回类型从 `AdaptyPaywall` 变更为 `AdaptyFlow`,并且不再需要传入 `locale` 参数——渲染 flow 时会自动解析本地化;对于自定义付费墙,所有已配置的语言版本将通过 `flow.remoteConfigs` 返回: ```diff showLineNumbers - final paywall = await Adapty().getPaywall(placementId: 'YOUR_PLACEMENT_ID', locale: 'en'); + final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); ``` `getPaywallForDefaultAudience` 也以同样的方式重命名: ```diff showLineNumbers - final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID', locale: 'en'); + final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); ``` fetch policy 类型从 `AdaptyPaywallFetchPolicy` 重命名为 `AdaptyFlowFetchPolicy`;其选项(`reloadRevalidatingCacheData`、`returnCacheDataElseLoad`、`returnCacheDataIfNotExpiredElseLoad`)保持不变。 ### getPaywallProducts(paywall) → getPaywallProducts(flow) `getPaywallProducts` 保持名称不变,但现在通过 `flow` 参数接收 `AdaptyFlow`: ```diff showLineNumbers - final products = await Adapty().getPaywallProducts(paywall: paywall); + final products = await Adapty().getPaywallProducts(flow: flow); ``` ## 数据模型 \{#data-model\} `getFlow` 返回的是 `AdaptyFlow` 而非 `AdaptyPaywall`,对象结构也有所变化: | v3 `AdaptyPaywall` 成员 | v4 `AdaptyFlow` 成员 | 操作 | |---|---|---| | `remoteConfig`(单个,可为空) | `remoteConfigs`(列表) | 一个流程为每种已配置的语言各携带一份远程配置。`remoteConfig` getter 仍然存在,返回第一个条目;若需指定语言,可按 `locale` 在 `remoteConfigs` 中查找。 | | `productIdentifiers` | `productIdentifiers` | 保留,但现在会汇总流程中所有付费墙变体的标识符。各变体的标识符存放在 `flow.paywalls[i].productIdentifiers`。 | | `hasViewConfiguration` | `hasViewConfiguration` | 不变。 | | `placementId`(已弃用) | 已移除 | 使用 `flow.placement.id`。 | | `revision`(已弃用) | 已移除 | 使用 `flow.placement.revision`。 | | `vendorProductIds`(已弃用) | 已移除 | 使用 `productIdentifiers`。 | | _(新增)_ | `paywalls`(`AdaptyFlowPaywall` 列表) | 每个条目对应流程中的一个付费墙变体,包含各自的 `name`、`variationId` 和 `productIdentifiers`。 | `AdaptyPaywallViewConfiguration` 不再对外暴露——视图配置现在是不透明的。请删除所有对该类型的引用。 ## Web 付费墙方法 \{#web-paywall-methods\} `openWebPaywall` 和 `createWebPaywallUrl` 的名称保持不变,但 `paywall` 参数现在接受 `AdaptyFlowPaywall`(流程变体),而不再是 `AdaptyPaywall`。你仍然可以传入 `AdaptyPaywallProduct`。 ```diff showLineNumbers final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); - await Adapty().openWebPaywall(paywall: paywall); + if (flow.paywalls.isNotEmpty) { + await Adapty().openWebPaywall(paywall: flow.paywalls[0]); + } ``` ## 追踪流程视图 \{#tracking-flow-views\} ### logShowPaywall → logShowFlow `logShowPaywall` 已重命名为 `logShowFlow`,现在接收一个 `AdaptyFlow` 参数。事件仍会记录在相同的变体下,因此现有的转化漏斗和 A/B 测试数据图表无需在看板中做任何更改即可继续正常使用。 ```diff showLineNumbers - await Adapty().logShowPaywall(paywall: paywall); + await Adapty().logShowFlow(flow: flow); ``` 与 v3 相同,当通过[流程编辑工具](adapty-flow-builder)或[付费墙编辑工具](adapty-paywall-builder)渲染流程或付费墙时,无需手动调用此方法——Adapty 会自动追踪这些页面的展示。 ## 显示流程 \{#displaying-flows\} ### createPaywallView → createFlowView 将方法重命名,并通过 `flow` 参数传入 `AdaptyFlow`。其他参数(`loadTimeout`、`preloadProducts`、`customTags`、`customTimers`、`customAssets`、`productPurchaseParams`)保持不变,视图方法 `present`、`dismiss` 和 `showDialog` 同样不变: ```diff showLineNumbers - final view = await AdaptyUI().createPaywallView(paywall: paywall); + final view = await AdaptyUI().createFlowView(flow: flow); await view.present(); ``` ### AdaptyUIPaywallView → AdaptyUIFlowView 视图类型已重命名。其已废弃的 `paywallVariationId` 属性已移除——请改用 `variationId`: ```diff showLineNumbers - void flowViewDidAppear(AdaptyUIPaywallView view) { + void flowViewDidAppear(AdaptyUIFlowView view) { ``` ### AdaptyUIPaywallPlatformView → AdaptyUIFlowPlatformView 如果你将视图作为 widget 嵌入到 widget 树中,请重命名它并传入 `flow` 参数。事件回调(`onDidAppear`、`onDidFinishPurchase` 等)名称保持不变: ```diff showLineNumbers - AdaptyUIPaywallPlatformView( - paywall: paywall, + AdaptyUIFlowPlatformView( + flow: flow, onDidFinishPurchase: (view, product, purchaseResult) { /* … */ }, ) ``` :::note 使用 `createFlowView` 创建的流程视图只能使用一次:调用 `dismiss()` 后,该视图会从内存中释放,无法再次展示——如需再次展示流程,请重新调用 `createFlowView`。 ::: ## 处理事件 \{#handling-events\} 观察者类已从 `AdaptyUIPaywallsEventsObserver` 更名为 `AdaptyUIFlowsEventsObserver`,其注册方法已从 `setPaywallsEventsObserver` 更名为 `setFlowsEventsObserver`,所有 `paywallViewDid*` 回调也已更名为 `flowViewDid*`: ```diff showLineNumbers - class MyObserver extends AdaptyUIPaywallsEventsObserver { + class MyObserver extends AdaptyUIFlowsEventsObserver { @override - void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { + void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { // … } } - AdaptyUI().setPaywallsEventsObserver(this); + AdaptyUI().setFlowsEventsObserver(this); ``` 现在有三个回调是**必须实现的**——缺少它们将导致编译错误: - **`flowViewDidFinishPurchase`**: 在 v3 中为可选项,默认行为是购买后关闭视图。现在由你决定后续操作:继续流程或调用 `view.dismiss()`。 - **`flowViewDidFinishRestore`**: 必填项,与 v3 相同。 - **`flowViewDidReceiveError`**: 替代 `paywallViewDidFailRendering`,同时还可接收其他视图错误。 另外两个小改动: - `setFlowsEventsObserver`(以及 `setOnboardingsEventsObserver`)现在接受 `null` 来解除之前设置的观察者,SDK 不再持有对它的引用。 - 新增的可选回调 `flowViewDidReceiveAnalyticEvent` 用于接收 flow 中的自定义分析事件。目前 flow 尚未向你的代码发送此类事件,因此无需实现该回调。 v4 还新增了一些可按需启用的功能: - `AdaptyUI().setObserverModeResolver(...)` 配合 `AdaptyUIObserverModeResolver` — 在 SDK 以[观察者模式](implement-observer-mode-flutter)运行时,处理从流程发起的购买和恢复操作。此前该功能仅在原生 iOS 和 Android SDK 中可用。请参阅[在观察者模式下展示流程](flutter-present-flows-in-observer-mode)。 - `AdaptyUI().setSystemRequestsHandler(...)` 配合 `AdaptyUISystemRequestsHandler` — 用于处理流程中的系统请求(系统权限提示和 App Store 评价请求)。目前流程尚未触发这些请求,因此无需注册处理器。 ## 已移除的 API \{#removed-apis\} 以下符号在 3.x 中已被标记为弃用,并在 v4 中正式移除: ### setFallbackPaywalls → setFallback ```diff showLineNumbers - await Adapty().setFallbackPaywalls(assetId); + await Adapty().setFallback(assetId); ``` ### withIdfaCollectionDisabled → withAppleIdfaCollectionDisabled ```diff showLineNumbers configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') - ..withIdfaCollectionDisabled(true), + ..withAppleIdfaCollectionDisabled(true), ``` ### 其他已移除的成员 \{#other-removed-members\} - **`AdaptyPurchaseResultSuccess.jwsTransaction`**:请使用 `appleJwsTransaction`。 - **`AdaptyUIFlowView.paywallVariationId`**:请使用 `variationId`。 - **`AdaptyUIObserver` 和 `AdaptyUI().setObserver(...)`**:请使用 `AdaptyUIFlowsEventsObserver` 和 `setFlowsEventsObserver(...)`。 ## 默认行为变更 \{#default-behavior-changes\} 这些变更不会导致编译错误,请在运行时进行测试: - **成功购买**:在 v3 中,默认的 `paywallViewDidFinishPurchase` 会关闭视图。在 v4 中,`flowViewDidFinishPurchase` 是必须实现的,且没有默认行为——如果你希望关闭视图,需要自行处理。 - **Android 系统返回按钮**:默认情况下,它不再关闭流程。该操作会以 `AndroidSystemBackAction` 的形式传递给 `flowViewDidPerformAction`——如果你希望返回按钮关闭流程,请在此处处理。 - **URL 打开**:默认的 `flowViewDidPerformAction` 现在会通过 `OpenUrlAction` 以原生方式打开 URL(遵循看板中的应用内或外部浏览器设置),同时在 `CloseAction` 时关闭视图。如需自行处理 URL,请覆盖此回调。 - **视图错误**:`flowViewDidReceiveError` 是必须实现的,是否关闭视图取决于你的实现。如果你的 v3 集成依赖于渲染错误时自动关闭视图的行为,请在此回调中调用 `view.dismiss()`。 - **视图生命周期**:关闭流程或用户引导视图后,该视图会从内存中释放。已关闭的视图无法再次显示——请重新创建一个新视图。 ## 用户引导 API 已弃用 \{#onboarding-api-deprecation\} 旧版用户引导 API 已在 v4.0 中弃用,请改用 [Flow Builder](adapty-flow-builder)。该 API 目前仍可正常使用,IDE 会通过 `@Deprecated` 注解标记已弃用的符号,不会产生任何运行时警告。这些符号将在未来版本中移除,请提前规划将你的用户引导迁移至 Flow Builder。 已废弃的符号:`getOnboarding`、`getOnboardingForDefaultAudience`、`createOnboardingView`、`presentOnboardingView`、`dismissOnboardingView`、`setOnboardingsEventsObserver`、`AdaptyOnboarding`、`AdaptyUIOnboardingView`、`AdaptyUIOnboardingPlatformView`、`AdaptyUIOnboardingsEventsObserver`,以及用户引导的状态、输入和分析模型。 --- # File: flutter-migration-guide-310 --- --- title: "迁移指南:Flutter Adapty SDK 3.10.0" description: "" --- Adapty SDK 3.10.0 是一个重大版本发布,带来了一些改进,但可能需要您执行以下迁移步骤: 1. 更新 `makePurchase` 方法,使用 `AdaptyPurchaseParameters` 替代单独的参数。 2. 在 `AdaptyPaywall` 模型中,将 `vendorProductIds` 替换为 `productIdentifiers`。 ## 更新 makePurchase 方法 \{#update-makepurchase-method\} `makePurchase` 方法现在使用 `AdaptyPurchaseParameters` 替代原有的 `subscriptionUpdateParams` 和 `isOfferPersonalized` 参数。这提供了更好的类型安全性,并为未来扩展购买参数提供了便利。 ```diff showLineNumbers - final purchaseResult = await adapty.makePurchase( - product: product, - subscriptionUpdateParams: subscriptionUpdateParams, - isOfferPersonalized: true, - ); + final parameters = AdaptyPurchaseParametersBuilder() + ..setSubscriptionUpdateParams(subscriptionUpdateParams) + ..setIsOfferPersonalized(true) + ..setObfuscatedAccountId('your-account-id') + ..setObfuscatedProfileId('your-profile-id'); + final purchaseResult = await adapty.makePurchase( + product: product, + parameters: parameters.build(), + ); ``` 如果不需要额外参数,可以直接使用: ```dart showLineNumbers final purchaseResult = await adapty.makePurchase( product: product, ); ``` ## 更新 AdaptyPaywall 模型的用法 \{#update-adaptypaywall-model-usage\} `vendorProductIds` 属性已被弃用,推荐使用 `productIdentifiers`。新属性返回 `AdaptyProductIdentifier` 对象而非简单字符串,提供了更结构化的产品信息。 ```diff showLineNumbers - paywall.vendorProductIds.map((vendorId) => - ListTextTile(title: vendorId) - ).toList() + paywall.productIdentifiers.map((productId) => + ListTextTile(title: productId.vendorProductId) + ).toList() ``` `AdaptyProductIdentifier` 对象通过 `vendorProductId` 属性提供对供应商产品 ID 的访问,在保持原有功能的同时,为未来的功能增强提供了更好的结构支持。 ## 向后兼容性 \{#backward-compatibility\} 两项更改均保持向后兼容: - `makePurchase` 中的旧参数已被弃用,但仍然可以正常使用 - `vendorProductIds` 属性已被弃用,但仍然可以访问 - 现有代码将继续正常运行,但您会看到弃用警告 我们建议更新您的代码以使用新的 API,以确保未来的兼容性,并充分利用改进后的类型安全性和可扩展性。 --- # File: flutter-migration-guide-38 --- --- title: "迁移 Adapty Flutter SDK 至 v3.8" description: "迁移至 Adapty Flutter SDK v3.8,获得更好的性能和新的货币化功能。" --- Adapty SDK 3.8.0 是一个重要版本,带来了一些改进,但可能需要你执行若干迁移步骤。 1. 更新 observer 类名和方法名。 2. 更新备用付费墙的方法名。 3. 更新事件处理方法中的 view 类名。 ## 更新观察者类和方法名称 \{#update-observer-class-and-method-names\} 观察者类及其注册方法已重命名: ```diff showLineNumbers - class MyObserver extends AdaptyUIObserver { + class MyObserver extends AdaptyUIPaywallsEventsObserver { @override void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) { // Handle action } } // Register observer - AdaptyUI().setObserver(this); + AdaptyUI().setPaywallsEventsObserver(this); ``` ## 更新备用付费墙方法名称 \{#update-fallback-paywalls-method-name\} 设置备用付费墙的方法已简化: ```diff showLineNumbers try { - await Adapty.setFallbackPaywalls(assetId); + await Adapty.setFallback(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## 在事件处理方法中更新视图类名 \{#update-view-class-name-in-event-handling-methods\} 所有事件处理方法现在使用新的 `AdaptyUIPaywallView` 类,替代原来的 `AdaptyUIView`: ```diff showLineNumbers - void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) + void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) - void paywallViewDidSelectProduct(AdaptyUIView view, AdaptyPaywallProduct product) + void paywallViewDidSelectProduct(AdaptyUIPaywallView view, AdaptyPaywallProduct product) - void paywallViewDidStartPurchase(AdaptyUIView view, AdaptyPaywallProduct product) + void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) - void paywallViewDidFinishPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyProfile profile) + void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyProfile profile) - void paywallViewDidFailPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyError error) + void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) - void paywallViewDidFinishRestore(AdaptyUIView view, AdaptyProfile profile) + void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) - void paywallViewDidFailRestore(AdaptyUIView view, AdaptyError error) + void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) - void paywallViewDidFailLoadingProducts(AdaptyUIView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) + void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) - void paywallViewDidFailRendering(AdaptyUIView view, AdaptyError error) + void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) ``` --- # File: migration-to-flutter-sdk-34 --- --- title: "迁移 Adapty Flutter SDK 至 v3.4" description: "迁移至 Adapty Flutter SDK v3.4,获得更好的性能和全新的变现功能。" --- Adapty SDK 3.4.0 是一个主要版本,引入了需要您进行迁移操作的改进。 ## 更新备用付费墙文件 \{#update-fallback-paywall-files\} 更新您的备用付费墙文件以确保与新 SDK 版本的兼容性: 1. 从 Adapty 看板[下载更新后的备用付费墙文件](fallback-paywalls)。 2. 用新文件[替换移动应用中现有的备用付费墙](flutter-use-fallback-paywalls)。 ## 更新 Observer Mode 的实现方式 \{#update-implementation-of-observer-mode\} 如果你正在使用 Observer Mode,请确保更新其实现方式。 此前,向 Adapty 上报交易时使用的是不同的方法。在新版本中,Android 和 iOS 应该统一使用 `reportTransaction` 方法来上报每笔交易,确保 Adapty 能够识别它。如果使用了付费墙,请传入 variation ID,以便将该交易与付费墙关联起来。 :::warning **不要跳过交易上报!** 如果不调用 `reportTransaction`,Adapty 将无法识别该交易,它不会出现在分析数据中,也不会被发送到集成渠道。 ::: ```diff showLineNumbers - // every time when calling transaction.finish() - if (Platform.isAndroid) { - try { - await Adapty().restorePurchases(); - } on AdaptyError catch (adaptyError) { - // handle the error - } catch (e) { - } - } try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` --- # File: migration-to-flutter330 --- --- title: "迁移 Adapty Flutter SDK 至 v3.3" description: "迁移至 Adapty Flutter SDK v3.3,获得更佳性能与全新变现功能。" --- Adapty SDK 3.3.0 是一个重大版本更新,带来了一些改进,但可能需要你执行一些迁移步骤。 --- title: "迁移指南:从 Adapty iOS SDK v2.x 迁移至 v3.x" description: "将您的 iOS 应用从 Adapty SDK v2.x 无缝迁移至 v3.x,遵循我们的分步指南,了解关键变更,轻松完成集成升级。" metadataTitle: "iOS SDK v2.x 至 v3.x 迁移指南 | Adapty 文档" --- Adapty SDK v3.x 是一个重大版本更新,包含多项破坏性变更。本指南重点介绍这些变更,帮助您顺利完成迁移。 ## 公共 API 变更 \{#public-api-changes\} ### AdaptyUI 更名 \{#adaptui-renamed\} `AdaptyUI` 已重命名为 `AdaptyUI`——抱歉,只是开个玩笑😄 实际上没有变化,但我们确实针对新版付费墙编辑工具对 `AdaptyUI` 进行了大量更新。 ### 获取付费墙和产品 \{#fetching-paywalls-and-products\} 在 Adapty SDK v3.x 中,`getPaywall` 方法的加载策略机制已更新。 ```swift do { let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") } catch { // handle the error } ``` ```swift Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` 在 v3.x 中,我们移除了 `PaywallFetchPolicy`,转而采用更简洁的方案:默认加载已缓存的数据(若无缓存则从远端拉取),同时开放了直接从远端强制拉取的选项。 ```swift // Default: loads cached data or fetches from remote do { let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") } catch { // handle the error } // Force fetch from remote do { let paywall = try await Adapty.getPaywall( placementId: "YOUR_PLACEMENT_ID", locale: "en", fetchPolicy: .reloadRevalidatingCacheData ) } catch { // handle the error } ``` ```swift Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en", fetchPolicy: .returnCacheDataElseLoad) { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en", fetchPolicy: .reloadRevalidatingCacheData) { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` ### 备用付费墙 \{#fallback-paywalls\} 在 v3.x 中,设置备用付费墙的方法已更新。 ```swift // Provide fallback paywalls from a file URL guard let url = Bundle.main.url(forResource: "fallback_paywalls", withExtension: "json") else { // handle the error return } do { try await Adapty.setFallbackPaywalls(fileURL: url) } catch { // handle the error } ``` ```swift guard let urlPath = Bundle.main.path(forResource: "fallback_paywalls", ofType: "json"), let paywallsData = FileManager.default.contents(atPath: urlPath) else { // handle the error return } Adapty.setFallbackPaywalls(paywallsData) { error in if let error = error { // handle the error } } ``` ### 移除 `getProductsIntroductoryOfferEligibility` \{#removal-of-getproductsintroductoryoffereligibility\} `getProductsIntroductoryOfferEligibility` 方法已从 SDK 中移除。新用户优惠资格信息现在直接包含在 `AdaptyPaywallProduct` 对象中。 ```swift do { let products = try await Adapty.getPaywallProducts(paywall: paywall) for product in products { let subscriptionOffer = product.subscriptionOffer // Use the offer directly } } catch { // handle the error } ``` ```swift Adapty.getPaywallProducts(paywall: paywall) { result in switch result { case let .success(products): let productIds = products.map { $0.vendorProductId } Adapty.getProductsIntroductoryOfferEligibility(vendorProductIds: productIds) { result in switch result { case let .success(eligibilities): // use eligibilities case let .failure(error): // handle the error } } case let .failure(error): // handle the error } } ``` #### `AdaptyPaywallProduct` 变更 \{#adaptpaywallproduct-changes\} `AdaptyPaywallProduct` 的结构已更新。在 v3.x 中,`subscriptionOffer` 属性会自动包含相应的优惠(新用户优惠或促销活动),无需额外的资格检查。 | v2.x 属性 | v3.x 属性 | | :-------------------------- | :------------------------------------- | | `introductoryDiscount` | 现已合并至 `subscriptionOffer` | | `promotionalDiscount` | 现已合并至 `subscriptionOffer` | | `promotionalOfferId` | 现已合并至 `subscriptionOffer` | `subscriptionOffer` 属性返回适合当前用户的最优优惠:若用户符合新用户优惠资格,则返回新用户优惠;若存在促销活动,则返回促销活动;其他情况返回 `nil`。 ### 第三方集成配置变更 \{#integration-configuration-changes\} 在 v3.x 中,多个第三方集成的配置方式已更新,从专有方法迁移至通用的 `setIntegrationIdentifier` 方法。 #### Adjust \{#adjust\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "adjust_device_id", value: adjustDeviceId ) } catch { // handle the error } ``` ```swift Adapty.setAdjustId(adjustId) ``` #### AirBridge \{#airbridge\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "airbridge_device_id", value: airbridgeDeviceId ) } catch { // handle the error } ``` ```swift Adapty.setAirbridgeId(airbridgeDeviceId) ``` #### Amplitude \{#amplitude\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "amplitude_user_id", value: amplitudeUserId ) try await Adapty.setIntegrationIdentifier( key: "amplitude_device_id", value: amplitudeDeviceId ) } catch { // handle the error } ``` ```swift Adapty.setAmplitudeUserId(amplitudeUserId) Adapty.setAmplitudeDeviceId(amplitudeDeviceId) ``` #### AppMetrica \{#appmetrica\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "appmetrica_profile_id", value: appMetricaProfileId ) try await Adapty.setIntegrationIdentifier( key: "appmetrica_device_id", value: appMetricaDeviceId ) } catch { // handle the error } ``` ```swift Adapty.setAppMetricaProfileId(appMetricaProfileId) Adapty.setAppMetricaDeviceId(appMetricaDeviceId) ``` #### AppsFlyer \{#appsflyer\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "appsflyer_id", value: appsFlyerId ) } catch { // handle the error } ``` ```swift Adapty.setAppsFlyerId(appsFlyerId) ``` #### Branch \{#branch\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "branch_id", value: branchId ) } catch { // handle the error } ``` ```swift Adapty.setBranchId(branchId) ``` #### Facebook Ads \{#facebook-ads\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "facebook_anonymous_id", value: facebookAnonymousId ) } catch { // handle the error } ``` ```swift Adapty.setFacebookAnonymousId(facebookAnonymousId) ``` #### Firebase 与 Google Analytics \{#firebase-and-google-analytics\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "firebase_app_instance_id", value: firebaseAppInstanceId ) } catch { // handle the error } ``` ```swift Adapty.setFirebaseAppInstanceId(firebaseAppInstanceId) ``` #### Mixpanel \{#mixpanel\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "mixpanel_user_id", value: mixpanelUserId ) } catch { // handle the error } ``` ```swift Adapty.setMixpanelUserId(mixpanelUserId) ``` #### OneSignal \{#onesignal\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "one_signal_player_id", value: oneSignalPlayerId ) try await Adapty.setIntegrationIdentifier( key: "one_signal_subscription_id", value: oneSignalSubscriptionId ) } catch { // handle the error } ``` ```swift Adapty.setOneSignalPlayerId(oneSignalPlayerId) ``` #### Pushwoosh \{#pushwoosh\} ```swift do { try await Adapty.setIntegrationIdentifier( key: "pushwoosh_hwid", value: pushwooshHWID ) } catch { // handle the error } ``` ```swift Adapty.setPushwooshHWID(pushwooshHWID) ``` ### 观察者模式 \{#observer-mode\} 在 v3.x 中,观察者模式的实现方式已更新。 ```swift // Enable Observer mode during configuration let builder = AdaptyConfiguration .builder(withAPIKey: "YOUR_API_KEY") .with(observerMode: true) Adapty.activate(with: builder.build()) // Report transactions in Observer mode do { try await Adapty.reportTransaction(transaction) } catch { // handle the error } ``` ```swift // Enable Observer mode during configuration Adapty.activate("YOUR_API_KEY", observerMode: true) // Report transactions in Observer mode Adapty.setTransactionVariationId(transaction, variationId: variationId) { error in if let error = error { // handle the error } } ``` ## 更新备用付费墙的提供方式 \{#update-method-for-providing-fallback-paywalls\} 此前,该方法需要以 JSON 字符串(`jsonString`)的形式传入备用付费墙,现在改为传入本地备用文件的路径(`assetId`)。 ```diff showLineNumbers import 'dart:async' show Future; import 'dart:io' show Platform; -import 'package:flutter/services.dart' show rootBundle; -final filePath = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; -final jsonString = await rootBundle.loadString(filePath); +final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; try { - await adapty.setFallbackPaywalls(jsonString); + await adapty.setFallbackPaywalls(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 完整的代码示例请参阅[使用备用付费墙](flutter-use-fallback-paywalls)页面。 ## 移除 `getProductsIntroductoryOfferEligibility` 方法 \{#remove-getproductsintroductoryoffereligibility-method\} 在 Adapty iOS SDK 3.3.0 之前,无论用户是否具有资格,产品对象始终包含优惠信息。您需要在使用优惠之前手动检查资格。 现在,只有当用户具有资格时,产品对象才会包含优惠信息。这意味着您不再需要检查资格——如果存在优惠,则用户具有资格。 ## 更新第三方集成 SDK 配置 \{#update-third-party-integration-sdk-configuration\} 为确保集成在 Adapty Flutter SDK 3.3.0 及更高版本中正常工作,请按照以下各节所述更新您的 SDK 配置。 ### Adjust 按照以下方式更新您的移动应用代码。完整代码示例请参阅 [Adjust 集成的 SDK 配置](adjust#connect-your-app-to-adjust)。 ```diff showLineNumbers import 'package:adjust_sdk/adjust.dart'; import 'package:adjust_sdk/adjust_config.dart'; try { final adid = await Adjust.getAdid(); if (adid == null) { // handle the error } + await Adapty().setIntegrationIdentifier( + key: "adjust_device_id", + value: adid, + ); final attributionData = await Adjust.getAttribution(); var attribution = Map(); if (attributionData.trackerToken != null) attribution['trackerToken'] = attributionData.trackerToken!; if (attributionData.trackerName != null) attribution['trackerName'] = attributionData.trackerName!; if (attributionData.network != null) attribution['network'] = attributionData.network!; if (attributionData.adgroup != null) attribution['adgroup'] = attributionData.adgroup!; if (attributionData.creative != null) attribution['creative'] = attributionData.creative!; if (attributionData.clickLabel != null) attribution['clickLabel'] = attributionData.clickLabel!; if (attributionData.costType != null) attribution['costType'] = attributionData.costType!; if (attributionData.costAmount != null) attribution['costAmount'] = attributionData.costAmount!.toString(); if (attributionData.costCurrency != null) attribution['costCurrency'] = attributionData.costCurrency!; if (attributionData.fbInstallReferrer != null) attribution['fbInstallReferrer'] = attributionData.fbInstallReferrer!; - Adapty().updateAttribution( - attribution, - source: AdaptyAttributionSource.adjust, - networkUserId: adid, - ); + await Adapty().updateAttribution(attribution, source: "adjust"); } catch (e) { // handle the error } on AdaptyError catch (adaptyError) { // handle the error } ``` ### AirBridge 按照以下说明更新您的移动应用代码。完整代码示例请参阅 [AirBridge 集成的 SDK 配置](airbridge#connect-your-app-to-airbridge)。 ```diff showLineNumbers import 'package:airbridge_flutter_sdk/airbridge_flutter_sdk.dart'; final deviceUUID = await Airbridge.state.deviceUUID; try { - final builder = AdaptyProfileParametersBuilder() - ..setAirbridgeDeviceId(deviceUUID); - await Adapty().updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "airbridge_device_id", + value: deviceUUID, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ### Amplitude 按如下所示更新您的移动应用代码。完整代码示例请参阅 [Amplitude 集成的 SDK 配置](amplitude#sdk-configuration)。 ```diff showLineNumbers import 'package:amplitude_flutter/amplitude.dart'; final Amplitude amplitude = Amplitude.getInstance(instanceName: "YOUR_INSTANCE_NAME"); final deviceId = await amplitude.getDeviceId(); final userId = await amplitude.getUserId(); try { - final builder = AdaptyProfileParametersBuilder() - ..setAmplitudeDeviceId(deviceId) - ..setAmplitudeUserId(userId); - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "amplitude_user_id", + value: userId, + ); + await Adapty().setIntegrationIdentifier( + key: "amplitude_device_id", + value: deviceId, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ### AppMetrica 按照以下示例更新你的移动应用代码。完整代码示例请参阅 [AppMetrica 集成的 SDK 配置](appmetrica#sdk-configuration)。 ```diff showLineNumbers import 'package:appmetrica_plugin/appmetrica_plugin.dart'; final deviceId = await AppMetrica.deviceId; if (deviceId != null) { try { - final builder = AdaptyProfileParametersBuilder() - ..setAppmetricaDeviceId(deviceId) - ..setAppmetricaProfileId("YOUR_ADAPTY_CUSTOMER_USER_ID"); - - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "appmetrica_device_id", + value: deviceId, + ); + await Adapty().setIntegrationIdentifier( + key: "appmetrica_profile_id", + value: "YOUR_ADAPTY_CUSTOMER_USER_ID", + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } } ``` ### AppsFlyer 按照以下说明更新您的移动应用代码。完整代码示例请参阅 [AppsFlyer 集成的 SDK 配置](appsflyer#connect-your-app-to-appsflyer)。 ```diff showLineNumbers import 'package:appsflyer_sdk/appsflyer_sdk.dart'; AppsflyerSdk appsflyerSdk = AppsflyerSdk(); appsflyerSdk.onInstallConversionData((data) async { try { final appsFlyerUID = await appsFlyerSdk.getAppsFlyerUID(); - await Adapty().updateAttribution( - data, - source: AdaptyAttributionSource.appsflyer, - networkUserId: appsFlyerUID, - ); + await Adapty().setIntegrationIdentifier( + key: "appsflyer_id", + value: appsFlyerUID, + ); + + await Adapty().updateAttribution(data, source: "appsflyer"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } }); appsflyerSdk.initSdk( registerConversionDataCallback: true, registerOnAppOpenAttributionCallback: true, registerOnDeepLinkingCallback: true, ); ``` ### Branch 按照以下方式更新您的移动应用代码。完整代码示例请参阅 [Branch 集成的 SDK 配置](branch#connect-your-app-to-branch)。 ```diff showLineNumbers FlutterBranchSdk.initSession().listen((data) async { try { + await Adapty().setIntegrationIdentifier( + key: "branch_id", + value: , + ); - await Adapty().updateAttribution(data, source: AdaptyAttributionSource.branch); + await Adapty().updateAttribution(data, source: "branch"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ); ``` ### Firebase 与 Google Analytics \{#firebase-and-google-analytics\} 按照以下示例更新你的移动应用代码。完整代码示例请参阅 [Firebase 与 Google Analytics 集成的 SDK 配置](firebase-and-google-analytics)。 ```diff showLineNumbers final appInstanceId = await FirebaseAnalytics.instance.appInstanceId; try { - final builder = AdaptyProfileParametersBuilder() - ..setFirebaseAppInstanceId(appInstanceId); - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "firebase_app_instance_id", + value: appInstanceId, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ### Mixpanel 按照以下方式更新您的移动应用代码。完整代码示例请参阅 [Mixpanel 集成的 SDK 配置](mixpanel#sdk-configuration)。 ```diff showLineNumbers final mixpanel = await Mixpanel.init("Your Token", trackAutomaticEvents: true); final distinctId = await mixpanel.getDistinctId(); try { - final builder = AdaptyProfileParametersBuilder() - ..setMixpanelUserId(distinctId); - await Adapty().updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "mixpanel_user_id", + value: distinctId, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ### OneSignal 按照以下方式更新您的移动应用代码。完整代码示例请参阅 [OneSignal 集成的 SDK 配置](onesignal#sdk-configuration)。 ```diff showLineNumbers OneSignal.shared.setSubscriptionObserver((changes) { final playerId = changes.to.userId; if (playerId != null) { - final builder = - AdaptyProfileParametersBuilder() - ..setOneSignalPlayerId(playerId); - // ..setOneSignalSubscriptionId(playerId); try { - Adapty().updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "one_signal_player_id", + value: playerId, + ); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle error } } }); ``` ### Pushwoosh 按照以下说明更新你的移动应用代码。完整代码示例请参阅 [Pushwoosh 集成的 SDK 配置](pushwoosh#sdk-configuration)。 ```diff showLineNumbers final hwid = await Pushwoosh.getInstance.getHWID; - final builder = AdaptyProfileParametersBuilder() - ..setPushwooshHWID(hwid); try { - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "pushwoosh_hwid", + value: hwid, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## 更新 Observer 模式实现 \{#update-observer-mode-implementation\} 更新付费墙与交易的关联方式。此前,你需要使用 `setVariationId` 方法来分配 `variationId`。现在,你可以在使用新的 `reportTransaction` 方法记录交易时直接传入 `variationId`。请参阅[在 Observer 模式下将付费墙与购买交易关联](report-transactions-observer-mode-flutter)中的完整代码示例。 :::warning 不要忘记使用 `reportTransaction` 方法记录交易。如果跳过此步骤,Adapty 将无法识别该交易,不会授予访问等级,不会将其纳入分析数据,也不会将其发送到集成渠道。此步骤至关重要! ::: ```diff showLineNumbers try { - await Adapty().setVariationId("YOUR_TRANSACTION_ID", "PAYWALL_VARIATION_ID"); + // every time when calling transaction.finish() + await Adapty().reportTransaction( + "YOUR_TRANSACTION_ID", + variationId: "PAYWALL_VARIATION_ID", // optional + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` --- # File: migration-to-flutter-sdk-v3 --- --- title: "迁移 Adapty Flutter SDK 至 v3.0" description: "迁移至 Adapty Flutter SDK v3.0,获得更好的性能和新的变现功能。" --- Adapty Flutter SDK v3.0 引入了重大变更,本指南将帮助你顺利完成迁移。 ## 主要变更 \{#main-changes\} ### 1. 公共 API 变更 \{#public-api-changes\} #### `AdaptyPaywall` \{#adaptypaywall\} `remoteConfig` 属性现在返回 `AdaptyRemoteConfig?` 而非 `Map?`。 `AdaptyRemoteConfig` 是一个包含以下属性的新类: - `id` — 远程配置的标识符 - `string` — 原始字符串值 - `dataValue` — 解析后的 `Map?` **迁移前:** ```dart final remoteConfig = paywall.remoteConfig; // Map? ``` **迁移后:** ```dart final remoteConfig = paywall.remoteConfig; // AdaptyRemoteConfig? final data = paywall.remoteConfig?.dataValue; // Map? ``` ### 2. `AdaptyPaywallProduct` \{#adaptypawallproduct\} `variationId` 属性已重命名为 `paywallVariationId`。 **迁移前:** ```dart final variationId = product.variationId; ``` **迁移后:** ```dart final variationId = product.paywallVariationId; ``` ### 3. Paywall Builder \{#paywall-builder\} SDK v3.0 支持新版付费墙编辑工具。如需了解如何展示用付费墙编辑工具创建的付费墙,请参阅[展示付费墙编辑工具付费墙](display-pb-paywalls)。 Adapty SDK v3.0 带来了全新的 [Adapty 付费墙编辑工具](adapty-paywall-builder)支持,这是一款全新版本的无代码、用户友好的付费墙创建工具。凭借其极高的灵活性和丰富的设计能力,您的付费墙将变得更加高效和盈利。 :::info 请注意,AdaptyUI 库已被弃用,现已作为 AdaptySDK 的一部分包含在内。 ::: ## 移除 AdaptyUI SDK \{#remove-adaptyu-sdk\} 1. AdaptyUI 已成为 Adapty SDK 的一个模块,请从您的 `pubspec.yaml` 文件中移除 `adapty_ui_flutter`: ```diff showLineNumbers dependencies: + adapty_flutter: ^3.2.1 - adapty_flutter: ^2.10.3 - adapty_ui_flutter: ^2.1.3 ``` 2. 运行: ```bash showLineNumbers title="Bash" flutter pub get ``` ## 配置 Adapty SDK \{#configure-adapty-sdks\} 此前,您需要使用 `Adapty-Info.plist` 和 `AndroidManifest.xml` 文件来配置 Adapty SDK。 现在无需使用额外的配置文件,您可以在激活时直接提供所有必要参数。 您只需配置一次 Adapty SDK,通常在应用程序生命周期开始时进行。 ### 激活 Adapty SDK 的 Adapty 模块 \{#activate-adapty-module-of-adapty-sdk\} 1. 按如下方式从应用中移除 AdaptyUI SDK 的导入: ```diff showLineNumbers import 'package:adapty_flutter/adapty_flutter.dart'; - import 'package:adapty_ui_flutter/adapty_ui_flutter.dart'; ``` 2. 按如下方式更新 Adapty SDK 的激活代码: ```diff showLineNumbers try { - Adapty().activate(); + await Adapty().activate( + configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') + ..withLogLevel(AdaptyLogLevel.debug) + ..withObserverMode(false) + ..withCustomerUserId(null) + ..withIpAddressCollectionDisabled(false) + ..withIdfaCollectionDisabled(false), + ); } catch (e) { // handle the error } ``` 参数: | 参数 | 是否必填 | 描述 | | ----------------------------------- | -------- | ------------------------------------------------------------ | | **PUBLIC_SDK_KEY** | 必填 | 可在 Adapty 应用设置的 **Public SDK key** 字段中找到该密钥:[**App settings** -> **General** 标签页 -> **API keys** 子部分](https://app.adapty.io/settings/general) | | **withLogLevel** | 可选 | Adapty 会记录错误及其他关键信息,帮助您了解应用的运行状况。可用的日志级别如下:
  • error:仅记录错误。
  • warn:记录错误,以及 SDK 中不会导致严重错误但值得关注的消息。
  • info:记录错误、警告及重要信息,例如各模块生命周期的日志。
  • verbose:记录所有可能在调试时有用的附加信息,如函数调用、API 请求等。
| | **withObserverMode** | 可选 |

一个布尔值,用于控制[观察者模式](observer-vs-full-mode)。如果您自行处理购买和订阅状态,并使用 Adapty 发送订阅事件和分析数据,请将其开启。

默认值为 `false`。

🚧 在观察者模式下,Adapty SDK 不会关闭任何交易,请确保您自行处理。

| | **withCustomerUserId** | 可选 | 您系统中的用户标识符。我们会在订阅和分析事件中发送该标识符,以便将事件归因到正确的用户画像。您也可以在 [**Profiles and Segments**](https://app.adapty.io/profiles/users) 菜单中通过 `customerUserId` 查找用户。 | | **withIdfaCollectionDisabled** | 可选 |

设为 `true` 可禁用 IDFA 的收集与共享。

以及用户 IP 地址的共享。

默认值为 `false`。

有关 IDFA 收集的更多详情,请参阅[分析集成](analytics-integration#disable-collection-of-advertising-identifiers)部分。

| | **withIpAddressCollectionDisabled** | 可选 |

设为 `true` 可禁用用户 IP 地址的收集与共享。

默认值为 `false`。

| ### 激活 Adapty SDK 的 AdaptyUI 模块 \{#activate-adaptyui-module-of-adapty-sdk\} 只有当你计划使用[付费墙编辑工具](adapty-paywall-builder)时,才需要配置 AdaptyUI 模块: ```dart showLineNumbers title="Dart" try { final mediaCache = AdaptyUIMediaCacheConfiguration( memoryStorageTotalCostLimit: 100 * 1024 * 1024, // 100MB memoryStorageCountLimit: 2147483647, // 2^31 - 1, max int value in Dart diskStorageSizeLimit: 100 * 1024 * 1024, // 100MB ); await AdaptyUI().activate( configuration: AdaptyUIConfiguration(mediaCache: mediaCache), observer: , ); } catch (e) { // handle the error } ``` 请注意,AdaptyUI 配置是可选的,您可以在不使用配置的情况下激活 AdaptyUI 模块。但如果您使用配置,其中所有参数均为必填项。 参数: | 参数 | 是否必填 | 描述 | | :------------------------------ | :------- | :----------------------------------------------------------- | | **memoryStorageTotalCostLimit** | 必填 | 存储空间的总容量限制,单位为字节。 | | **memoryStorageCountLimit** | 必填 | 内存存储的最大条目数量。 | | **diskStorageSizeLimit** | 必填 | 存储在磁盘上的文件大小限制,单位为字节。0 表示不限制。 | --- # End of Documentation _Generated on: 2026-07-24T13:01:53.345Z_ _Successfully processed: 44/44 files_