Skip to content

SwiftUI-native framework that simplifies StoreKit integration with a fluent, chainable API and feature-first approach

License

Notifications You must be signed in to change notification settings

tddworks/InAppKit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

9 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

InAppKit

Swift Package Manager Platform Swift License

Add in-app purchases to your SwiftUI app in minutes, not hours. No StoreKit complexity, just simple code that works.

๐Ÿš€ InAppKit - Because in-app purchases shouldn't be complicated.

๐Ÿ“– Table of Contents

โœจ What You Get

  • ๐Ÿ”— Simple Setup - Add .withPurchases() to any view and you're done
  • ๐ŸŽฏ Smart Gating - Use .requiresPurchase() on any button or view
  • ๐ŸŽจ Beautiful Paywalls - Professional upgrade screens included
  • โšก Zero Config - Works with App Store Connect automatically
  • ๐Ÿ”„ Handles Everything - Purchases, restoration, validation - all automatic
  • ๐Ÿ“ฑ Works Everywhere - iOS, macOS, watchOS, tvOS
  • ๐ŸŽจ Ready-to-Use UI - Premium badges and upgrade flows included

๐Ÿšง Requirements

  • iOS 17.0+ / macOS 15.0+ / watchOS 10.0+ / tvOS 17.0+
  • Xcode 15.0+
  • Swift 6.1+

๐Ÿ“ฆ Installation

Swift Package Manager

Add InAppKit to your project using Xcode:

  1. Go to File โ†’ Add Package Dependencies
  2. Enter the repository URL: https://github.com/tddworks/InAppKit.git
  3. Select Up to Next Major Version starting from 1.0.0

Or add it to your Package.swift:

dependencies: [
    .package(url: "https://github.com/tddworks/InAppKit.git", from: "1.0.0")
]

๐Ÿš€ Quick Start

1. Add InAppKit (2 lines)

ContentView()
    .withPurchases("com.yourapp.pro")

Or with Product array if you need features:

ContentView()
    .withPurchases(products: [Product("com.yourapp.pro")])

2. Gate any feature (1 line)

Button("Premium Feature") { doPremiumThing() }
    .requiresPurchase()

That's it! ๐ŸŽ‰ InAppKit handles the rest automatically.


Want More Control?

๐Ÿ“‹ Define specific features
enum AppFeature: String, InAppKit.AppFeature {
    case removeAds = "remove_ads"
    case cloudSync = "cloud_sync"
    case exportPDF = "export_pdf"
}

ContentView()
    .withPurchases(products: [
        Product("com.yourapp.pro", AppFeature.allCases)
    ])
๐ŸŽจ Customize the paywall
ContentView()
    .withPurchases("com.yourapp.pro")
    .withPaywall { context in
        Text("Unlock \(context.triggeredBy ?? "premium features")")
        // Your custom paywall UI here
    }
๐ŸŽฏ Smart conditional upgrades
Button("Save Document") { save() }
    .requiresPurchase(AppFeature.cloudSync, when: documentCount > 5)

๐Ÿ’ก Real-World Examples

๐Ÿ“ธ Photo App: Remove Watermark

Button("Export Photo") { exportPhoto() }
    .requiresPurchase()

Result: User sees upgrade screen when they try to export

โ˜๏ธ Note App: Storage Limit

Button("Save Note") { saveNote() }
    .requiresPurchase(when: noteCount > 50)

Result: After 50 notes, upgrade prompt appears

๐ŸŽจ Design App: Professional Features

Button("Export for Client") { exportForClient() }
    .requiresPurchase(AppFeature.clientTools)

Result: Business users see relevant upgrade options

๐Ÿ“– How It Works

Two Main Concepts

1. Products - What users can buy 2. Features - What gets unlocked

// Product: "Pro Version" 
// Features: No ads, cloud sync, export
Product("com.app.pro", [.noAds, .cloudSync, .export])

Choose Your Monetization Strategy

