在 iOS SDK 中通过 Flow 与付费墙编辑工具启用购买功能
要启用应用内购买,你需要了解三个核心概念:
- 产品 – 用户可购买的内容(订阅、消耗型商品、永久授权)
- 流程与付费墙 – 向用户展示产品的页面,通过无代码的流程与付费墙编辑工具构建,SDK 通过
getFlow 获取。如果你更倾向于用自己的代码构建 UI,请参阅实现自定义付费墙。
- 版位 – 应用中展示流程与付费墙的位置和时机(例如
main、onboarding、settings)。你在看板中将其关联到版位,然后在代码中通过版位 ID 请求。这样可以轻松运行 A/B 测试,并向不同用户展示不同内容。
Adapty 为您提供三种在应用中启用购买的方式,请根据应用需求选择其中一种:
| 实现方式 | 复杂度 | 适用场景 |
|---|
| Adapty Flow 与付费墙编辑工具 | ✅ 简单 | 你在无代码编辑工具中创建完整的、可直接购买的流程或付费墙。Adapty 自动渲染界面,并在后台处理所有复杂的购买逻辑、收据验证和订阅管理。 |
| 自定义付费墙 | 🟡 中等 | 你在应用代码中自行实现付费墙 UI,但仍从 Adapty 获取流程对象,以保持产品组合的灵活性。参阅指南。 |
| 观察者模式 | 🔴 复杂 | 你已有自己的购买处理基础设施,并希望继续使用它。请注意,观察者模式在 Adapty 中存在一定限制。参阅文章。 |
Important
以下步骤展示了如何实现在 Adapty Flow & Paywall Builder 中创建的流程或付费墙。
如果您希望自行构建付费墙 UI,请参阅实现自定义付费墙。
要在应用代码中展示在 Adapty Flow & Paywall Builder 中创建的流程或付费墙,您只需:
- 获取流程或付费墙:从 Adapty 获取。
- 展示付费墙,Adapty 将为你处理购买流程:在你的应用中展示视图。
- 处理按钮操作:将用户交互与应用的响应逻辑关联起来。例如,当用户点击按钮时打开链接或关闭付费墙。
开始之前
在开始之前,请先完成以下步骤:
- 在 Adapty 看板中将你的应用连接到 App Store。
- 在 Adapty 中创建产品。
- 创建流程或付费墙,并为其添加产品。
- 创建版位并将你的流程或付费墙添加到其中。
- 在你的应用代码中安装并激活 Adapty SDK。本指南使用 Adapty iOS SDK v4 API。
1. 获取流程或付费墙
您的流程和付费墙与在看板中配置的版位相关联。通过版位,您可以为不同的目标受众展示不同的流程或付费墙,也可以运行 A/B 测试。
要获取在 Adapty Flow & Paywall Builder 中创建的流程或付费墙,您需要:
- 使用
getFlow 方法通过版位 ID 获取 flow 对象,并检查它是否包含视图配置。
- 使用
getFlowConfiguration 方法获取视图配置。该配置包含显示所需的 UI 元素和样式。
func loadFlow() async {
let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID")
guard flow.hasViewConfiguration else {
print("Flow doesn't have a view configuration")
return
}
flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow)
}
2. 展示流程或付费墙
现在,当你拿到流程配置后,只需添加几行代码即可展示你的流程或付费墙。
在 SwiftUI 中展示流程时,还需要处理相关事件。didFinishPurchase、didFailPurchase、didFinishRestore、didFailRestore 和 didReceiveError 是必须处理的事件。在测试阶段,你可以直接复制下方代码片段来记录这些事件。
Tip
购买成功后,流程不会自动关闭。在 didFinishPurchase 中,将你的 presentation binding 设置为 false 来关闭它,或者不做任何操作以让流程继续进入下一个界面。
.flow(
isPresented: $flowPresented,
flowConfiguration: flowConfiguration,
didFinishPurchase: { product, purchaseResult in
print("Purchase finished successfully")
flowPresented = false // or do nothing to let the flow continue
},
didFailPurchase: { product, error in
print("Purchase failed: \(error)")
},
didFinishRestore: { profile in
print("Restore finished successfully")
},
didFailRestore: { error in
print("Restore failed: \(error)")
},
didReceiveError: { error in
flowPresented = false
print("Flow error: \(error)")
}
)
func presentFlow(with config: AdaptyUI.FlowConfiguration) {
let flowController = try AdaptyUI.flowController(
with: config,
delegate: self
)
present(flowController, animated: true)
}
实现 AdaptyFlowControllerDelegate 来处理事件。至少需要实现四个没有默认实现的方法。注意,控制器在成功购买后不会自动关闭——请在 didFinishPurchase 中关闭它,或者不做任何处理以让流程继续跳转到下一个页面:
extension YourViewController: AdaptyFlowControllerDelegate {
func flowController(_ controller: AdaptyFlowController,
didFinishPurchase product: AdaptyPaywallProduct,
purchaseResult: AdaptyPurchaseResult) {
if !purchaseResult.isPurchaseCancelled {
controller.dismiss(animated: true) // or do nothing to let the flow continue
}
}
func flowController(_ controller: AdaptyFlowController,
didFailPurchase product: AdaptyPaywallProduct,
error: AdaptyError) {
print("Purchase failed: \(error)")
}
func flowController(_ controller: AdaptyFlowController,
didFinishRestoreWith profile: AdaptyProfile) {
print("Restore finished successfully")
}
func flowController(_ controller: AdaptyFlowController,
didFailRestoreWith error: AdaptyError) {
print("Restore failed: \(error)")
}
}
Info
有关如何展示流程或付费墙的更多详情,请参阅我们的指南。
当用户点击按钮时,iOS SDK 会自动处理购买、恢复、关闭页面和打开链接等操作。
不过,其他按钮带有自定义或预定义的 ID,需要在代码中处理相应操作,或者你也可以覆盖它们的默认行为。
例如,以下是处理关闭按钮的方式。在 UIKit 中,SDK 会在 .close 触发时自动关闭控制器——仅在需要自定义行为时才覆盖。在 SwiftUI 中,你必须自行将 isPresented 绑定设为 false。
.flow(
isPresented: $flowPresented,
flowConfiguration: flowConfiguration,
didPerformAction: { action in
switch action {
case .close:
flowPresented = false // dismiss the flow when the user taps close
default:
break
}
},
didFinishPurchase: { product, purchaseResult in flowPresented = false },
didFailPurchase: { product, error in /* handle the error */ },
didFinishRestore: { profile in /* check access level and dismiss */ },
didFailRestore: { error in /* handle the error */ },
didReceiveError: { error in flowPresented = false }
)
extension YourViewController: AdaptyFlowControllerDelegate {
func flowController(_ controller: AdaptyFlowController,
didPerform action: AdaptyUI.Action) {
switch action {
case .close:
controller.dismiss(animated: true) // default behavior — override only if needed
default:
break
}
}
}
下一步
Tip
有疑问或遇到问题?欢迎访问我们的支持论坛,在那里你可以找到常见问题的解答,也可以提出自己的问题。我们的团队和社区随时为你提供帮助!
你的流程或付费墙已准备好在应用中展示。在沙盒模式下测试购买,确保你能成功完成测试购买。
接下来,你需要检查用户的访问等级,确保向正确的用户展示付费墙或开放付费功能。
完整示例
以下是本指南中所有步骤在应用中整合的完整示例。
struct ContentView: View {
@State private var flowPresented = false
@State private var flowConfiguration: AdaptyUI.FlowConfiguration?
@State private var isLoading = false
@State private var hasInitialized = false
var body: some View {
VStack {
if isLoading {
ProgressView("Loading...")
} else {
Text("Your App Content")
}
}
.task {
guard !hasInitialized else { return }
await initializeFlow()
hasInitialized = true
}
.flow(
isPresented: $flowPresented,
flowConfiguration: flowConfiguration,
didPerformAction: { action in
switch action {
case .close:
flowPresented = false
default:
break
}
},
didFinishPurchase: { product, purchaseResult in
print("Purchase finished successfully")
flowPresented = false // or do nothing to let the flow continue
},
didFailPurchase: { product, error in
print("Purchase failed: \(error)")
},
didFinishRestore: { profile in
print("Restore finished successfully")
},
didFailRestore: { error in
print("Restore failed: \(error)")
},
didReceiveError: { error in
print("Flow error: \(error)")
flowPresented = false
}
)
}
private func initializeFlow() async {
isLoading = true
defer { isLoading = false }
await loadFlow()
flowPresented = true
}
private func loadFlow() async {
do {
let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID")
guard flow.hasViewConfiguration else {
print("Flow doesn't have a view configuration")
return
}
flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow)
} catch {
print("Failed to load: \(error)")
}
}
}
class ViewController: UIViewController {
private var flowConfiguration: AdaptyUI.FlowConfiguration?
override func viewDidLoad() {
super.viewDidLoad()
Task {
await initializeFlow()
}
}
private func initializeFlow() async {
do {
flowConfiguration = try await loadFlow()
if let flowConfiguration {
await MainActor.run {
presentFlow(with: flowConfiguration)
}
}
} catch {
print("Error initializing: \(error)")
}
}
private func loadFlow() async throws -> AdaptyUI.FlowConfiguration? {
let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID")
guard flow.hasViewConfiguration else {
print("Flow doesn't have a view configuration")
return nil
}
return try await AdaptyUI.getFlowConfiguration(forFlow: flow)
}
private func presentFlow(with config: AdaptyUI.FlowConfiguration) {
guard let flowController = try? AdaptyUI.flowController(
with: config,
delegate: self
) else { return }
present(flowController, animated: true)
}
}
extension ViewController: AdaptyFlowControllerDelegate {
func flowController(_ controller: AdaptyFlowController,
didFinishPurchase product: AdaptyPaywallProduct,
purchaseResult: AdaptyPurchaseResult) {
if !purchaseResult.isPurchaseCancelled {
controller.dismiss(animated: true) // or do nothing to let the flow continue
}
}
func flowController(_ controller: AdaptyFlowController,
didFailPurchase product: AdaptyPaywallProduct,
error: AdaptyError) {
print("Purchase failed for \(product.vendorProductId): \(error)")
guard error.adaptyErrorCode != .paymentCancelled else { return }
let message = switch error.adaptyErrorCode {
case .paymentNotAllowed:
"Purchases are not allowed on this device."
default:
"Purchase failed. Please try again."
}
let alert = UIAlertController(title: "Purchase Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
func flowController(_ controller: AdaptyFlowController,
didFinishRestoreWith profile: AdaptyProfile) {
print("Restore finished successfully")
controller.dismiss(animated: true)
}
func flowController(_ controller: AdaptyFlowController,
didFailRestoreWith error: AdaptyError) {
print("Restore failed: \(error)")
}
func flowController(_ controller: AdaptyFlowController,
didReceiveError error: AdaptyUIError) {
print("Flow error: \(error)")
controller.dismiss(animated: true)
}
}