PAYATHON 2026

Adapty Paywall 已上线但 Flutter 无法显示 UI

数据阿明

结论

getPaywall() 成功,说明 Adapty 已根据 Placement 返回 Paywall 及其 variation,但这个 variation 不一定包含 AdaptyUI 可以渲染的 Paywall Builder 配置。

日志中的关键信号是:

hasViewConfiguration: false
GET /paywall-builder/ ... statusCode: 404

这表示当前 variation 没有可用的 UI 配置,所以 AdaptyUI().createPaywallView() 无法创建 View。

Dashboard 中的 live 状态通常只表示 Paywall 或 Placement 已发布,SDK 可以获取它。还需要单独确认当前 variation 对应的 Paywall Builder 页面已经创建、保存并发布。

remoteConfig: null 不一定表示异常。Remote Config 和 Paywall Builder UI 配置是两套不同的数据。没有配置 Remote Config 时,返回 null 是正常的。

常见原因

1. 只发布了 Paywall,没有发布可视化页面

Paywall 可以承载产品、权限和实验配置。即使没有在 Paywall Builder 中创建并发布页面,SDK 仍可能通过 getPaywall() 获取它,但此时:

paywall.hasViewConfiguration == false

这种 Paywall 适合由应用自行绘制购买界面,不能直接传给 AdaptyUI().createPaywallView()。

请进入 Dashboard,找到当前 Paywall 返回的具体 variation,确认它已绑定 Builder 页面,并且页面已经保存和发布。仅看到 Paywall 状态为 live 还不够。

2. Placement 返回了另一个 variation

如果 Placement 配置了 A/B 测试、受众规则或多个 variation,设备实际拿到的版本可能不是你正在 Dashboard 中检查的版本。

日志中的:

variationId: <<variationid>>
experimentName: Subscription

只能说明设备命中了某个 variation。需要根据 variationId 在 Dashboard 中找到实际返回的版本,再确认这个 variation 是否包含已发布的 Builder UI。不要只检查同一 Paywall 下的默认 variation。

3. App 使用了其他项目的 Public SDK Key

如果应用初始化时使用的 Public SDK Key 属于另一个 Adapty 项目,可能出现以下情况:

  • Placement ID 在另一个项目中也存在;
  • SDK 获取到了同名或旧的 Paywall;
  • Builder UI 配置不属于当前 SDK Key 对应的项目。

检查 Adapty.activate() 使用的 Public SDK Key,确认它与当前编辑 Paywall 的 Dashboard 项目一致。不要混用测试项目、生产项目、iOS 项目或 Android 项目的 Key。

不要在日志、截图或客户端代码仓库中公开 Secret Key。移动端只能使用适用于客户端的 Public SDK Key。

4. Dashboard 修改尚未反映到当前结果

修改 Paywall、Placement 或实验后,设备可能仍在使用缓存的配置。可以按以下顺序处理:

  1. 确认 Builder 页面已经发布。
  2. 重新启动应用并请求 Paywall。
  3. 清除测试设备上的应用数据,或重新安装应用。
  4. 暂时关闭相关实验或受众规则,改用单一 variation 验证。
  5. 新建一个临时 Placement,并关联带有 Builder UI 的 Paywall,用它排除旧配置或缓存的影响。

缓存控制参数取决于项目使用的 Adapty Flutter SDK 版本,不要直接照搬其他版本的 API。

5. Flutter、Adapty 原生 SDK 与 AdaptyUI 版本不兼容

错误详情中包含原生组件版本:

[2.10.3]

如果项目使用的 Flutter SDK、原生 Adapty SDK 或 AdaptyUI 较旧,可能无法正确处理 Dashboard 当前生成的 Builder 配置。还需要检查 adapty_flutter 和对应 AdaptyUI 包的版本是否兼容。

升级时应参考当前安装版本对应的官方迁移文档,因为不同主版本的初始化方式、类型和方法签名可能不同。升级后重新安装 Flutter 依赖,并根据平台刷新 iOS Pods 或 Android 构建缓存。

不过,仅凭这段日志还不能确定是版本问题。hasViewConfiguration: false 更直接地说明当前 variation 没有返回 UI 配置。

推荐排查顺序

第一步:确认拿到的是预期 variation

保留并核对以下信息:

debugPrint('placementId: $placementId');
debugPrint('variationId: ${paywall.variationId}');
debugPrint('paywallName: ${paywall.name}');
debugPrint('hasViewConfiguration: ${paywall.hasViewConfiguration}');

