@@ -13,9 +13,10 @@ struct SubscriptionFlowScreen: View {
1313 @State private var isInitialLoading = true
1414
1515 // Product IDs for subscription testing
16+ // Ordered from lowest to highest tier for upgrade scenarios
1617 private let subscriptionIds : [ String ] = [
17- " dev.hyo.martie.premium " ,
18- " dev.hyo.martie.premium_year "
18+ " dev.hyo.martie.premium " , // Monthly subscription (lower tier)
19+ " dev.hyo.martie.premium_year " // Yearly subscription (higher tier)
1920 ]
2021
2122 var body : some View {
@@ -70,20 +71,31 @@ struct SubscriptionFlowScreen: View {
7071 } else {
7172 ForEach ( productIds, id: \. self) { productId in
7273 let product = product ( for: productId)
74+ let currentSubscription = getCurrentSubscription ( )
75+ let upgradeInfo = getUpgradeInfo ( from: currentSubscription, to: productId)
76+
7377 SubscriptionCard (
7478 productId: productId,
7579 product: product,
7680 purchase: purchase ( for: productId) ,
7781 isSubscribed: isSubscribed ( productId: productId) ,
7882 isCancelled: isCancelled ( productId: productId) ,
7983 isLoading: iapStore. status. isPurchasing ( productId) ,
84+ upgradeInfo: upgradeInfo,
8085 onSubscribe: {
8186 let subscribed = isSubscribed ( productId: productId)
8287
8388 if subscribed {
8489 Task {
8590 await manageSubscriptions ( )
8691 }
92+ } else if upgradeInfo. canUpgrade {
93+ // Handle upgrade scenario
94+ if let product = product {
95+ Task {
96+ await upgradeSubscription ( from: currentSubscription, to: product)
97+ }
98+ }
8799 } else {
88100 if let product = product {
89101 purchaseProduct ( product)
@@ -256,7 +268,7 @@ struct SubscriptionFlowScreen: View {
256268 }
257269
258270 // MARK: - Purchase Flow
259-
271+
260272 private func purchaseProduct( _ product: OpenIapProduct ) {
261273 print ( " 🔄 [SubscriptionFlow] Starting subscription purchase for: \( product. id) " )
262274 Task {
@@ -268,6 +280,92 @@ struct SubscriptionFlowScreen: View {
268280 }
269281 }
270282 }
283+
284+ // MARK: - Subscription Upgrade Flow
285+
286+ private func upgradeSubscription( from currentSubscription: OpenIapPurchase ? , to product: OpenIapProduct ) async {
287+ print ( " ⬆️ [SubscriptionFlow] Starting subscription upgrade " )
288+ print ( " From: \( currentSubscription? . productId ?? " none " ) " )
289+ print ( " To: \( product. id) " )
290+ print ( " iOS will automatically prorate the subscription " )
291+
292+ do {
293+ // Request the upgrade purchase
294+ // iOS handles proration automatically when upgrading within the same subscription group
295+ _ = try await iapStore. requestPurchase (
296+ sku: product. id,
297+ type: . subs,
298+ autoFinish: true
299+ )
300+
301+ print ( " ✅ [SubscriptionFlow] Upgrade successful to: \( product. id) " )
302+
303+ // Reload purchases to update UI
304+ await loadPurchases ( )
305+
306+ } catch {
307+ print ( " ❌ [SubscriptionFlow] Upgrade failed: \( error. localizedDescription) " )
308+ await MainActor . run {
309+ errorMessage = " Failed to upgrade subscription: \( error. localizedDescription) "
310+ showError = true
311+ }
312+ }
313+ }
314+
315+ // Get current active subscription
316+ private func getCurrentSubscription( ) -> OpenIapPurchase ? {
317+ // Find the currently active subscription with highest priority
318+ let activeSubscriptions = iapStore. iosAvailablePurchases. filter { purchase in
319+ if !purchase. isSubscription { return false }
320+
321+ // Check if subscription is active
322+ if let expirationTime = purchase. expirationDateIOS {
323+ let expirationDate = Date ( timeIntervalSince1970: expirationTime / 1000 )
324+ return expirationDate > Date ( ) && purchase. isAutoRenewing
325+ }
326+
327+ return purchase. purchaseState == . purchased && purchase. isAutoRenewing
328+ }
329+
330+ // Return the subscription with the highest tier (yearly over monthly)
331+ return activeSubscriptions. first { $0. productId. contains ( " year " ) } ?? activeSubscriptions. first
332+ }
333+
334+ // Determine upgrade possibilities
335+ private func getUpgradeInfo( from currentSubscription: OpenIapPurchase ? , to targetProductId: String ) -> UpgradeInfo {
336+ guard let current = currentSubscription else {
337+ return UpgradeInfo ( canUpgrade: false , isDowngrade: false , currentTier: nil )
338+ }
339+
340+ // Don't show upgrade for the same product
341+ if current. productId == targetProductId {
342+ return UpgradeInfo ( canUpgrade: false , isDowngrade: false , currentTier: current. productId)
343+ }
344+
345+ // Determine tier based on product ID
346+ let currentTier = getSubscriptionTier ( current. productId)
347+ let targetTier = getSubscriptionTier ( targetProductId)
348+
349+ let canUpgrade = targetTier > currentTier
350+ let isDowngrade = targetTier < currentTier
351+
352+ return UpgradeInfo (
353+ canUpgrade: canUpgrade,
354+ isDowngrade: isDowngrade,
355+ currentTier: current. productId,
356+ message: canUpgrade ? " Upgrade available " : ( isDowngrade ? " Downgrade option " : nil )
357+ )
358+ }
359+
360+ // Get subscription tier level (higher number = higher tier)
361+ private func getSubscriptionTier( _ productId: String ) -> Int {
362+ if productId. contains ( " year " ) || productId. contains ( " annual " ) {
363+ return 2 // Yearly is higher tier
364+ } else if productId. contains ( " month " ) || productId. contains ( " premium " ) {
365+ return 1 // Monthly is lower tier
366+ }
367+ return 0 // Unknown tier
368+ }
271369
272370 private func restorePurchases( ) async {
273371 do {
@@ -354,6 +452,21 @@ private extension SubscriptionFlowScreen {
354452 }
355453}
356454
455+ // MARK: - Upgrade Info Model
456+ struct UpgradeInfo {
457+ let canUpgrade : Bool
458+ let isDowngrade : Bool
459+ let currentTier : String ?
460+ let message : String ?
461+
462+ init ( canUpgrade: Bool = false , isDowngrade: Bool = false , currentTier: String ? = nil , message: String ? = nil ) {
463+ self . canUpgrade = canUpgrade
464+ self . isDowngrade = isDowngrade
465+ self . currentTier = currentTier
466+ self . message = message
467+ }
468+ }
469+
357470@available ( iOS 15 . 0 , * )
358471@MainActor
359472private extension SubscriptionFlowScreen {
0 commit comments