@@ -7,17 +7,22 @@ struct AvailablePurchasesScreen: View {
77 @State private var showError = false
88 @State private var errorMessage = " "
99 @State private var selectedPurchase : OpenIapPurchase ?
10+ @State private var isInitialLoading = true
1011
1112 var body : some View {
1213 ScrollView {
1314 VStack ( spacing: 24 ) {
14- availablePurchasesSection
15- purchaseHistorySection
16-
17- // Debug section for Sandbox testing
18- #if DEBUG
19- sandboxToolsSection
20- #endif
15+ if isInitialLoading {
16+ LoadingCard ( text: " Loading purchases... " )
17+ } else {
18+ availablePurchasesSection
19+ purchaseHistorySection
20+
21+ // Debug section for Sandbox testing
22+ #if DEBUG
23+ sandboxToolsSection
24+ #endif
25+ }
2126 }
2227 . padding ( . vertical)
2328 }
@@ -26,9 +31,9 @@ struct AvailablePurchasesScreen: View {
2631 . navigationBarTitleDisplayMode ( . large)
2732 . toolbar {
2833 ToolbarItem ( placement: . navigationBarTrailing) {
29- Button ( action : {
30- loadPurchases ( )
31- } ) {
34+ Button {
35+ Task { await loadPurchases ( ) }
36+ } label : {
3237 Image ( systemName: " arrow.clockwise " )
3338 }
3439 . disabled ( iapStore. status. loadings. restorePurchases)
@@ -48,11 +53,17 @@ struct AvailablePurchasesScreen: View {
4853 Text ( errorMessage)
4954 }
5055 . onAppear {
56+ isInitialLoading = true
5157 setupIapProvider ( )
5258 }
5359 . onDisappear {
5460 teardownConnection ( )
5561 }
62+ . onChange ( of: iapStore. iosAvailablePurchases. count) { _ in
63+ if isInitialLoading {
64+ isInitialLoading = false
65+ }
66+ }
5667 }
5768
5869 // MARK: - Available Purchases Section (Currently Owned/Active)
@@ -76,20 +87,23 @@ struct AvailablePurchasesScreen: View {
7687 if purchase. isSubscription {
7788 // Active subscriptions: check auto-renewing or expiry time
7889 if purchase. isAutoRenewing {
90+ OpenIapLog . debug ( " 📦 Active subscription auto-renewing product= \( purchase. productId) " )
7991 return true // Always show auto-renewing subscriptions
8092 }
8193 // For non-auto-renewing, check expiry time
8294 if let expiryTime = purchase. expirationDateIOS {
8395 let expiryDate = Date ( timeIntervalSince1970: expiryTime / 1000 )
96+ OpenIapLog . debug ( " 📦 Subscription product= \( purchase. productId) expiry= \( expiryDate) isActive= \( expiryDate > Date ( ) ) " )
8497 return expiryDate > Date ( ) // Only show if not expired
8598 }
8699 return true // Show if no expiry info
87100 } else {
88101 // Consumables: show if not acknowledged
102+ OpenIapLog . debug ( " 📦 Consumable product= \( purchase. productId) state= \( purchase. purchaseState. rawValue) isAcknowledged= \( purchase. purchaseState. isAcknowledged) " )
89103 return !purchase. purchaseState. isAcknowledged
90104 }
91105 }
92-
106+
93107 // Return sorted by date
94108 return allActivePurchases. sorted ( by: { $0. transactionDate > $1. transactionDate } )
95109 }
@@ -121,6 +135,26 @@ struct AvailablePurchasesScreen: View {
121135 // MARK: - Purchase History Section (All Past Purchases)
122136 private var purchaseHistorySection : some View {
123137 VStack ( alignment: . leading, spacing: 16 ) {
138+ Button {
139+ Task { await showManageSubscriptions ( ) }
140+ } label: {
141+ HStack {
142+ Image ( systemName: " gearshape.fill " )
143+ Text ( " Manage Subscriptions " )
144+ . fontWeight ( . semibold)
145+ Spacer ( )
146+ Image ( systemName: " arrow.up.forward.app " )
147+ . font ( . caption)
148+ }
149+ . padding ( )
150+ . frame ( maxWidth: . infinity)
151+ . background ( AppColors . secondary)
152+ . foregroundColor ( . white)
153+ . cornerRadius ( 10 )
154+ }
155+ . buttonStyle ( . plain)
156+ . padding ( . horizontal)
157+
124158 SectionHeaderView (
125159 title: " Purchase History " ,
126160 subtitle: " All your past purchases " ,
@@ -287,30 +321,30 @@ struct AvailablePurchasesScreen: View {
287321
288322 iapStore. onPurchaseSuccess = { purchase in
289323 if purchase. asIOS ( ) != nil {
290- Task { @MainActor in
291- loadPurchases ( )
292- }
324+ Task { await loadPurchases ( ) }
293325 }
294326 }
295-
327+
296328 iapStore. onPurchaseError = { error in
297329 Task { @MainActor in
298330 errorMessage = error. message
299331 showError = true
300332 }
301333 }
302-
334+
303335 Task {
304336 do {
305337 try await iapStore. initConnection ( )
306338 print ( " ✅ [AvailablePurchases] Connection initialized " )
307- loadPurchases ( )
339+ await loadPurchases ( )
308340 } catch {
309341 await MainActor . run {
310342 errorMessage = " Failed to initialize connection: \( error. localizedDescription) "
311343 showError = true
344+ isInitialLoading = false
312345 }
313346 }
347+ await MainActor . run { isInitialLoading = false }
314348 }
315349 }
316350
@@ -324,16 +358,32 @@ struct AvailablePurchasesScreen: View {
324358
325359 // MARK: - Purchase Loading
326360
327- private func loadPurchases( ) {
328- Task {
329- do {
330- try await iapStore. getAvailablePurchases ( )
331- print ( " ✅ [AvailablePurchases] Loaded \( iapStore. iosAvailablePurchases. count) purchases " )
332- } catch {
333- await MainActor . run {
334- errorMessage = " Failed to load purchases: \( error. localizedDescription) "
335- showError = true
336- }
361+ @MainActor
362+ private func loadPurchases( retry: Int = 0 ) async {
363+ isInitialLoading = false
364+ do {
365+ try await iapStore. getAvailablePurchases ( )
366+ print ( " ✅ [AvailablePurchases] Loaded \( iapStore. iosAvailablePurchases. count) purchases " )
367+ } catch {
368+ errorMessage = " Failed to load purchases: \( error. localizedDescription) "
369+ showError = true
370+ }
371+
372+ if iapStore. iosAvailablePurchases. isEmpty && retry < 3 {
373+ let delay = UInt64 ( ( retry + 1 ) * 2 ) * 1_000_000_000
374+ OpenIapLog . debug ( " 🕑 No purchases yet, retrying loadPurchases attempt= \( retry + 1 ) " )
375+ try ? await Task . sleep ( nanoseconds: delay)
376+ await loadPurchases ( retry: retry + 1 )
377+ }
378+ }
379+
380+ private func showManageSubscriptions( ) async {
381+ do {
382+ _ = try await iapStore. showManageSubscriptionsIOS ( )
383+ } catch {
384+ await MainActor . run {
385+ errorMessage = " Failed to open subscription management: \( error. localizedDescription) "
386+ showError = true
337387 }
338388 }
339389 }
@@ -344,8 +394,7 @@ struct AvailablePurchasesScreen: View {
344394 do {
345395 try await iapStore. finishTransaction ( purchase: purchase)
346396 print ( " ✅ [AvailablePurchases] Transaction finished: \( purchase. id) " )
347- // Reload purchases to update UI
348- loadPurchases ( )
397+ await loadPurchases ( )
349398 } catch {
350399 await MainActor . run {
351400 errorMessage = " Failed to finish transaction: \( error. localizedDescription) "
@@ -359,13 +408,13 @@ struct AvailablePurchasesScreen: View {
359408 private func clearAllTransactions( ) async {
360409 // Note: This would require additional API in OpenIapStore
361410 // For now, just reload purchases
362- loadPurchases ( )
411+ await loadPurchases ( )
363412 print ( " 🧪 [AvailablePurchases] Clear transactions requested (reloaded purchases) " )
364413 }
365-
414+
366415 private func syncSubscriptions( ) async {
367416 // Reload purchases to sync subscription status
368- loadPurchases ( )
417+ await loadPurchases ( )
369418 print ( " 🧪 [AvailablePurchases] Subscription sync requested (reloaded purchases) " )
370419 }
371420
@@ -382,7 +431,7 @@ struct AvailablePurchasesScreen: View {
382431 }
383432
384433 // Reload after finishing transactions
385- loadPurchases ( )
434+ await loadPurchases ( )
386435 }
387436}
388437
0 commit comments