InAppKit adapts to how your users think about value, not just technical features:

๐ŸŽฏ "Try Before You Buy" (Freemium)

Perfect for: Apps where users need to experience value first

// Users get core functionality, pay for advanced features
Product("com.photoapp.pro", [AppFeature.advancedFilters, AppFeature.cloudStorage])

User Mental Model: "I love this app, now I want more powerful features"

  • โœ… Users understand the upgrade value
  • โœ… Natural conversion from free to paid
  • โœ… Low barrier to entry

๐Ÿ† "Good, Better, Best" (Tiered Value)

Perfect for: Different user types with different needs

// Starter: Casual users
Product("com.designapp.starter", [AppFeature.basicTemplates, AppFeature.export])

// Professional: Power users  
Product("com.designapp.pro", [AppFeature.premiumTemplates, AppFeature.advancedExport, AppFeature.teamSharing])

// Enterprise: Teams & organizations
Product("com.designapp.enterprise", AppFeature.allCases)

User Mental Model: "I know what level of user I am, show me my tier"

  • โœ… Clear value differentiation
  • โœ… Room for users to grow
  • โœ… Predictable pricing psychology

๐Ÿ“ฆ "Feature Packs" (Bundled Solutions)

Perfect for: Specialized workflows and use cases

// Content Creator Pack
Product("com.videoapp.creator", [AppFeature.advancedEditing, AppFeature.exportFormats, AppFeature.musicLibrary])

// Business Pack
Product("com.videoapp.business", [AppFeature.branding, AppFeature.analytics, AppFeature.teamWorkspace])

User Mental Model: "I need tools for my specific workflow"

  • โœ… Solves complete user problems
  • โœ… Higher perceived value
  • โœ… Targets specific personas

โฐ "Ongoing Value" (Subscriptions)

Perfect for: Services that provide continuous value

// Monthly: Try it out
Product("com.cloudapp.monthly", [AppFeature.cloudSync, AppFeature.prioritySupport])

// Annual: Committed users
Product("com.cloudapp.annual", [AppFeature.cloudSync, AppFeature.prioritySupport, AppFeature.advancedFeatures])

User Mental Model: "I'm paying for ongoing service and updates"

  • โœ… Matches recurring value delivery
  • โœ… Lower monthly commitment
  • โœ… Incentivizes annual savings

API Design Philosophy

InAppKit uses a fluent chainable API for clean, readable configuration:

ContentView()
    .withPurchases(products: [Product("com.app.pro", AppFeature.allCases)])
    .withPaywall { context in CustomPaywall(context) }
    .withTerms { TermsView() }
    .withPrivacy { PrivacyView() }

๐ŸŽจ Paywall & UI Customization

Default Paywall

InAppKit includes a beautiful, modern paywall out of the box:

// Use default paywall with fluent API
ContentView()
    .withPurchases(products: products)

Custom Paywall

Create your own paywall with full context information:

// Context-aware paywall with fluent API
ContentView()
    .withPurchases(products: products)
    .withPaywall { context in
        VStack {
            Text("Upgrade to unlock \(context.triggeredBy ?? "premium features")")
            
            ForEach(context.availableProducts, id: \.self) { product in
                Button(product.displayName) {
                    Task {
                        try await InAppKit.shared.purchase(product)
                    }
                }
            }
            
            if let recommended = context.recommendedProduct {
                Text("Recommended: \(recommended.displayName)")
            }
        }
    }

Paywall Context

The PaywallContext provides rich information about how the paywall was triggered:

public struct PaywallContext {
    public let triggeredBy: String?        // What action triggered this
    public let availableProducts: [StoreKit.Product] // Products that can be purchased  
    public let recommendedProduct: StoreKit.Product?  // Best product recommendation
}

Built-in UI Components

InAppKit provides several built-in UI components:

  • PaywallView - Modern, animated paywall with product selection
  • PurchaseRequiredBadge - Premium crown badge overlay
  • TermsPrivacyFooter - Configurable footer for terms and privacy
  • FeatureRow - Styled feature list rows
  • ModernProductCard - Product selection cards