在 Dashboard 中找到这个准确的 variationId,确认它不是实验中的另一个 variation。

第二步:检查该 variation 的 Builder 页面

逐项确认:

  • 页面确实通过 Adapty Paywall Builder 创建;
  • 页面绑定到了当前返回的 variation;
  • Builder 中的修改已经保存并发布;
  • 页面不只是草稿;
  • Placement 当前指向的确实是该 Paywall 或 variation;
  • 产品配置完整,没有引用已删除或未绑定的商品。

需要分别确认 Paywall 已经 live,以及 Builder UI 已经发布。

第三步:确认项目和 SDK Key

检查应用初始化代码,例如:

await Adapty().activate(
  configuration: AdaptyConfiguration(
    apiKey: publicSdkKey,
  ),
);

具体签名以项目当前使用的 SDK 版本为准。确认 publicSdkKey 来自正在编辑该 Paywall 的同一个 Dashboard 项目。

第四步:升级兼容的 SDK 组合

检查 pubspec.yaml、pubspec.lock,以及 iOS 和 Android 最终解析出的版本。升级时需要保持核心 SDK 与 AdaptyUI 版本兼容,不要只升级其中一个。

如果升级后仍然返回相同的 404,应继续检查 Dashboard 配置、项目归属和 variation 绑定,而不是反复修改 locale。

第五步:用最小配置交叉验证

创建一个新的测试 Placement:

  • 只关联一个 Paywall;
  • 不启用 A/B 测试和复杂受众规则;
  • 使用 Builder 创建一个最简单的页面;
  • 保存并发布;
  • 在同一设备上使用该 Placement ID 发起请求。

如果新 Placement 可以正常显示,说明 SDK 集成本身基本正常,问题更可能出在原 Placement 的 variation、实验配置或 Builder 发布状态。

代码中应先检查 UI 配置

创建 View 前先判断 hasViewConfiguration,可以避免继续把没有 Builder UI 的 Paywall 传给 AdaptyUI:

static Future<void> showPaywallUI({
  required String placementId,
  String? locale,
}) async {
  try {
    final paywall = await getPaywallByPlacement(placementId);

    if (paywall == null) {
      debugPrint('❌ Paywall not found for placement: $placementId');
      return;
    }

    debugPrint('✅ Paywall loaded for placement: $placementId');
    debugPrint('🧩 variationId: ${paywall.variationId}');
    debugPrint(
      '📦 hasViewConfiguration: ${paywall.hasViewConfiguration}',
    );
    debugPrint('🧾 remoteConfig: ${paywall.remoteConfig}');
    debugPrint('🧮 paywallName: ${paywall.name}');

    if (!paywall.hasViewConfiguration) {
      debugPrint(
        '❌ The returned paywall variation has no published '
        'Paywall Builder configuration.',
      );
      return;
    }

    final view = await AdaptyUI().createPaywallView(
      paywall: paywall,
      locale: locale ?? 'en',
    );

    await view.present();
    debugPrint('✅ Paywall UI presented for: $placementId');
  } catch (e, stackTrace) {
    debugPrint('❌ Failed to present Paywall UI: $e');
    debugPrintStack(stackTrace: stackTrace);
  }
}

这项检查只能让配置问题更容易被发现,不能自动补上缺失的 UI。如果该 Paywall 原本就用于自定义界面,应使用 getPaywall() 返回的产品数据自行绘制 UI,而不是调用 createPaywallView()。

关于 locale

方法接收了 locale 参数,但原实现固定传入:

locale: 'en'

可以改为:

locale: locale ?? 'en'

不过,缺少某种本地化版本通常不能直接解释当前的 hasViewConfiguration: false 和 Builder 接口 404。应先检查基础 Builder 配置是否存在并已发布。

仍无法解决时需要提供的信息

如果已经确认 Builder UI 发布到了正确的 variation,但请求仍然返回 404,可以向 Adapty 支持提供:

  • Adapty 项目 ID;
  • Placement ID;
  • 日志中的 variationId;
  • Paywall ID;
  • adapty_flutter 和 AdaptyUI 包版本;
  • iOS 或 Android 平台及系统版本;
  • 完整错误码、HTTP 状态码和发生时间;
  • Dashboard 中 variation 与 Builder 发布状态的截图。

不要公开完整 SDK Key。如果此类 404 并非由配置错误或项目不匹配引起,需要由 Adapty 根据项目和 Builder 资源状态检查服务端记录。

备注:内容仅供参考。