SDK
Custom Views

Custom Views

Custom views let you embed your own SwiftUI components inside AgentWallie paywalls. When the built-in component types are not enough, register a native view and reference it by name in the paywall schema.

When to Use Custom Views

  • Brand-specific UI that cannot be expressed with standard components (animated logos, custom charts, interactive demos)
  • Complex interactions beyond tap actions (sliders, custom pickers, drag gestures)
  • Third-party SDK integrations (video players, maps, AR previews)
  • Highly custom layouts that go beyond stack/carousel/slides arrangements

Use built-in components whenever possible. Custom views require native code changes and app updates, while standard components can be updated server-side.

Registration API

Register custom views at app startup, before any paywall is presented.

Simple View

import AgentWallieKit
 
// In your App init or AppDelegate
AgentWallie.shared.registerView(name: "PremiumHero") {
    PremiumHeroView()
}

Context-Aware View

For views that need access to paywall context (product data, theme, actions):

AgentWallie.shared.registerView(name: "PricingCard") { context in
    PricingCardView(
        price: context.selectedProduct?.displayPrice ?? "",
        period: context.selectedProduct?.displayPeriod ?? "",
        theme: context.theme,
        customData: context.customData
    )
}

CustomViewContext

The context object passed to context-aware views provides access to paywall runtime data.

PropertyTypeDescription
themePaywallThemeCurrent theme tokens (colors, radius, font)
selectedProductResolvedProduct?Currently selected product with price/period
products[ResolvedProduct]All available products
customData[String: AnyCodable]?Custom data from the schema's custom_data prop
userAttributes[String: Any]User attributes set via setUserAttributes

Triggering Actions

Custom views can trigger paywall actions using the action handler provided by the context:

Purchase

struct PricingCardView: View {
    @EnvironmentObject var paywallActions: PaywallActionHandler
 
    var body: some View {
        Button("Subscribe Now") {
            paywallActions.purchase(product: "primary")
        }
    }
}

Close

paywallActions.close()

Custom Action

paywallActions.customAction(name: "show_comparison")

The customAction call is forwarded to your AgentWallieDelegate.handleCustomAction(name:) implementation.

Referencing in Schemas

Once registered, reference the view by name in the paywall JSON schema:

{
  "type": "custom_view",
  "id": "hero_section",
  "props": {
    "view_name": "PremiumHero"
  }
}

With Custom Data

Pass arbitrary data to the view via custom_data:

{
  "type": "custom_view",
  "id": "pricing_card",
  "props": {
    "view_name": "PricingCard",
    "custom_data": {
      "highlight_color": "#FF6B6B",
      "show_comparison": true,
      "features": ["Unlimited access", "Priority support", "Offline mode"]
    }
  }
}

The custom_data object is available in the view context as context.customData. You can also use expressions inside custom_data values:

{
  "custom_data": {
    "accent_color": "{{ theme.accent }}",
    "price_text": "{{ products.selected.price }}/{{ products.selected.period }}"
  }
}

Best Practices

Naming

  • Use PascalCase for view names: PremiumHero, PricingCard, WorkoutPreview
  • Prefix with your app or feature name if you have many: FitnessPricingCard, MediaVideoPreview
  • Keep names stable across app versions -- changing a name breaks existing schemas

Lifecycle

  • Register all views before configuring AgentWallie.configure() or immediately after
  • Views are retained by the registry for the app's lifetime
  • Do not register views lazily or conditionally -- the registry must be complete before any paywall renders

Testing

  • Test custom views in isolation with mock CustomViewContext data
  • Use the AgentWallie preview tool to verify your view renders correctly within a paywall
  • Test with different theme configurations to ensure your view respects theme tokens
  • Verify actions (purchase, close, custom) trigger correctly from your view

Performance

  • Keep custom views lightweight -- they render inline within the paywall scroll view
  • Avoid heavy network requests in the view body
  • Use @StateObject or @ObservedObject for async data, not blocking the main thread

Full Example: Custom Pricing Card

Swift View

struct CustomPricingCard: View {
    let context: CustomViewContext
 
    private var features: [String] {
        (context.customData?["features"] as? [String]) ?? []
    }
 
    var body: some View {
        VStack(spacing: 16) {
            Text(context.selectedProduct?.displayPrice ?? "")
                .font(.system(size: 48, weight: .bold))
                .foregroundColor(Color(hex: context.theme.primary))
 
            Text(context.selectedProduct?.displayPeriod ?? "")
                .font(.subheadline)
                .foregroundColor(Color(hex: context.theme.textSecondary))
 
            ForEach(features, id: \.self) { feature in
                HStack {
                    Image(systemName: "checkmark.circle.fill")
                        .foregroundColor(Color(hex: context.theme.accent))
                    Text(feature)
                }
            }
        }
        .padding(24)
        .background(Color(hex: context.theme.surface))
        .cornerRadius(CGFloat(context.theme.cornerRadius))
    }
}

Registration

AgentWallie.shared.registerView(name: "CustomPricingCard") { context in
    CustomPricingCard(context: context)
}

Schema

{
  "type": "custom_view",
  "id": "pricing",
  "props": {
    "view_name": "CustomPricingCard",
    "custom_data": {
      "features": ["Unlimited access", "No ads", "Priority support", "Offline mode"]
    }
  },
  "style": { "margin_horizontal": 16, "margin_bottom": 24 }
}