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.
| Property | Type | Description |
|---|---|---|
theme | PaywallTheme | Current theme tokens (colors, radius, font) |
selectedProduct | ResolvedProduct? | 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
CustomViewContextdata - 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
@StateObjector@ObservedObjectfor 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 }
}