diff --git a/README.md b/README.md index c107fda..4a02138 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/Sources/InAppPurchaseKit/Activities/InAppPurchaseView.swift b/Sources/InAppPurchaseKit/Activities/InAppPurchaseView.swift index 4fe63c6..bee504a 100644 --- a/Sources/InAppPurchaseKit/Activities/InAppPurchaseView.swift +++ b/Sources/InAppPurchaseKit/Activities/InAppPurchaseView.swift @@ -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? @@ -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 { @@ -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) @@ -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 @@ -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) +} diff --git a/Sources/InAppPurchaseKit/Models/Configuration/InAppPurchaseViewContent.swift b/Sources/InAppPurchaseKit/Models/Configuration/InAppPurchaseViewContent.swift new file mode 100644 index 0000000..181f592 --- /dev/null +++ b/Sources/InAppPurchaseKit/Models/Configuration/InAppPurchaseViewContent.swift @@ -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 + ] + } +}