Using UI Components

// Add premium badge to any view
MyCustomView()
    .withTermsAndPrivacy()

// Use paywall directly
PaywallView()

// Built-in premium badge appears automatically with .requiresPurchase()

๐Ÿ” Type-Safe Premium Gating

All the Ways to Gate Features

// Basic - any premium purchase required
.requiresPurchase()

// Specific feature required  
.requiresPurchase(AppFeature.export)

// Only when condition is true
.requiresPurchase(when: fileCount > 10)

// Combine feature + condition
.requiresPurchase(AppFeature.export, when: fileSize > 5.mb)

What happens: Premium features show a badge, then display your paywall when tapped.

๐Ÿ—๏ธ Under the Hood

Main Components

InAppKit.shared - Handles all the StoreKit complexity

// Check what user owns
InAppKit.shared.hasAnyPurchase
InAppKit.shared.isPurchased("com.app.pro")

// Manual purchase (usually not needed)
await InAppKit.shared.purchase(product)

Two View Modifiers:

  • .withPurchases("product-id") - Set up your products
  • .requiresPurchase() - Gate any feature

Available Variants:

// Simple: Just a product ID
.withPurchases("com.app.pro")

// Advanced: Products with specific features
.withPurchases(products: [Product("com.app.pro", AppFeature.allCases)])

๐ŸŽฏ Advanced Features

Multiple Product Tiers in Practice

// E-commerce App Example
ContentView()
    .withPurchases(products: [
        Product("com.shopapp.basic", [AppFeature.trackOrders, AppFeature.wishlist]),
        Product("com.shopapp.plus", [AppFeature.trackOrders, AppFeature.wishlist, AppFeature.fastShipping]),
        Product("com.shopapp.premium", AppFeature.allCases)
    ])
    .withPaywall { context in
        ShopPaywallView(context: context)
    }

// Productivity App Example  
ContentView()
    .withPurchases(products: [
        Product("com.prodapp.starter", [AppFeature.basicProjects]),
        Product("com.prodapp.professional", [AppFeature.basicProjects, AppFeature.teamCollaboration, AppFeature.advancedReports]),
        Product("com.prodapp.enterprise", AppFeature.allCases)
    ])

// Media App Subscription Tiers
ContentView()
    .withPurchases(products: [
        Product("com.mediaapp.monthly", [AppFeature.hdStreaming, AppFeature.downloads]),
        Product("com.mediaapp.annual", [AppFeature.hdStreaming, AppFeature.downloads, AppFeature.offlineMode, AppFeature.familySharing])
    ])

Feature Registration

Features are automatically registered when you use the fluent API, but you can also register them manually:

InAppKit.shared.registerFeature(
    AppFeature.advanced, 
    productIds: ["com.app.pro"]
)

Custom Premium Modifiers

Create your own premium gating logic:

extension View {
    func myCustomPremium<T: Hashable>(_ feature: T) -> some View {
        self.modifier(MyPremiumModifier(feature: feature))
    }
}

๐Ÿ› ๏ธ Troubleshooting

Error Handling

InAppKit handles errors gracefully and provides debugging information:

// Check for errors
if let error = InAppKit.shared.purchaseError {
    // Handle purchase error
}

// Purchase states
if InAppKit.shared.isPurchasing {
    // Show loading state
}

Debugging

Enable detailed logging to debug StoreKit issues:

// InAppKit uses OSLog with category "statistics"
// Filter in Console.app or Xcode console for "statistics" messages

โš ๏ธ Important Notes

StoreKit Configuration

  • Products must be configured in App Store Connect before testing
  • Test with Sandbox accounts during development
  • Features are automatically registered when using the fluent API
  • Debug builds provide helpful warnings for unregistered features

Testing & Validation

