Skip to content

Commit 1eea1ea

Browse files
authored
feat: add example app with enhanced StoreServicePreview (#1)
- Add ExampleApp executable target for iOS and macOS - Enhance StoreServicePreview with configurable mock data - Add MockProductData struct for flexible test data - Add withDefaultMockData() factory for realistic previews - Update DeveloperSupportStoreView previews to use mock data - Fix card width constraint for better layout
1 parent 14b2b8d commit 1eea1ea

5 files changed

Lines changed: 320 additions & 10 deletions

File tree

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ let package = Package(
2424
dependencies: ["StoreHelper"],
2525
resources: [
2626
.process("Resources"),
27+
],
28+
),
29+
.executableTarget(
30+
name: "ExampleApp",
31+
dependencies: ["DeveloperSupportStore"],
32+
linkerSettings: [
33+
.linkedFramework("AppKit", .when(platforms: [.macOS])),
34+
.linkedFramework("UIKit", .when(platforms: [.iOS])),
35+
.linkedFramework("SwiftUI"),
2736
]
2837
),
2938
.testTarget(

Sources/DeveloperSupportStore/Services/StoreService.swift

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,31 +91,141 @@ public final class StoreService: StoreServiceProtocol {
9191

9292
// MARK: - Preview Service
9393

94+
/// Mock product data for previews and testing.
95+
public struct MockProductData: Sendable {
96+
/// The product identifier.
97+
public let productId: String
98+
/// The display name.
99+
public let name: String
100+
/// The product description.
101+
public let description: String
102+
/// The formatted price string.
103+
public let price: String
104+
105+
/// Creates mock product data.
106+
public init(productId: String, name: String, description: String, price: String) {
107+
self.productId = productId
108+
self.name = name
109+
self.description = description
110+
self.price = price
111+
}
112+
}
113+
94114
/// A preview implementation of `StoreServiceProtocol` for SwiftUI previews and testing.
95115
@MainActor
96116
public final class StoreServicePreview: StoreServiceProtocol {
97117
public var hasActiveSubscription: Bool
98118

99-
/// Creates a preview store service.
119+
private let mockProducts: [String: MockProductData]
120+
private let purchaseResult: StorePurchaseResult
121+
private let syncDelay: Duration
122+
123+
/// Creates a preview store service with configurable mock data.
100124
///
101-
/// - Parameter hasActiveSubscription: Whether to simulate an active subscription.
102-
public init(hasActiveSubscription: Bool = false) {
125+
/// - Parameters:
126+
/// - hasActiveSubscription: Whether to simulate an active subscription.
127+
/// - mockProducts: Array of mock product data to use.
128+
/// - purchaseResult: The result to return from purchase attempts.
129+
/// - syncDelay: Artificial delay for sync operations (simulates network).
130+
public init(
131+
hasActiveSubscription: Bool = false,
132+
mockProducts: [MockProductData] = [],
133+
purchaseResult: StorePurchaseResult = .success(productId: "mock.product"),
134+
syncDelay: Duration = .zero
135+
) {
103136
self.hasActiveSubscription = hasActiveSubscription
137+
self.mockProducts = Dictionary(uniqueKeysWithValues: mockProducts.map { ($0.productId, $0) })
138+
self.purchaseResult = purchaseResult
139+
self.syncDelay = syncDelay
104140
}
105141

106142
public func syncStoreData() async throws {
107-
// No-op for preview
143+
if syncDelay > .zero {
144+
try await Task.sleep(for: syncDelay)
145+
}
108146
}
109147

110148
public func purchase(_ productId: String) async throws -> StorePurchaseResult {
111-
.success(productId: productId)
149+
if syncDelay > .zero {
150+
try await Task.sleep(for: syncDelay)
151+
}
152+
153+
switch purchaseResult {
154+
case .success:
155+
return .success(productId: productId)
156+
default:
157+
return purchaseResult
158+
}
112159
}
113160

114-
public func info(for _: String) throws -> StoreProductInfo {
115-
StoreProductInfo(
161+
public func info(for productId: String) throws -> StoreProductInfo {
162+
if let mockProduct = mockProducts[productId] {
163+
return StoreProductInfo(
164+
name: mockProduct.name,
165+
description: mockProduct.description,
166+
price: mockProduct.price
167+
)
168+
}
169+
170+
return StoreProductInfo(
116171
name: "Preview Product",
117172
description: "This is a preview product description.",
118173
price: "$0.99"
119174
)
120175
}
121176
}
177+
178+
// MARK: - Default Mock Data
179+
180+
extension StoreServicePreview {
181+
/// Creates a preview service with realistic mock data for a tip jar / subscription store.
182+
///
183+
/// - Parameters:
184+
/// - subscriptionIds: The subscription product IDs to mock.
185+
/// - inAppPurchaseIds: The one-time purchase product IDs to mock.
186+
/// - hasActiveSubscription: Whether to simulate an active subscription.
187+
/// - Returns: A configured preview service.
188+
public static func withDefaultMockData(
189+
subscriptionIds: [String] = ["com.example.subscription.monthly"],
190+
inAppPurchaseIds: [String] = ["com.example.tip.small", "com.example.tip.large"],
191+
hasActiveSubscription: Bool = false
192+
) -> StoreServicePreview {
193+
var mockProducts: [MockProductData] = []
194+
195+
// Add subscription mocks
196+
for (index, subscriptionId) in subscriptionIds.enumerated() {
197+
let tier = index == 0 ? "Monthly" : "Yearly"
198+
let price = index == 0 ? "$2.99" : "$24.99"
199+
mockProducts.append(MockProductData(
200+
productId: subscriptionId,
201+
name: "\(tier) Support",
202+
description: "Support ongoing development with a \(tier.lowercased()) contribution.",
203+
price: price
204+
))
205+
}
206+
207+
// Add one-time purchase mocks
208+
let tipNames = ["Small Tip", "Large Tip", "Generous Tip"]
209+
let tipDescriptions = [
210+
"A small token of appreciation.",
211+
"A generous contribution to development.",
212+
"An amazing show of support!"
213+
]
214+
let tipPrices = ["$0.99", "$4.99", "$9.99"]
215+
216+
for (index, purchaseId) in inAppPurchaseIds.enumerated() {
217+
let safeIndex = min(index, tipNames.count - 1)
218+
mockProducts.append(MockProductData(
219+
productId: purchaseId,
220+
name: tipNames[safeIndex],
221+
description: tipDescriptions[safeIndex],
222+
price: tipPrices[safeIndex]
223+
))
224+
}
225+
226+
return StoreServicePreview(
227+
hasActiveSubscription: hasActiveSubscription,
228+
mockProducts: mockProducts
229+
)
230+
}
231+
}

Sources/DeveloperSupportStore/Views/DeveloperSupportStoreView.swift

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public struct DeveloperSupportStoreView: View {
271271
.foregroundStyle(colors.primaryText)
272272
}
273273
.padding(layout.paddingDefault)
274-
.frame(maxWidth: .infinity, minHeight: 180)
274+
.frame(maxWidth: 220, minHeight: 180)
275275
.background(
276276
RoundedRectangle(cornerRadius: layout.radiusDefault)
277277
.fill(colors.secondaryBackground)
@@ -368,9 +368,32 @@ private struct PreviewConfiguration: StoreConfigurationProtocol {
368368
}
369369

370370
#Preview("DeveloperSupportStoreView") {
371+
let config = PreviewConfiguration()
372+
let previewService = StoreServicePreview.withDefaultMockData(
373+
subscriptionIds: config.subscriptionIds,
374+
inAppPurchaseIds: config.inAppPurchaseIds
375+
)
376+
377+
DeveloperSupportStoreView(
378+
configuration: config,
379+
storeService: previewService,
380+
onPurchaseSuccess: { _ in },
381+
onDismiss: {}
382+
)
383+
.frame(width: 400, height: 500)
384+
}
385+
386+
#Preview("DeveloperSupportStoreView - With Subscription") {
387+
let config = PreviewConfiguration()
388+
let previewService = StoreServicePreview.withDefaultMockData(
389+
subscriptionIds: config.subscriptionIds,
390+
inAppPurchaseIds: config.inAppPurchaseIds,
391+
hasActiveSubscription: true
392+
)
393+
371394
DeveloperSupportStoreView(
372-
configuration: PreviewConfiguration(),
373-
storeService: StoreServicePreview(),
395+
configuration: config,
396+
storeService: previewService,
374397
onPurchaseSuccess: { _ in },
375398
onDismiss: {}
376399
)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//
2+
// ExampleApp.swift
3+
//
4+
// Example app demonstrating DeveloperSupportStore on iOS and macOS.
5+
// Copyright © 2025 IGR Soft. All rights reserved.
6+
//
7+
8+
import DeveloperSupportStore
9+
import SwiftUI
10+
11+
// MARK: - Example Configuration
12+
13+
/// Example store configuration with sample product IDs.
14+
struct ExampleStoreConfiguration: StoreConfigurationProtocol {
15+
var subscriptionIds: [String] {
16+
["com.example.subscription.monthly"]
17+
}
18+
19+
var inAppPurchaseIds: [String] {
20+
["com.example.tip.small", "com.example.tip.large"]
21+
}
22+
23+
var privacyPolicyURL: URL {
24+
URL(string: "https://example.com/privacy")!
25+
}
26+
27+
var termsOfUseURL: URL {
28+
URL(string: "https://example.com/terms")!
29+
}
30+
}
31+
32+
// MARK: - App Entry Point
33+
#if os(macOS)
34+
import AppKit
35+
36+
final class AppDelegate: NSObject, NSApplicationDelegate {
37+
func applicationDidFinishLaunching(_ notification: Notification) {
38+
NSApp.setActivationPolicy(.regular)
39+
NSApp.activate(ignoringOtherApps: true)
40+
}
41+
}
42+
#else
43+
import UIKit
44+
45+
final class AppDelegate: NSObject, UIApplicationDelegate {
46+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
47+
return true
48+
}
49+
}
50+
#endif
51+
52+
@main
53+
struct ExampleApp: App {
54+
#if os(macOS)
55+
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
56+
#else
57+
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
58+
#endif
59+
60+
@State private var isStorePresented = false
61+
62+
var body: some Scene {
63+
WindowGroup {
64+
ContentView(isStorePresented: $isStorePresented)
65+
}
66+
#if os(macOS)
67+
.windowStyle(.hiddenTitleBar)
68+
.windowResizability(.contentSize)
69+
#endif
70+
}
71+
}
72+
73+
// MARK: - Content View
74+
75+
struct ContentView: View {
76+
@Binding var isStorePresented: Bool
77+
78+
private let configuration = ExampleStoreConfiguration()
79+
private var previewService: StoreServicePreview {
80+
.withDefaultMockData(
81+
subscriptionIds: configuration.subscriptionIds,
82+
inAppPurchaseIds: configuration.inAppPurchaseIds
83+
)
84+
}
85+
86+
var body: some View {
87+
launcherView
88+
.sheet(isPresented: $isStorePresented) {
89+
DeveloperSupportStoreView(
90+
configuration: configuration,
91+
storeService: previewService,
92+
onPurchaseSuccess: { productId in
93+
print("✅ Purchase successful: \(productId)")
94+
},
95+
onDismiss: {
96+
isStorePresented = false
97+
}
98+
)
99+
}
100+
#if os(iOS)
101+
.frame(maxWidth: .infinity, maxHeight: .infinity)
102+
#else
103+
.frame(width: 420, height: 580)
104+
#endif
105+
}
106+
107+
@ViewBuilder
108+
private var storeView: some View {
109+
DeveloperSupportStoreView(
110+
configuration: configuration,
111+
storeService: previewService,
112+
onPurchaseSuccess: { productId in
113+
print("✅ Purchase successful: \(productId)")
114+
},
115+
onDismiss: {
116+
isStorePresented = false
117+
}
118+
)
119+
}
120+
121+
@ViewBuilder
122+
private var launcherView: some View {
123+
VStack(spacing: 24) {
124+
Image(systemName: "heart.fill")
125+
.font(.system(size: 64))
126+
.foregroundStyle(.pink)
127+
128+
Text("DeveloperSupportStore")
129+
.font(.title)
130+
.fontWeight(.bold)
131+
132+
Text("Example App")
133+
.font(.headline)
134+
.foregroundStyle(.secondary)
135+
136+
Button {
137+
isStorePresented = true
138+
} label: {
139+
Label("Open Store", systemImage: "storefront")
140+
.font(.headline)
141+
.padding(.horizontal, 24)
142+
.padding(.vertical, 12)
143+
}
144+
.buttonStyle(.borderedProminent)
145+
.tint(.pink)
146+
}
147+
.padding(40)
148+
}
149+
}
150+
151+
// MARK: - Previews
152+
153+
#Preview("Example App - Launcher") {
154+
ContentView(isStorePresented: .constant(false))
155+
.frame(width: 420, height: 580)
156+
}
157+
158+
#Preview("Example App - Store") {
159+
ContentView(isStorePresented: .constant(true))
160+
.frame(width: 420, height: 580)
161+
}

0 commit comments

Comments
 (0)