1+ import SwiftUI
2+ import OpenIAP
3+
4+ @available ( iOS 15 . 0 , * )
5+ struct AllProductsView : View {
6+ @StateObject private var store = OpenIapStore ( )
7+ @State private var isLoading = false
8+ @State private var errorMessage : String ?
9+ @Environment ( \. dismiss) private var dismiss
10+
11+ // Product IDs from other screens
12+ private let allProductIds : [ String ] = [
13+ " dev.hyo.martie.10bulbs " ,
14+ " dev.hyo.martie.30bulbs " ,
15+ " dev.hyo.martie.premium " ,
16+ " dev.hyo.martie.premium_year "
17+ ]
18+
19+
20+ var body : some View {
21+ NavigationView {
22+ ZStack {
23+ Color ( UIColor . systemGroupedBackground)
24+ . ignoresSafeArea ( )
25+
26+ ScrollView {
27+ VStack ( spacing: 16 ) {
28+ if !store. isConnected {
29+ connectionWarningCard
30+ }
31+
32+ if isLoading {
33+ loadingCard
34+ }
35+
36+ if !isLoading && store. iosProducts. isEmpty && store. iosSubscriptionProducts. isEmpty && store. isConnected {
37+ emptyStateCard
38+ }
39+
40+ // Display regular products
41+ ForEach ( store. iosProducts, id: \. id) { product in
42+ productCard ( for: product)
43+ }
44+
45+ // Display subscription products
46+ ForEach ( store. iosSubscriptionProducts, id: \. id) { subscription in
47+ subscriptionCard ( for: subscription)
48+ }
49+
50+ if let error = errorMessage {
51+ errorCard ( message: error)
52+ }
53+ }
54+ . padding ( )
55+ }
56+ }
57+ . navigationTitle ( " All Products " )
58+ . navigationBarTitleDisplayMode ( . large)
59+ . toolbar {
60+ ToolbarItem ( placement: . navigationBarLeading) {
61+ Button ( action: {
62+ Task {
63+ try ? await store. endConnection ( )
64+ }
65+ dismiss ( )
66+ } ) {
67+ Image ( systemName: " arrow.left " )
68+ . foregroundColor ( . primary)
69+ }
70+ }
71+ }
72+ }
73+ . navigationViewStyle ( StackNavigationViewStyle ( ) )
74+ . onAppear {
75+ Task {
76+ await initializeStore ( )
77+ }
78+ }
79+ }
80+
81+ private var connectionWarningCard : some View {
82+ HStack {
83+ Image ( systemName: " exclamationmark.triangle.fill " )
84+ . foregroundColor ( . orange)
85+ . font ( . title3)
86+
87+ VStack ( alignment: . leading, spacing: 4 ) {
88+ Text ( " Not Connected " )
89+ . font ( . headline)
90+ Text ( " Billing service is not connected " )
91+ . font ( . caption)
92+ . foregroundColor ( . secondary)
93+ }
94+
95+ Spacer ( )
96+
97+ Button ( " Retry " ) {
98+ Task {
99+ await initializeStore ( )
100+ }
101+ }
102+ . buttonStyle ( . bordered)
103+ . controlSize ( . small)
104+ }
105+ . padding ( )
106+ . background ( Color . orange. opacity ( 0.1 ) )
107+ . cornerRadius ( 12 )
108+ }
109+
110+ private var loadingCard : some View {
111+ HStack {
112+ ProgressView ( )
113+ . progressViewStyle ( CircularProgressViewStyle ( ) )
114+
115+ Text ( " Loading products... " )
116+ . font ( . subheadline)
117+ . foregroundColor ( . secondary)
118+ . padding ( . leading, 8 )
119+ }
120+ . frame ( maxWidth: . infinity)
121+ . padding ( )
122+ . background ( Color ( UIColor . secondarySystemGroupedBackground) )
123+ . cornerRadius ( 12 )
124+ }
125+
126+ private var emptyStateCard : some View {
127+ VStack ( spacing: 12 ) {
128+ Image ( systemName: " bag " )
129+ . font ( . largeTitle)
130+ . foregroundColor ( . secondary)
131+
132+ Text ( " No products available " )
133+ . font ( . headline)
134+ . foregroundColor ( . secondary)
135+ }
136+ . frame ( maxWidth: . infinity)
137+ . padding ( . vertical, 32 )
138+ . background ( Color ( UIColor . secondarySystemGroupedBackground) )
139+ . cornerRadius ( 12 )
140+ }
141+
142+ private func productCard( for product: ProductIOS ) -> some View {
143+ VStack ( alignment: . leading, spacing: 12 ) {
144+ HStack ( alignment: . top) {
145+ VStack ( alignment: . leading, spacing: 4 ) {
146+ Text ( product. displayName ?? product. displayNameIOS)
147+ . font ( . headline)
148+
149+ if !product. description. isEmpty {
150+ Text ( product. description)
151+ . font ( . caption)
152+ . foregroundColor ( . secondary)
153+ . lineLimit ( 2 )
154+ }
155+ }
156+
157+ Spacer ( )
158+
159+ // Product type badge
160+ Text ( product. type == . subs ? " subs " : " in-app " )
161+ . font ( . caption)
162+ . fontWeight ( . medium)
163+ . padding ( . horizontal, 8 )
164+ . padding ( . vertical, 4 )
165+ . background ( product. type == . subs ? Color . blue. opacity ( 0.1 ) : Color . green. opacity ( 0.1 ) )
166+ . foregroundColor ( product. type == . subs ? . blue : . green)
167+ . cornerRadius ( 6 )
168+ }
169+
170+ HStack {
171+ Text ( product. displayPrice ?? " -- " )
172+ . font ( . title2)
173+ . fontWeight ( . bold)
174+ . foregroundColor ( . blue)
175+
176+ Spacer ( )
177+
178+ Text ( " SKU: \( product. id) " )
179+ . font ( . caption2)
180+ . foregroundColor ( . secondary)
181+ }
182+ }
183+ . padding ( )
184+ . background ( Color ( UIColor . secondarySystemGroupedBackground) )
185+ . cornerRadius ( 12 )
186+ . shadow ( color: . black. opacity ( 0.05 ) , radius: 2 , x: 0 , y: 1 )
187+ }
188+
189+ private func errorCard( message: String ) -> some View {
190+ HStack {
191+ Image ( systemName: " exclamationmark.circle.fill " )
192+ . foregroundColor ( . red)
193+
194+ Text ( message)
195+ . font ( . subheadline)
196+ . foregroundColor ( . red)
197+
198+ Spacer ( )
199+ }
200+ . padding ( )
201+ . background ( Color . red. opacity ( 0.1 ) )
202+ . cornerRadius ( 12 )
203+ }
204+
205+ private func initializeStore( ) async {
206+ isLoading = true
207+ errorMessage = nil
208+
209+ do {
210+ try await store. initConnection ( )
211+
212+ if store. isConnected {
213+ // Fetch all products using "all" type
214+ try await store. fetchProducts (
215+ skus: allProductIds,
216+ type: . all
217+ )
218+ } else {
219+ errorMessage = " Failed to connect to App Store "
220+ }
221+ } catch {
222+ errorMessage = error. localizedDescription
223+ }
224+
225+ isLoading = false
226+ }
227+
228+ private func subscriptionCard( for subscription: ProductSubscriptionIOS ) -> some View {
229+ VStack ( alignment: . leading, spacing: 12 ) {
230+ HStack ( alignment: . top) {
231+ VStack ( alignment: . leading, spacing: 4 ) {
232+ Text ( subscription. displayName ?? subscription. displayNameIOS)
233+ . font ( . headline)
234+
235+ if !subscription. description. isEmpty {
236+ Text ( subscription. description)
237+ . font ( . caption)
238+ . foregroundColor ( . secondary)
239+ . lineLimit ( 2 )
240+ }
241+ }
242+
243+ Spacer ( )
244+
245+ // Subscription badge
246+ Text ( " subs " )
247+ . font ( . caption)
248+ . fontWeight ( . medium)
249+ . padding ( . horizontal, 8 )
250+ . padding ( . vertical, 4 )
251+ . background ( Color . blue. opacity ( 0.1 ) )
252+ . foregroundColor ( . blue)
253+ . cornerRadius ( 6 )
254+ }
255+
256+ HStack {
257+ Text ( subscription. displayPrice)
258+ . font ( . title2)
259+ . fontWeight ( . bold)
260+ . foregroundColor ( . blue)
261+
262+ Spacer ( )
263+
264+ Text ( " SKU: \( subscription. id) " )
265+ . font ( . caption2)
266+ . foregroundColor ( . secondary)
267+ }
268+ }
269+ . padding ( )
270+ . background ( Color ( UIColor . secondarySystemGroupedBackground) )
271+ . cornerRadius ( 12 )
272+ . shadow ( color: . black. opacity ( 0.05 ) , radius: 2 , x: 0 , y: 1 )
273+ }
274+ }
275+
276+ #Preview {
277+ AllProductsView ( )
278+ }
0 commit comments