Client-Side Integration

Turnkey In‑App Purchases for iOS

Breeze Swift SDK lets you migrate from StoreKit with minimal code changes while unlocking advanced payment features: multiple payment methods, unified receipts, built‑in risk & fraud management, and more.

Installation

Xcode UI

  1. In Xcode choose File ▸ Add Packages
  2. Enter the repository URL: https://github.com/beamo-co/breeze-swift.git
  3. Select Up to Next Major Version and confirm the latest tag.
  4. Click Add Package.

Package.swift

dependencies: [
    .package(url: "https://github.com/beamo-co/breeze-swift.git", from: "0.0.4")
]

Quick Start

Breeze’s API mirrors the StoreKit model classes so you can switch over with minimal changes. The SDK complies with US Apple’s App Store Review Guidelines for “External Payment Links” by redirecting users to an external browser to complete payment and then validating the receipt on return.

1. SDK Initialization

import Breeze

@main
struct exampleApp: App {
    init() {
        Breeze.shared.configure(with: BreezeConfiguration(
            apiKey: "API_KEY",
            appScheme: "testapp://"
            userId: UIDevice.current.identifierForVendor?.uuidString ?? ""
        ))
    }
}

Configuration

FieldDescription
apiKeyBreeze client API Key, contact our support team at [email protected] to get one. This key is different from the server API key and only acts as a client identifier that is safe to expose on the client side.
appSchemeDeep link scheme or universal link for Breeze SDK to redirect back to your app after a successful payment. You can follow apple docs to learn more.
userIdUnique user identifier for the user who made the purchase. This must be unique and constant for the purchased product to be reflected properly.

2. Setup Redirection Listener

Configure a redirection listener in your main application view to handle payment redirection. This listener is essential for processing redirects from the Breeze payment page back to your application. The Breeze SDK will automatically validate both the redirect URL and payment receipt, then invoke the purchase callback upon successful validation.

var body: some Scene {
  WindowGroup {
    ContentView()
    .onOpenURL { url in
      Breeze.shared.verifyUrl(url)
    }
  }
}

3. Fetch products

BreezeProduct has the same properties as StoreKit.Product, so your existing parsing logic can remain unchanged. Note that Breeze products are backward compatible, meaning they will also return the original StoreKit products.

let breezeProducts = try await Breeze.shared.products(["IAP_PRODUCT_ID"])

4. Make payment

You can call the Breeze purchase function and pass the success callback that will be called after the user redirects back to the app and Breeze has successfully validated the receipt.

func purchase(_ product: BreezeProduct) async throws {
   try await product.purchase(using: Breeze.shared, onSuccess: onPurchaseSuccessful)
}

func onPurchaseSuccessful(_ transaction: BreezeTransaction){
   //Your logic on successful purchase
   await transaction.finish(using: Breeze.shared) //always finish transaction
}

Set pending transaction listener

If the user doesn’t return to the app after payment, Breeze stores the pending transaction locally. Call this early in the component initialization phase.

init() {
    Breeze.shared.setPurchaseCallback(onSuccess: onPurchaseSuccessful)
}

5. Get entitlements

You can call the Breeze getEntitlement function to retrieve all entitlements that include purchased non-consumable, auto-renewable, and non-renewable products. The returned entitlements combine both Breeze purchases and existing Apple StoreKit purchases, ensuring backward compatibility with products bought through the original Apple system.

func updatePurchaseStatus(){
    let allEntitlements = await Breeze.shared.getEntitlements()

    for transaction in allEntitlements {
        switch transaction.productType {
            case .nonConsumable:
            //handle purchase nonConsumable product

            case .nonRenewable:
            //handle purchase nonConsumable product

            case .autoRenewable:
            //handle purchase nonConsumable product

        default:
            break
        }
    }
}

Full Code Overview

// YourMainApp.swift
import SwiftUI
import Breeze

@main
struct SKDemoApp: App {
  init() {
      Breeze.shared.configure(with: BreezeConfiguration(
          apiKey: "BREEZE_API_KEY",
          appScheme: "appscheme://", //deep link scheme or universal link
          userId: String(UIDevice.current.identifierForVendor?.uuidString ?? ""), //set unique user id
      ))
  }

  var body: some Scene {
      WindowGroup {
          MyCarsView()
          .onOpenURL { url in
              Breeze.shared.verifyUrl(url) //set Breeze redirection listener
          }
      }
  }
}
// YourStoreLogic.swift
import Foundation
import StoreKit
import Breeze

class Store: ObservableObject {
    //your original IAP product IDs
    private let productIds = ["nonConsumable.standard", "consumable.standard", "nonRenewing.standard"]
    
    init() {
        Task {
            // Listen for Breeze Listener
            await Breeze.shared.setPurchaseCallback(onSuccess: {breezeTransaction in
                Task {
                    try await self.onPurchaseSuccessful(breezeTransaction)
                }
            })
            
            // During store initialization, request products to load the product list.
            await requestProducts()

            // Deliver products that the customer purchases.
            await updateCustomerProductStatus()
        }
    }

    @MainActor
    func requestProducts() async {
        do {
            // Request product list
            let storeProducts = try await Breeze.shared.products(productIds: productIds)

            for product in storeProducts {
                switch product.type {
                case .consumable:
                    //add consumable product to list
                case .nonConsumable:
                    //add non consumable product to list
                case .autoRenewable:
                    //add auto renewable product to list
                case .nonRenewable:
                    //add non renewable product to list
                }
            }
        } catch {
            print("Failed product request from the App Store server. \(error)")
        }
    }

    func purchase(_ product: BreezeProduct,  onSuccess: (() -> Void)? = nil) async throws {
         try await product.purchase(using: Breeze.shared, onSuccess: { transaction in
            Task {
                try await self.onPurchaseSuccessful(transaction)
            }
            onSuccess?()
        })
    }

    func onPurchaseSuccessful(_ transaction: BreezeTransaction) async throws {
        //Add your purchase successful logic here
        await updateCustomerProductStatus()
        
        // Always finish a transaction.
        if(transaction.skTransaction != nil){
            await transaction.skTransaction!.finish() //using storekit tx
            return
        } else {
            await Breeze.shared.finish(transaction) //using breeze tx
        }
    }

    @MainActor
    func updateCustomerProductStatus() async {
        //get all entitlements
        let allEntitlements = await Breeze.shared.getEntitlements()
        
        for transaction in allEntitlements {
            switch transaction.productType {
            case .nonConsumable:
                //Add purchased non consumable product to list
            case .nonRenewable:
                //Add purchased non renewable product to list
            case .autoRenewable:
                //Add purchased auto renewable product to list
            default:
                break
            }
        }
    }
}

Sample App

A fully‑worked example is available in the our github