#if DEBUG
// Test purchases in development
InAppKit.shared.simulatePurchase("com.myapp.pro")
InAppKit.shared.clearPurchases() // Reset for testing
#endif

๐Ÿ”’ Privacy & Security

InAppKit follows Apple's privacy guidelines:

  • No personal data collection
  • All transactions handled by StoreKit
  • Local feature validation only
  • No analytics or tracking

๐ŸŽฏ Choose Your App's Monetization Pattern

๐Ÿ“ฑ App Type: What Problem Do You Solve?

๐ŸŽจ Creative Apps (Photo, Video, Design)

User Mindset: "I want to create something amazing"

// Problem: User creates something, wants to share/export without limitations
enum CreativeFeature: String, InAppKit.AppFeature {
    case removeWatermark = "no_watermark"
    case hdExport = "hd_export"
    case premiumFilters = "premium_filters"
    case cloudStorage = "cloud_storage"
}

// Solution: Let them create first, then offer enhancement
ContentView()
    .withPurchases(products: [
        Product("com.creative.pro", CreativeFeature.allCases)
    ])
    .withPaywall { context in
        CreativePaywallView(triggeredBy: context.triggeredBy)
    }

๐Ÿ“Š Productivity Apps (Notes, Tasks, Documents)

User Mindset: "I need this to work better/faster"

// Problem: User accumulates data, needs more power/space
enum ProductivityFeature: String, InAppKit.AppFeature {
    case unlimitedItems = "unlimited_items"
    case advancedSearch = "advanced_search"
    case teamSync = "team_sync"
    case prioritySync = "priority_sync"
}

// Solution: Usage-based upgrades feel natural
Button("Add Project") {
    addProject()
}
.requiresPurchase(ProductivityFeature.unlimitedItems, when: projectCount > 5)

๐ŸŽฎ Entertainment Apps (Games, Media, Social)

User Mindset: "I want more fun/content"

// Problem: User enjoys experience, wants more
enum EntertainmentFeature: String, InAppKit.AppFeature {
    case premiumContent = "premium_content"
    case noAds = "ad_free"
    case earlyAccess = "early_access"
    case specialFeatures = "special_features"
}

// Solution: Offer "more of what they love"
ContentView()
    .withPurchases(products: [
        Product("com.game.premium", EntertainmentFeature.allCases)
    ])

๐Ÿ’ผ Business Apps (CRM, Finance, Analytics)

User Mindset: "I need this for my business success"

// Problem: User needs professional features for work
enum BusinessFeature: String, InAppKit.AppFeature {
    case teamAccounts = "team_accounts"
    case advancedReports = "advanced_reports"
    case apiAccess = "api_access"
    case prioritySupport = "priority_support"
}

// Solution: Clear business tiers
ContentView()
    .withPurchases(products: [
        Product("com.business.professional", [BusinessFeature.advancedReports, BusinessFeature.prioritySupport]),
        Product("com.business.enterprise", BusinessFeature.allCases)
    ])

๐Ÿง  User Psychology Patterns

"I'm Invested" Pattern

// User has data/content โ†’ natural to protect/enhance it
.requiresPurchase(AppFeature.backup, when: userContentCount > 20)

"I'm Professional" Pattern

// User identity drives purchase โ†’ business features feel necessary
.requiresPurchase(AppFeature.clientSharing, when: isBusinessUser)

"I Hit a Wall" Pattern

// User reaches limitation โ†’ upgrade removes friction
.requiresPurchase(AppFeature.moreStorage, when: storageUsed > freeLimit)

"I Want More" Pattern

// User enjoys free features โ†’ wants enhanced experience
.requiresPurchase(AppFeature.premiumContent)

Complete Implementation: Photo Editing App

import SwiftUI
import InAppKit

// Define app features aligned with business tiers
enum AppFeature: String, InAppKit.AppFeature {
    // Basic tier features
    case basicFilters = "basic_filters"
    case cropResize = "crop_resize"
    
