Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,30 @@ This is a view containing all of the in-app purchase options. It can be created
InAppPurchaseView(
includeNavigationStack: true,
includeDismissButton: true,
onPurchase onPurchaseAction: nil
onPurchase onPurchaseAction: nil,
contentOrder: InAppPurchaseViewContent.defaultOrder
)
```

It has options to include its own navigation stack and dismiss button which is useful for when displaying in a sheet instead of in an existing navigation stack. A separate purchase action can be added too.

The order of content in the purchase view can also be customised. Supports reordering the order and adding custom SwiftUI content:

```swift
InAppPurchaseView(
contentOrder: [
.header,
.custom(AnyView(
Text("Limited-time discount")
.font(.headline)
)),
.tiers,
.features,
.additionalOptions
]
)
```

### LockedInAppPurchaseFeatureButton

This is a view containing a button that performs an action when a user is subscribed or is locked when not. It can be created like so:
Expand Down
94 changes: 72 additions & 22 deletions Sources/InAppPurchaseKit/Activities/InAppPurchaseView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public struct InAppPurchaseView: View {
/// will be performed. If an action is set, you will need to also dismiss the view. This
/// is handled automatically when no action is set.
private let onPurchaseAction: (@Sendable () -> Void)?

/// The order that content should be displayed in the purchase view.
private let contentOrder: [InAppPurchaseViewContent]

/// The current in-app purchase tier that has been selected in the list.
@State private var selectedTier: PurchaseTier?
Expand All @@ -46,14 +49,18 @@ public struct InAppPurchaseView: View {
/// to the action set in `InAppPurchaseKitConfiguration` but both
/// will be performed. If an action is set, you will need to also dismiss the view. This
/// is handled automatically when no action is set. Defaults to `nil`.
/// - contentOrder: The order that content should be displayed in the purchase view.
/// Defaults to `InAppPurchaseViewContent.defaultOrder`.
public init(
includeNavigationStack: Bool = true,
includeDismissButton: Bool = true,
onPurchase onPurchaseAction: (@Sendable () -> Void)? = nil
onPurchase onPurchaseAction: (@Sendable () -> Void)? = nil,
contentOrder: [InAppPurchaseViewContent] = InAppPurchaseViewContent.defaultOrder
) {
self.includeNavigationStack = includeNavigationStack
self.includeDismissButton = includeDismissButton
self.onPurchaseAction = onPurchaseAction
self.contentOrder = contentOrder
}

public var body: some View {
Expand Down Expand Up @@ -125,27 +132,8 @@ public struct InAppPurchaseView: View {
private var subscriptionViewContents: some View {
ScrollView {
VStack(spacing: SizingConstants.mainSpacing) {
InAppPurchaseHeaderView(
configuration: inAppPurchase.configuration
)
.frame(maxWidth: .infinity)

TiersView(
selectedTier: $selectedTier,
ignorePurchaseState: $ignorePurchaseState
)

VStack(spacing: SizingConstants.mainSpacing / 2) {
Group {
Divider()
FeaturesView(inAppPurchase.configuration.features)
Divider()
}
.frame(maxWidth: SizingConstants.mainContentWidth)

AdditionalOptionsView(
ignorePurchaseState: $ignorePurchaseState
)
ForEach(contentOrder.indices, id: \.self) { index in
renderView(for: contentOrder[index])
}
}
.frame(maxWidth: .infinity)
Expand All @@ -160,6 +148,41 @@ public struct InAppPurchaseView: View {
}
}

@ViewBuilder
private func renderView(for content: InAppPurchaseViewContent) -> some View {
switch content {
case .header:
InAppPurchaseHeaderView(
configuration: inAppPurchase.configuration
)
.frame(maxWidth: .infinity)

case .tiers:
TiersView(
selectedTier: $selectedTier,
ignorePurchaseState: $ignorePurchaseState
)

case .features:
VStack(spacing: SizingConstants.mainSpacing / 2) {
Group {
Divider()
FeaturesView(inAppPurchase.configuration.features)
Divider()
}
}
.frame(maxWidth: SizingConstants.mainContentWidth)

case .additionalOptions:
AdditionalOptionsView(
ignorePurchaseState: $ignorePurchaseState
)

case .custom(let customContent):
customContent
}
}


// MARK: - Update

Expand Down Expand Up @@ -218,3 +241,30 @@ public struct InAppPurchaseView: View {
InAppPurchaseView()
.environment(inAppPurchase)
}

#Preview("InAppPurchaseView+CustomContent") {
let inAppPurchase = InAppPurchaseKit.configure(with: .example)

InAppPurchaseView(
contentOrder: [
.header,
.custom(AnyView(
VStack(spacing: 8) {
Text("Limited-time discount")
.font(.headline)
Text("Save 20% with App Store Code XYZ")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
)),
.tiers,
.features,
.additionalOptions
]
)
.environment(inAppPurchase)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// InAppPurchaseViewContent.swift
// InAppPurchaseKit
//
// Created by Adam Foot on 08/05/2026.
//

import SwiftUI

public enum InAppPurchaseViewContent {
case header
case tiers
case features
case additionalOptions
case custom(AnyView)

public static var defaultOrder: [InAppPurchaseViewContent] {
[
.header,
.tiers,
.features,
.additionalOptions
]
}
}