Make InAppKit match your app's design and boost conversions
InAppKit provides beautiful defaults but gives you full control over the user experience.
- Marketing-Enhanced Products
- Custom Paywalls
- Product Configuration
- UI Customization
- Advanced Configuration
Boost conversion rates with badges, feature highlights, and savings displays.
You can manually specify promotional text using .withPromoText():
ContentView()
.withPurchases(products: [
Product("com.app.basic", features: [Feature.removeAds])
.withBadge("Popular", color: .orange)
.withMarketingFeatures(["No ads", "Basic support"]),
Product("com.app.pro", features: [Feature.removeAds, Feature.cloudSync])
.withBadge("Best Value", color: .green)
.withMarketingFeatures([
"Everything in Basic",
"Cloud sync across devices",
"Priority support"
])
.withPromoText("Save 30%"),
Product("com.app.premium", features: Feature.allCases)
.withBadge("Professional", color: .purple)
.withMarketingFeatures([
"Everything in Pro",
"Advanced analytics",
"Team collaboration",
"API access"
])
.withPromoText("Save 50%")
])The .withRelativeDiscount() method automatically calculates savings by comparing product prices:
ContentView()
.withPurchases(products: [
Product("com.app.monthly", features: Feature.allCases)
.withBadge("Monthly"),
Product("com.app.yearly", features: Feature.allCases)
.withBadge("Best Value", color: .green)
.withRelativeDiscount(comparedTo: "com.app.monthly")
// Automatically displays: "Save 31%" (calculated from actual prices)
])Available Discount Styles:
// Percentage discount (default)
Product("com.app.yearly", features: features)
.withRelativeDiscount(comparedTo: "com.app.monthly")
// Displays: "Save 31%"
// Amount discount
Product("com.app.yearly", features: features)
.withRelativeDiscount(comparedTo: "com.app.monthly", style: .amount)
// Displays: "Save $44"
// Free time calculation
Product("com.app.yearly", features: features)
.withRelativeDiscount(comparedTo: "com.app.monthly", style: .freeTime)
// Displays: "2 months free"
// With custom color
Product("com.app.yearly", features: features)
.withRelativeDiscount(comparedTo: "com.app.monthly", color: .green)
// Displays: "Save 31%" in greenDiscount Styles:
.percentage- "Save 31%" (default).amount- "Save $44".freeTime- "2 months free"
Benefits:
- ✅ Automatic calculation - no manual math
- ✅ Always accurate - updates with App Store price changes
- ✅ Localized - currency formatting by locale
- ✅ Customizable color - match your brand
Use .withRelativeDiscount() when:
- Comparing subscription tiers (monthly vs yearly)
- Showing savings on bundle products
- Prices change frequently or vary by region
- You want accurate, localized discount displays
Use .withPromoText() when:
- Running time-limited promotions ("50% off this week!")
- Displaying non-price benefits ("Includes 3 months free trial")
- Showing custom marketing messages ("Most popular choice")
You can use both methods together for rich marketing displays:
ContentView()
.withPurchases(products: [
Product("com.app.monthly", features: Feature.allCases)
.withBadge("Flexible")
.withMarketingFeatures(["Pay as you go", "Cancel anytime"]),
Product("com.app.yearly", features: Feature.allCases)
.withBadge("Best Value", color: .green)
.withRelativeDiscount(comparedTo: "com.app.monthly", style: .freeTime)
.withPromoText("Limited time offer!")
.withMarketingFeatures([
"All premium features",
"Priority support",
"Exclusive updates"
])
])struct PaywallView: View {
let context: PaywallContext
var body: some View {
VStack {
ForEach(context.availableProducts, id: \\.id) { product in
ProductCard(
product: product,
badge: context.badge(for: product),
features: context.marketingFeatures(for: product),
promoText: context.promoText(for: product)
)
}
}
}
}
struct ProductCard: View {
let product: Product
let badge: String?
let features: [String]?
let promoText: String?
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(product.displayName)
.font(.headline)
if let badge = badge {
Text(badge)
.font(.caption)
.padding(.horizontal, 8)
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Capsule())
}
}
if let features = features {
ForEach(features, id: \\.self) { feature in
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text(feature)
}
.font(.caption)
}
}
HStack {
Text(product.displayPrice)
.font(.title2)
.fontWeight(.bold)
if let promoText = promoText {
Text(promoText)
.font(.caption)
.foregroundColor(.orange)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
}Create paywalls that match your app's design and optimize for conversion.
ContentView()
.withPurchases(products: [...])
.withPaywall { context in
VStack {
Text("Unlock Premium Features")
.font(.title)
.fontWeight(.bold)
Text("Get the most out of your app")
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
ForEach(context.availableProducts, id: \\.id) { product in
PurchaseButton(product: product)
}
Spacer()
Button("Restore Purchases") {
InAppKit.shared.restorePurchases()
}
.font(.caption)
}
.padding()
}struct AnimatedPaywallView: View {
let context: PaywallContext
@State private var showFeatures = false
var body: some View {
VStack {
Text("Upgrade to Pro")
.font(.largeTitle)
.fontWeight(.bold)
if showFeatures {
FeatureListView()
.transition(.slide)
}
ProductGridView(products: context.availableProducts)
Button("Maybe Later") {
// Dismiss paywall
}
.foregroundColor(.secondary)
}
.onAppear {
withAnimation(.easeInOut(duration: 0.8)) {
showFeatures = true
}
}
}
}struct FeatureGateView: View {
@State private var showPaywall = false
var body: some View {
VStack {
Button("Export to PDF") {
if InAppKit.shared.hasAccess(to: Feature.exportPDF) {
exportToPDF()
} else {
showPaywall = true
}
}
}
.sheet(isPresented: $showPaywall) {
CustomPaywallView(
triggeredBy: "export_pdf",
focusProduct: "com.app.pro"
)
}
}
}let config = PurchaseSetup()
.withPurchases(products: [
Product("com.app.basic", features: [Feature.removeAds]),
Product("com.app.pro", features: [Feature.removeAds, Feature.cloudSync])
])
.withPaywall { context in
CustomPaywallView(context: context)
}
.withTerms {
TermsView()
}
.withPrivacy {
PrivacyView()
}
ContentView()
.withConfiguration(config)struct ContentView: View {
var body: some View {
MainAppView()
.withPurchases(products: products)
.withPaywall { context in
if UIDevice.current.userInterfaceIdiom == .pad {
iPadPaywallView(context: context)
} else {
iPhonePaywallView(context: context)
}
}
}
private var products: [ProductDefinition<AppFeature>] {
#if DEBUG
return [
Product("com.app.test", features: AppFeature.allCases)
]
#else
return [
Product("com.app.basic", features: [.removeAds]),
Product("com.app.pro", features: AppFeature.allCases)
]
#endif
}
}struct PurchaseButton: View {
let product: Product
@State private var isPurchasing = false
var body: some View {
Button(action: purchase) {
HStack {
if isPurchasing {
ProgressView()
.scaleEffect(0.8)
} else {
Text("Get \\(product.displayName)")
Spacer()
Text(product.displayPrice)
.fontWeight(.bold)
}
}
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(12)
}
.disabled(isPurchasing)
}
private func purchase() {
isPurchasing = true
Task {
await InAppKit.shared.purchase(product)
isPurchasing = false
}
}
}ContentView()
.withPurchases(products: [...])
.withTerms {
VStack {
Text("Terms of Service")
.font(.title2)
.fontWeight(.bold)
ScrollView {
Text(termsText)
.font(.body)
}
Button("Accept") {
// Handle acceptance
}
.buttonStyle(.borderedProminent)
}
.padding()
}
.withPrivacy {
VStack {
Text("Privacy Policy")
.font(.title2)
.fontWeight(.bold)
ScrollView {
Text(privacyText)
.font(.body)
}
Button("Understood") {
// Handle acknowledgment
}
.buttonStyle(.borderedProminent)
}
.padding()
}Customize the header section and features section of the default paywall to match your app's branding.
// Custom header with different icon and colors
ContentView()
.withPurchases(products: [...])
.withPaywallHeader {
PaywallHeader(
icon: .system("sparkles"),
title: "Go Premium",
subtitle: "Transform your experience with powerful features",
iconColor: .purple,
backgroundColor: .purple.opacity(0.15)
)
}
.withPaywallFeatures {
PaywallFeatures(
title: "Premium Benefits",
features: [
PaywallFeature(
icon: .system("wand.and.stars"),
title: "AI-Powered Features",
subtitle: "Smart automation and intelligent suggestions",
iconColor: .purple
),
PaywallFeature(
icon: .asset("cloud-sync-icon"), // Using custom asset
title: "Cloud Sync",
subtitle: "Seamless sync across all your devices",
iconColor: .blue
),
PaywallFeature(
icon: .system("chart.line.uptrend.xyaxis"),
title: "Advanced Analytics",
subtitle: "Detailed insights and performance metrics",
iconColor: .green
),
PaywallFeature(
icon: .custom(Image("priority-badge").renderingMode(.template)),
title: "Priority Support",
subtitle: "Get expert help within 24 hours",
iconColor: .orange
)
]
)
}Or use the convenience methods for simpler customization:
// System icon (default)
ContentView()
.withPurchases(products: [...])
.withPaywallHeader(
systemIcon: "crown.fill",
title: "Unlock Pro",
subtitle: "Get access to all premium features",
iconColor: .gold
)
// Asset icon
ContentView()
.withPurchases(products: [...])
.withPaywallHeader(
assetIcon: "premium-crown",
title: "Unlock Pro",
subtitle: "Get access to all premium features",
iconColor: .gold
)
// PaywallIcon enum
ContentView()
.withPurchases(products: [...])
.withPaywallHeader(
icon: .asset("premium-crown"),
title: "Unlock Pro",
subtitle: "Get access to all premium features",
iconColor: .gold
)
.withPaywallFeatures(
title: "What You Get",
features: PaywallFeature.defaultFeatures
)PaywallIcon supports three different icon types:
// System icons (SF Symbols)
PaywallFeature(icon: .system("star.fill"), title: "Premium", subtitle: "...")
// Asset images from your app bundle
PaywallFeature(icon: .asset("premium-icon"), title: "Premium", subtitle: "...")
// Custom images with full control
PaywallFeature(
icon: .custom(
Image("custom-icon")
.renderingMode(.template)
.resizable()
),
title: "Premium",
subtitle: "..."
)
// Convenience initializers for backward compatibility
PaywallFeature(systemIcon: "star.fill", title: "Premium", subtitle: "...")
PaywallFeature(assetIcon: "premium-icon", title: "Premium", subtitle: "...")Build completely custom sections using the components:
ContentView()
.withPurchases(products: [...])
.withPaywallHeader {
VStack(spacing: 20) {
// Custom animated header
Lottie(name: "premium-animation")
.frame(height: 120)
VStack(spacing: 8) {
Text("Welcome to Premium")
.font(.largeTitle)
.fontWeight(.black)
.foregroundStyle(
LinearGradient(
colors: [.blue, .purple],
startPoint: .leading,
endPoint: .trailing
)
)
Text("Experience the full potential of our app")
.font(.headline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
}
}
.withPaywallFeatures {
VStack(spacing: 24) {
Text("Exclusive Features")
.font(.title2)
.fontWeight(.bold)
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 16) {
ForEach(customFeatures, id: \.id) { feature in
FeatureCard(feature: feature)
}
}
}
}struct AdvancedView: View {
var body: some View {
VStack {
Button("Export PDF") {
exportPDF()
}
.requiresPurchase(
Feature.exportPDF,
paywall: { context in
ExportPaywallView(context: context)
}
)
Button("Cloud Sync") {
syncToCloud()
}
.requiresPurchase(
Feature.cloudSync,
paywall: { context in
CloudSyncPaywallView(context: context)
}
)
}
}
}struct DynamicConfigView: View {
@State private var products: [ProductDefinition<AppFeature>] = []
var body: some View {
ContentView()
.withPurchases(products: products)
.onAppear {
loadProducts()
}
}
private func loadProducts() {
// Load from remote config, A/B test, etc.
if UserDefaults.standard.bool(forKey: "showPremiumTier") {
products = [
Product("com.app.basic", features: [.removeAds]),
Product("com.app.pro", features: [.removeAds, .cloudSync]),
Product("com.app.premium", features: AppFeature.allCases)
]
} else {
products = [
Product("com.app.pro", features: AppFeature.allCases)
]
}
}
}struct ConditionalFeatureView: View {
@State private var userTier: UserTier = .free
var body: some View {
VStack {
switch userTier {
case .free:
FreeContentView()
case .basic:
BasicContentView()
case .pro:
ProContentView()
case .premium:
PremiumContentView()
}
}
.onAppear {
updateUserTier()
}
}
private func updateUserTier() {
if InAppKit.shared.hasAccess(to: "com.app.premium") {
userTier = .premium
} else if InAppKit.shared.hasAccess(to: "com.app.pro") {
userTier = .pro
} else if InAppKit.shared.hasAccess(to: "com.app.basic") {
userTier = .basic
} else {
userTier = .free
}
}
}#if DEBUG
struct DebugPaywallView: View {
let context: PaywallContext
var body: some View {
VStack {
Text("DEBUG: Paywall")
.foregroundColor(.red)
Text("Triggered by: \\(context.triggeredBy ?? "unknown")")
ForEach(context.availableProducts, id: \\.id) { product in
VStack {
Text(product.id)
Text("Price: \\(product.displayPrice)")
Button("Simulate Purchase") {
// Simulate purchase for testing
UserDefaults.standard.set(true, forKey: "purchased_\\(product.id)")
}
}
.padding()
.border(Color.gray)
}
}
}
}
#endifNext Steps:
- API Reference → Complete technical documentation
- Monetization Patterns → Choose the right strategy
- Getting Started → Learn the basics