    // Pro tier features
    case advancedFilters = "advanced_filters"
    case batchProcessing = "batch_processing"
    case cloudStorage = "cloud_storage"
    
    // Professional tier features
    case rawSupport = "raw_support"
    case teamCollaboration = "team_collaboration"
    case prioritySupport = "priority_support"
    
    // Enterprise tier features
    case apiAccess = "api_access"
    case whiteLabeling = "white_labeling"
    case ssoIntegration = "sso_integration"
}

@main
struct PhotoEditApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .withPurchases(products: [
                    // Freemium: Basic features included free
                    Product("com.photoapp.pro", [
                        AppFeature.advancedFilters, 
                        AppFeature.batchProcessing, 
                        AppFeature.cloudStorage
                    ]),
                    Product("com.photoapp.professional", [
                        AppFeature.advancedFilters, 
                        AppFeature.batchProcessing, 
                        AppFeature.cloudStorage,
                        AppFeature.rawSupport, 
                        AppFeature.teamCollaboration, 
                        AppFeature.prioritySupport
                    ]),
                    Product("com.photoapp.enterprise", AppFeature.allCases)
                ])
                .withPaywall { context in
                    PhotoAppPaywallView(
                        triggeredBy: context.triggeredBy,
                        products: context.availableProducts
                    )
                }
        }
    }
}

struct ContentView: View {
    @State private var imageCount = 1
    @State private var isTeamMember = false
    
    var body: some View {
        VStack(spacing: 20) {
            // Always free - basic features
            Button("Apply Basic Filter") { applyBasicFilter() }
            Button("Crop & Resize") { cropAndResize() }
            
            // Pro tier gating
            Button("Advanced AI Filter") { applyAIFilter() }
                .requiresPurchase(AppFeature.advancedFilters)
            
            Button("Batch Process") { batchProcess() }
                .requiresPurchase(AppFeature.batchProcessing, when: imageCount > 5)
            
            // Professional tier gating
            Button("Edit RAW Files") { editRAW() }
                .requiresPurchase(AppFeature.rawSupport)
            
            Button("Team Collaboration") { openTeamPanel() }
                .requiresPurchase(AppFeature.teamCollaboration, when: isTeamMember)
            
            // Enterprise tier gating
            Button("API Access") { configureAPI() }
                .requiresPurchase(AppFeature.apiAccess)
        }
    }
}

๐Ÿค Contributing

We welcome contributions! Here's how to get started:

Development Setup

  1. Fork the repository
  2. Clone your fork: git clone https://github.com/yourusername/InAppKit.git
  3. Create a feature branch: git checkout -b feature/amazing-feature
  4. Make your changes and add tests
  5. Run tests: swift test
  6. Commit your changes: git commit -m 'Add amazing feature'
  7. Push to your branch: git push origin feature/amazing-feature
  8. Open a Pull Request

Code Style

  • Follow Swift API Design Guidelines
  • Use meaningful variable and function names
  • Add documentation comments for public APIs
  • Maintain backward compatibility when possible

๐Ÿ› Bug Reports & Feature Requests

Please use GitHub Issues to report bugs or request features:

  • Bug Reports: Include steps to reproduce, expected vs actual behavior
  • Feature Requests: Describe the use case and proposed solution
  • Questions: Check existing issues first, then create a new discussion

๐ŸŒŸ Show Your Support

If InAppKit helps your project, please consider:

  • โญ Star this repository
  • ๐Ÿ› Report bugs and suggest features
  • ๐Ÿ“– Improve documentation
  • ๐Ÿ’ฌ Share your experience with the community

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

  • Built on Apple's StoreKit 2
  • Inspired by SwiftUI's declarative approach
  • Designed for modern iOS development

InAppKit - Because in-app purchases shouldn't be complicated. ๐Ÿš€

Made with โค๏ธ by the TDDWorks team

About

SwiftUI-native framework that simplifies StoreKit integration with a fluent, chainable API and feature-first approach

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

  •  

Packages

No packages published

Languages