소스 검색

Align paywall subscriptions with App Store Connect product IDs.

Switch StoreKit and paywall logic from legacy weekly/lifetime plans to monthly, bi-yearly, and yearly offerings, and surface the yearly 3-day free trial messaging directly from StoreKit metadata so sandbox and production use consistent product mapping.

Co-authored-by: Cursor <cursoragent@cursor.com>
Uzair Tahir 1 개월 전
부모
커밋
bedb1a8283
4개의 변경된 파일195개의 추가작업 그리고 100개의 파일을 삭제
  1. 140 0
      products.storekit
  2. 9 2
      zoom_app.xcodeproj/project.pbxproj
  3. 1 1
      zoom_app.xcodeproj/xcshareddata/xcschemes/zoom_app.xcscheme
  4. 45 97
      zoom_app/ViewController.swift

+ 140 - 0
products.storekit

@@ -0,0 +1,140 @@
+{
+  "appPolicies" : {
+    "eula" : "",
+    "policies" : [
+      {
+        "locale" : "en_US",
+        "policyText" : "",
+        "policyURL" : ""
+      }
+    ]
+  },
+  "identifier" : "F0BE382F",
+  "nonRenewingSubscriptions" : [
+
+  ],
+  "products" : [
+
+  ],
+  "settings" : {
+    "_applicationInternalID" : "6770505917",
+    "_askToBuyEnabled" : false,
+    "_billingGracePeriodEnabled" : false,
+    "_billingIssuesEnabled" : false,
+    "_developerTeamID" : "NNC7V99779",
+    "_disableDialogs" : false,
+    "_failTransactionsEnabled" : false,
+    "_lastSynchronizedDate" : 802086215.08905303,
+    "_locale" : "en_US",
+    "_renewalBillingIssuesEnabled" : false,
+    "_storefront" : "USA",
+    "_storeKitErrors" : [
+
+    ],
+    "_timeRate" : 0
+  },
+  "subscriptionGroups" : [
+    {
+      "id" : "22128868",
+      "localizations" : [
+
+      ],
+      "name" : "Companion AI for Zoom Subscription",
+      "subscriptions" : [
+        {
+          "adHocOffers" : [
+
+          ],
+          "codeOffers" : [
+
+          ],
+          "displayPrice" : "15.99",
+          "familyShareable" : false,
+          "groupNumber" : 2,
+          "internalID" : "6775781858",
+          "introductoryOffer" : null,
+          "localizations" : [
+            {
+              "description" : "Bi Yearly for Zoom",
+              "displayName" : "AI for Zoom Bi Yearly",
+              "locale" : "en_US"
+            }
+          ],
+          "productID" : "com.account.zoom.biyearly",
+          "recurringSubscriptionPeriod" : "P6M",
+          "referenceName" : "AI for Zoom Bi Yearly",
+          "subscriptionGroupID" : "22128868",
+          "type" : "RecurringSubscription",
+          "winbackOffers" : [
+
+          ]
+        },
+        {
+          "adHocOffers" : [
+
+          ],
+          "codeOffers" : [
+
+          ],
+          "displayPrice" : "3.99",
+          "familyShareable" : false,
+          "groupNumber" : 1,
+          "internalID" : "6775780680",
+          "introductoryOffer" : null,
+          "localizations" : [
+            {
+              "description" : "Monthly Subscription",
+              "displayName" : "AI for Zoom Monthly",
+              "locale" : "en_US"
+            }
+          ],
+          "productID" : "com.account.zoom.monthly",
+          "recurringSubscriptionPeriod" : "P1M",
+          "referenceName" : "AI for Zoom Monthly",
+          "subscriptionGroupID" : "22128868",
+          "type" : "RecurringSubscription",
+          "winbackOffers" : [
+
+          ]
+        },
+        {
+          "adHocOffers" : [
+
+          ],
+          "codeOffers" : [
+
+          ],
+          "displayPrice" : "29.99",
+          "familyShareable" : false,
+          "groupNumber" : 3,
+          "internalID" : "6775782281",
+          "introductoryOffer" : {
+            "internalID" : "10302B1C",
+            "numberOfPeriods" : 1,
+            "paymentMode" : "free",
+            "subscriptionPeriod" : "P3D"
+          },
+          "localizations" : [
+            {
+              "description" : "Yearly Subscription",
+              "displayName" : "AI for Zoom Yearly",
+              "locale" : "en_US"
+            }
+          ],
+          "productID" : "com.account.zoom.yearly",
+          "recurringSubscriptionPeriod" : "P1Y",
+          "referenceName" : "AI for Zoom Yearly",
+          "subscriptionGroupID" : "22128868",
+          "type" : "RecurringSubscription",
+          "winbackOffers" : [
+
+          ]
+        }
+      ]
+    }
+  ],
+  "version" : {
+    "major" : 4,
+    "minor" : 0
+  }
+}

