iOS SDKでFlow Builderを使ってアプリ内課金を有効にする
アプリ内課金を有効にするには、3つの重要なコンセプトを理解する必要があります。
- プロダクト – ユーザーが購入できるもの(サブスクリプション、消耗型アイテム、永続アクセスなど)
- フロー – ノーコードのFlow Builderで作成された、ユーザーにプロダクトを提示する画面シーケンス。SDKは
getFlowを通じてフローを取得します。UIを自分のコードで構築したい場合はペイウォールを使ってください。詳しくはペイウォールを手動で実装するを参照してください。
- プレースメント – アプリのどこでいつフローを表示するかを定義します(
main、onboarding、settingsなど)。ダッシュボードでフローをプレースメントに紐付けて、コードではプレースメントIDで取得します。これにより、A/Bテストの実行や異なるユーザーへの異なるフロー表示が簡単になります。
Adaptyでは、アプリ内課金を有効にする3つの方法を提供しています。アプリの要件に応じて選択してください。
| 実装方法 | 複雑さ | 使用タイミング |
|---|
| Adapty Flow Builder | ✅ 簡単 | ノーコードビルダーで購入準備が整ったフローを作成します。Adaptyが自動的にレンダリングし、複雑な購入フロー、レシート検証、サブスクリプション管理をすべて処理します。 |
| 手動で作成したペイウォール | 🟡 中程度 | アプリのコードでペイウォールUIを実装しつつ、プロダクト提供の柔軟性を保つためにAdaptyからフローオブジェクトを取得します。詳しくはガイドを参照してください。 |
| オブザーバーモード | 🔴 難しい | すでに独自の購入処理インフラがあり、それを継続して使いたい場合。オブザーバーモードにはAdaptyでの制限があります。詳しくはこちらを参照してください。 |
以下の手順は、Adapty Flow Builderで作成したフローを実装する方法を示しています。
ペイウォールUIを自分で構築したい場合は、ペイウォールを手動で実装するを参照してください。
Adapty Flow Builderで作成したフローをアプリで表示するには、コードで行うことは次の3つだけです。
- フローの取得: Adaptyからフローを取得します。
- 表示する — 購入はAdaptyが処理: アプリにビューを表示します。
- ボタンアクションの処理: ユーザーの操作をアプリの応答に紐付けます。例えば、リンクを開いたり、ユーザーがボタンをタップしたときにフローを閉じたりします。
始める前に
始める前に、以下の手順を完了してください。
- Adapty ダッシュボードでアプリをApp Storeに接続する。
- Adaptyでプロダクトを作成する。
- フローを作成してプロダクトを追加する。
- プレースメントを作成してフローを追加する。
- アプリのコードにAdapty SDKをインストールして有効化する。このガイドではAdapty iOS SDK v4(ベータ版)のAPIを使用します。
1. フローを取得する
フローはダッシュボードで設定したプレースメントに紐付けられています。プレースメントを使うと、異なるオーディエンスに対して異なるフローを実行したり、A/Bテストを実行したりできます。
Adapty Flow Builderで作成したフローを取得するには、次の手順を行います。
getFlowメソッドを使ってプレースメントIDからフローオブジェクトを取得し、ビュー設定があるかどうかを確認する。
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では、フローを表示する際にイベントも処理する必要があります。didFailPurchase、didFinishRestore、didFailRestore、didReceiveErrorは必須です。テスト中は、以下のスニペットのコードをコピーしてこれらのイベントをログ出力することができます。
didFinishPurchaseの処理は必須ではありませんが、購入成功後にアクションを実行したい場合に便利です。このコールバックを実装しない場合、フローは自動的に閉じられます。
.flow(
isPresented: $flowPresented,
flowConfiguration: flowConfiguration,
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を実装します。最低限、デフォルト実装がない3つのメソッド(必須のエラーハンドラー)を実装してください。
extension YourViewController: AdaptyFlowControllerDelegate {
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)")
}
}
フローの表示方法の詳細については、ガイドを参照してください。
ユーザーがボタンをタップすると、iOS SDKは購入、復元、フローのクローズ、リンクの開封を自動的に処理します。
ただし、他のボタンにはカスタムまたは事前定義されたIDがあり、コードでアクションを処理する必要があります。または、デフォルトの動作を上書きしたい場合もあります。
例えば、クローズボタンの処理方法を次に示します。UIKitでは、.closeが発火すると SDKが自動的にコントローラーを閉じます — カスタム動作が必要な場合のみオーバーライドしてください。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
}
},
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
}
}
}
次のステップ
ご質問やお困りのことがあれば、サポートフォーラムをご覧ください。よくある質問への回答を見つけたり、ご自身の質問を投稿することができます。チームとコミュニティがサポートいたします!
フローをアプリで表示する準備ができました。サンドボックスモードで購入をテストして、テスト購入が正常に完了できることを確認してください。
次に、適切なユーザーにフローを表示したり有料機能へのアクセスを付与したりするために、ユーザーのアクセスレベルを確認する必要があります。
完全なサンプル
このガイドのすべての手順をアプリに統合した例を以下に示します。
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
}
},
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,
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)
}
}