+ 9 - 2
zoom_app.xcodeproj/project.pbxproj

@@ -6,8 +6,13 @@
 	objectVersion = 77;
 	objects = {
 
+/* Begin PBXBuildFile section */
+		1695A1832FCEDC89003C253A /* products.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 1695A1822FCEDC89003C253A /* products.storekit */; };
+/* End PBXBuildFile section */
+
 /* Begin PBXFileReference section */
 		1391CC002F8E456500B3B198 /* Companion AI for Zoom.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Companion AI for Zoom.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		1695A1822FCEDC89003C253A /* products.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = products.storekit; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -32,6 +37,7 @@
 		1391CBF72F8E456500B3B198 = {
 			isa = PBXGroup;
 			children = (
+				1695A1822FCEDC89003C253A /* products.storekit */,
 				1391CC022F8E456500B3B198 /* zoom_app */,
 				1391CC012F8E456500B3B198 /* Products */,
 			);
@@ -109,6 +115,7 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				1695A1832FCEDC89003C253A /* products.storekit in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -251,7 +258,7 @@
 				CODE_SIGN_ENTITLEMENTS = zoom_app/zoom_app.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = 2;
 				DEVELOPMENT_TEAM = NNC7V99779;
 				ENABLE_APP_SANDBOX = YES;
 				ENABLE_USER_SELECTED_FILES = readonly;
@@ -293,7 +300,7 @@
 				CODE_SIGN_ENTITLEMENTS = zoom_app/zoom_app.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = 2;
 				DEVELOPMENT_TEAM = NNC7V99779;
 				ENABLE_APP_SANDBOX = YES;
 				ENABLE_USER_SELECTED_FILES = readonly;

+ 1 - 1
zoom_app.xcodeproj/xcshareddata/xcschemes/zoom_app.xcscheme

@@ -52,7 +52,7 @@
          </BuildableReference>
       </BuildableProductRunnable>
       <StoreKitConfigurationFileReference
-         identifier = "../../zoom_app/StoreKit.storekit">
+         identifier = "../../products.storekit">
       </StoreKitConfigurationFileReference>
    </LaunchAction>
    <ProfileAction

+ 45 - 97
zoom_app/ViewController.swift

@@ -612,17 +612,15 @@ class ViewController: NSViewController {
     }
 
     private enum PremiumPlan: String, CaseIterable {
-        case weekly = "com.mqldev.zoomapp.premium.weekly"
-        case monthly = "com.mqldev.zoomapp.premium.monthly"
-        case yearly = "com.mqldev.zoomapp.premium.yearly"
-        case lifetime = "com.mqldev.zoomapp.premium.lifetime"
+        case monthly = "com.account.zoom.monthly"
+        case biyearly = "com.account.zoom.biyearly"
+        case yearly = "com.account.zoom.yearly"
 
         var displayName: String {
             switch self {
-            case .weekly: return "Premium Weekly"
-            case .monthly: return "Premium Monthly"
-            case .yearly: return "Premium Yearly"
-            case .lifetime: return "Premium Lifetime"
+            case .monthly: return "AI for Zoom Monthly"
+            case .biyearly: return "AI for Zoom Bi Yearly"
+            case .yearly: return "AI for Zoom Yearly"
             }
         }
     }
@@ -639,8 +637,6 @@ class ViewController: NSViewController {
         private(set) var activeProductIDs = Set<String>()
 
         var hasPremiumAccess: Bool { !activeProductIDs.isEmpty }
-        var hasLifetimePremiumAccess: Bool { activeProductIDs.contains(PremiumPlan.lifetime.rawValue) }
-
         private var transactionUpdatesTask: Task<Void, Never>?
         var onEntitlementsChanged: ((Bool) -> Void)?
 
@@ -3051,7 +3047,7 @@ class ViewController: NSViewController {
     }
 
     @objc private func upgradeToProTapped() {
-        if storeKitCoordinator.hasLifetimePremiumAccess {
+        if storeKitCoordinator.hasPremiumAccess {
             openManageSubscriptions()
         } else {
             showPaywall()
@@ -5536,12 +5532,11 @@ class ViewController: NSViewController {
     @MainActor
     private func updatePremiumButtons() {
         let isPremium = storeKitCoordinator.hasPremiumAccess
-        let hasLifetimePremium = storeKitCoordinator.hasLifetimePremiumAccess
         settingsUpgradeButton?.title = ""
-        settingsUpgradeTitleLabel?.stringValue = hasLifetimePremium ? "Premium Lifetime" : "Upgrade"
-        settingsUpgradeButton?.isHidden = hasLifetimePremium
-        settingsUpgradeButton?.isEnabled = hasLifetimePremium == false
-        settingsUpgradeButton?.alphaValue = hasLifetimePremium ? 0.6 : 1.0
+        settingsUpgradeTitleLabel?.stringValue = isPremium ? "Premium Active" : "Upgrade"
+        settingsUpgradeButton?.isHidden = false
+        settingsUpgradeButton?.isEnabled = true
+        settingsUpgradeButton?.alphaValue = 1.0
         settingsNotificationsBlock?.isHidden = isPremium == false
         updatePremiumLockedSections()
         if isPremium == false, isUserLoggedIn() {
@@ -5778,15 +5773,14 @@ class ViewController: NSViewController {
     }
 
     @objc private func settingsUpgradePremiumTapped() {
-        guard storeKitCoordinator.hasLifetimePremiumAccess == false else { return }
         showPaywall()
     }
 
     // MARK: - Paywall (ported from meetings_app)
 
     private func showPaywall() {
-        if storeKitCoordinator.hasPremiumAccess && storeKitCoordinator.hasLifetimePremiumAccess == false {
-            selectedPremiumPlan = .lifetime
+        if storeKitCoordinator.hasPremiumAccess {
+            selectedPremiumPlan = .yearly
         }
         if let existing = paywallOverlayView {
             refreshPaywallStoreUI()
@@ -6040,42 +6034,33 @@ class ViewController: NSViewController {
         bodyStack.addArrangedSubview(planColumn)
         planColumn.widthAnchor.constraint(equalTo: bodyStack.widthAnchor).isActive = true
 
-        let weeklyCard = paywallPlanCard(
-            title: "Weekly",
-            price: paywallDisplayPrice(for: .weekly),
-            badge: "Basic",
-            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
-            subtitle: paywallPlanSubtitle(for: .weekly),
-            plan: .weekly,
-            strikePrice: nil
-        )
         let monthlyCard = paywallPlanCard(
             title: "Monthly",
             price: paywallDisplayPrice(for: .monthly),
-            badge: "Popular",
+            badge: "Starter",
             badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
             subtitle: paywallPlanSubtitle(for: .monthly),
             plan: .monthly,
             strikePrice: nil
         )
+        let biYearlyCard = paywallPlanCard(
+            title: "Bi Yearly",
+            price: paywallDisplayPrice(for: .biyearly),
+            badge: "Popular",
+            badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
+            subtitle: paywallPlanSubtitle(for: .biyearly),
+            plan: .biyearly,
+            strikePrice: nil
+        )
         let yearlyCard = paywallPlanCard(
             title: "Yearly",
             price: paywallDisplayPrice(for: .yearly),
-            badge: "Save 67%",
+            badge: "3 Days Free",
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
             subtitle: paywallPlanSubtitle(for: .yearly),
             plan: .yearly,
             strikePrice: nil
         )
-        let lifetimeCard = paywallPlanCard(
-            title: "Lifetime",
-            price: paywallDisplayPrice(for: .lifetime),
-            badge: "Best Value",
-            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
-            subtitle: paywallPlanSubtitle(for: .lifetime),
-            plan: .lifetime,
-            strikePrice: nil
-        )
 
         let plansRowWide = NSStackView()
         plansRowWide.translatesAutoresizingMaskIntoConstraints = false
@@ -6083,10 +6068,9 @@ class ViewController: NSViewController {
         plansRowWide.spacing = 12
         plansRowWide.alignment = .centerY
         plansRowWide.distribution = .fillEqually
-        plansRowWide.addArrangedSubview(weeklyCard)
         plansRowWide.addArrangedSubview(monthlyCard)
+        plansRowWide.addArrangedSubview(biYearlyCard)
         plansRowWide.addArrangedSubview(yearlyCard)
-        plansRowWide.addArrangedSubview(lifetimeCard)
         paywallPlansRowWide = plansRowWide
         planColumn.addArrangedSubview(plansRowWide)
         plansRowWide.widthAnchor.constraint(equalTo: planColumn.widthAnchor).isActive = true
@@ -6104,24 +6088,24 @@ class ViewController: NSViewController {
         plansRow1.spacing = 12
         plansRow1.alignment = .centerY
         plansRow1.distribution = .fillEqually
-        plansRow1.addArrangedSubview(paywallPlanCard(
-            title: "Weekly",
-            price: paywallDisplayPrice(for: .weekly),
-            badge: "Basic Deal",
-            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
-            subtitle: paywallPlanSubtitle(for: .weekly),
-            plan: .weekly,
-            strikePrice: nil
-        ))
         plansRow1.addArrangedSubview(paywallPlanCard(
             title: "Monthly",
             price: paywallDisplayPrice(for: .monthly),
-            badge: "Popular",
+            badge: "Starter",
             badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
             subtitle: paywallPlanSubtitle(for: .monthly),
             plan: .monthly,
             strikePrice: nil
         ))
+        plansRow1.addArrangedSubview(paywallPlanCard(
+            title: "Bi Yearly",
+            price: paywallDisplayPrice(for: .biyearly),
+            badge: "Popular",
+            badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
+            subtitle: paywallPlanSubtitle(for: .biyearly),
+            plan: .biyearly,
+            strikePrice: nil
+        ))
 
         let plansRow2 = NSStackView()
         plansRow2.translatesAutoresizingMaskIntoConstraints = false
@@ -6132,21 +6116,12 @@ class ViewController: NSViewController {
         plansRow2.addArrangedSubview(paywallPlanCard(
             title: "Yearly",
             price: paywallDisplayPrice(for: .yearly),
-            badge: "Best Deal",
+            badge: "3 Days Free",
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
             subtitle: paywallPlanSubtitle(for: .yearly),
             plan: .yearly,
             strikePrice: nil
         ))
-        plansRow2.addArrangedSubview(paywallPlanCard(
-            title: "Lifetime",
-            price: paywallDisplayPrice(for: .lifetime),
-            badge: "Save 50%",
-            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
-            subtitle: paywallPlanSubtitle(for: .lifetime),
-            plan: .lifetime,
-            strikePrice: nil
-        ))
 
         plansGridNarrow.addArrangedSubview(plansRow1)
         plansGridNarrow.addArrangedSubview(plansRow2)
@@ -6604,16 +6579,12 @@ class ViewController: NSViewController {
     }
 
     private func paywallOfferText(for plan: PremiumPlan) -> String {
-        if storeKitCoordinator.hasLifetimePremiumAccess {
-            return "Lifetime premium is active on this Apple ID."
-        }
         guard let product = storeKitCoordinator.productsByID[plan.rawValue] else {
             let price = paywallDisplayPrice(for: plan)
             switch plan {
-            case .weekly: return "\(price)/week"
             case .monthly: return "\(price)/month"
+            case .biyearly: return "\(price)/6 months"
             case .yearly: return "\(price)/year"
-            case .lifetime: return "\(price) one-time purchase"
             }
         }
 
@@ -6627,20 +6598,15 @@ class ViewController: NSViewController {
             return "Free for \(trial), then \(product.displayPrice)"
         }
 
-        if plan == .lifetime {
-            return "\(product.displayPrice) one-time purchase"
-        }
-
         if let renewal = paywallSubscriptionPeriodDescription(product.subscription?.subscriptionPeriod) {
             return "\(product.displayPrice)/\(renewal)"
         }
 
         let fallbackPrice = paywallDisplayPrice(for: plan)
         switch plan {
-        case .weekly: return "\(fallbackPrice)/week"
         case .monthly: return "\(fallbackPrice)/month"
+        case .biyearly: return "\(fallbackPrice)/6 months"
         case .yearly: return "\(fallbackPrice)/year"
-        case .lifetime: return "\(fallbackPrice) one-time purchase"
         }
     }
 
@@ -6655,14 +6621,11 @@ class ViewController: NSViewController {
     }
 
     private func paywallPlanSubtitle(for plan: PremiumPlan) -> String {
-        if plan == .lifetime { return "One-time purchase" }
-
         guard let product = storeKitCoordinator.productsByID[plan.rawValue] else {
             switch plan {
-            case .weekly: return "Billed weekly"
             case .monthly: return "Billed monthly"
+            case .biyearly: return "Billed every 6 months"
             case .yearly: return "Billed yearly"
-            case .lifetime: return "One-time purchase"
             }
         }
 
@@ -6678,10 +6641,9 @@ class ViewController: NSViewController {
         }
 
         switch plan {
-        case .weekly: return "Billed weekly"
         case .monthly: return "Billed monthly"
+        case .biyearly: return "Billed every 6 months"
         case .yearly: return "Billed yearly"
-        case .lifetime: return "One-time purchase"
         }
     }
 
@@ -6722,10 +6684,9 @@ class ViewController: NSViewController {
             return product.displayPrice
         }
         switch plan {
-        case .weekly: return "3.99"
-        case .monthly: return "7.99"
-        case .yearly: return "24.99"
-        case .lifetime: return "44.99"
+        case .monthly: return "3.99"
+        case .biyearly: return "15.99"
+        case .yearly: return "29.99"
         }
     }
 
@@ -6778,8 +6739,6 @@ class ViewController: NSViewController {
         paywallPurchaseTask?.cancel()
         updatePaywallContinueState(isLoading: true)
         let selectedPlan = selectedPremiumPlan
-        let hadPremiumBeforePurchase = storeKitCoordinator.hasPremiumAccess
-        let hadLifetimeBeforePurchase = storeKitCoordinator.hasLifetimePremiumAccess
         paywallPurchaseTask = Task { [weak self] in
             guard let self else { return }
             if self.storeKitCoordinator.productsByID[selectedPlan.rawValue] == nil {
@@ -6798,14 +6757,7 @@ class ViewController: NSViewController {
                         UserDefaults.standard.set(false, forKey: self.ratingPromptShownKey)
                         self.schedulePremiumRatingPromptIfNeeded()
                     }
-                    if selectedPlan == .lifetime, hadLifetimeBeforePurchase == false, hadPremiumBeforePurchase {
-                        self.showSimpleAlert(
-                            title: "Lifetime Activated",
-                            message: "Lifetime premium is now active. If you previously subscribed (weekly/monthly/yearly), please cancel that subscription in your Apple ID subscriptions to avoid future renewals."
-                        )
-                    } else {
-                        self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
-                    }
+                    self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
                     self.hidePaywall()
                     if self.storeKitCoordinator.hasPremiumAccess {
                         DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
@@ -6835,11 +6787,7 @@ class ViewController: NSViewController {
             paywallContinueButton?.alphaValue = 0.75
             return
         }
-        if storeKitCoordinator.hasLifetimePremiumAccess {
-            paywallContinueEnabled = false
-            paywallContinueLabel?.stringValue = "Lifetime Premium Active"
-            paywallContinueButton?.alphaValue = 0.75
-        } else if storeKitCoordinator.productsByID[selectedPremiumPlan.rawValue] == nil {
+        if storeKitCoordinator.productsByID[selectedPremiumPlan.rawValue] == nil {
             paywallContinueEnabled = false
             paywallContinueLabel?.stringValue = "Store Unavailable"
             paywallContinueButton?.alphaValue = 0.75