Sfoglia il codice sorgente

Integrate StoreKit 2 premium purchases and local testing.

Replace the custom checkout flow with StoreKit product loading/purchase/restore, wire premium state through the launcher UI, and add a shared scheme StoreKit configuration for local testing.

Made-with: Cursor
huzaifahayat12 3 mesi fa
parent
commit
7ab0e8838f

+ 80 - 0
google_apps.xcodeproj/xcshareddata/xcschemes/google_apps.xcscheme

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1640"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "13C4FCEB2F764FD400860356"
+               BuildableName = "google_apps.app"
+               BlueprintName = "google_apps"
+               ReferencedContainer = "container:google_apps.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "13C4FCEB2F764FD400860356"
+            BuildableName = "google_apps.app"
+            BlueprintName = "google_apps"
+            ReferencedContainer = "container:google_apps.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <StoreKitConfigurationFileReference
+         identifier = "../../google_apps/Products.storekit">
+      </StoreKitConfigurationFileReference>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "13C4FCEB2F764FD400860356"
+            BuildableName = "google_apps.app"
+            BlueprintName = "google_apps"
+            ReferencedContainer = "container:google_apps.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 11 - 6
google_apps/LauncherRootView.swift

@@ -8,6 +8,7 @@ struct LauncherRootView: View {
         case panel
     }
 
+    @EnvironmentObject private var premiumStore: PremiumStore
     @State private var query = ""
     @State private var selectedApp: LauncherApp?
     @State private var showingPremiumScreen = false
@@ -114,12 +115,14 @@ struct LauncherRootView: View {
                 )
                 .padding(.top, 12)
 
-                PromoBanner(
-                    onCloseTap: { showingPremiumScreen = true },
-                    onUpgradeTap: { showingPremiumScreen = true }
-                )
-                .padding(.bottom, 8)
-                .zIndex(1)
+                if !premiumStore.isPremiumUnlocked {
+                    PromoBanner(
+                        onCloseTap: { showingPremiumScreen = true },
+                        onUpgradeTap: { showingPremiumScreen = true }
+                    )
+                    .padding(.bottom, 8)
+                    .zIndex(1)
+                }
 
                 appsPage
             }
@@ -132,6 +135,7 @@ struct LauncherRootView: View {
         }
         .sheet(isPresented: $showingPremiumScreen) {
             PremiumFeaturesView()
+                .environmentObject(premiumStore)
                 .frame(minWidth: 560, minHeight: 690)
         }
         .sheet(isPresented: $showingCreateAppSheet) {
@@ -834,4 +838,5 @@ private struct HideMenuIndicatorIfAvailable: ViewModifier {
 
 #Preview {
     LauncherRootView()
+        .environmentObject(PremiumStore.shared)
 }

+ 135 - 154
google_apps/PremiumFeaturesView.swift

@@ -1,14 +1,22 @@
+import StoreKit
 import SwiftUI
 
 struct PremiumFeaturesView: View {
     enum PlanOption: String {
         case perpetual
         case yearly
+
+        fileprivate var productID: PremiumProductID {
+            switch self {
+            case .perpetual: return .perpetual
+            case .yearly: return .yearly
+            }
+        }
     }
 
+    @EnvironmentObject private var premiumStore: PremiumStore
     @Environment(\.dismiss) private var dismiss
     @State private var selectedPlan: PlanOption = .perpetual
-    @State private var showingCheckout = false
     @State private var showingSuccessAlert = false
 
     private let featureRows: [(String, String)] = [
@@ -29,6 +37,20 @@ struct PremiumFeaturesView: View {
         .indigo, .pink, .cyan, .gray, .mint,
     ]
 
+    private var selectedProduct: Product? {
+        premiumStore.product(for: selectedPlan.productID)
+    }
+
+    private func priceLine(for plan: PlanOption) -> String {
+        if let p = premiumStore.displayPrice(for: plan.productID) {
+            return p
+        }
+        if premiumStore.isLoadingProducts {
+            return "Loading…"
+        }
+        return "—"
+    }
+
     var body: some View {
         ZStack {
             LinearGradient(
@@ -61,15 +83,21 @@ struct PremiumFeaturesView: View {
 
                         Spacer()
 
-                        Text("Restore")
-                            .font(.system(size: 11, weight: .semibold))
-                            .foregroundStyle(.white.opacity(0.9))
-                            .padding(.horizontal, 8)
-                            .padding(.vertical, 3)
-                            .background(
-                                RoundedRectangle(cornerRadius: 7, style: .continuous)
-                                    .fill(Color.white.opacity(0.16))
-                            )
+                        Button {
+                            Task { await premiumStore.restorePurchases() }
+                        } label: {
+                            Text("Restore")
+                                .font(.system(size: 11, weight: .semibold))
+                                .foregroundStyle(.white.opacity(0.9))
+                                .padding(.horizontal, 8)
+                                .padding(.vertical, 3)
+                                .background(
+                                    RoundedRectangle(cornerRadius: 7, style: .continuous)
+                                        .fill(Color.white.opacity(0.16))
+                                )
+                        }
+                        .buttonStyle(.plain)
+                        .disabled(premiumStore.purchaseInProgress)
                     }
 
                     Text("Upgrade to unlock all\nPremium Features")
@@ -77,6 +105,18 @@ struct PremiumFeaturesView: View {
                         .foregroundStyle(.white)
                         .lineSpacing(1)
 
+                    if let err = premiumStore.loadError {
+                        Text(err)
+                            .font(.system(size: 11, weight: .medium))
+                            .foregroundStyle(.orange.opacity(0.95))
+                    }
+
+                    if let err = premiumStore.purchaseError {
+                        Text(err)
+                            .font(.system(size: 11, weight: .medium))
+                            .foregroundStyle(.orange.opacity(0.95))
+                    }
+
                     LazyVGrid(
                         columns: [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10)],
                         spacing: 10
@@ -86,9 +126,44 @@ struct PremiumFeaturesView: View {
                         }
                     }
 
+                    if productsUnavailable {
+                        VStack(alignment: .leading, spacing: 10) {
+                            Text("Store products not loaded")
+                                .font(.system(size: 14, weight: .bold))
+                                .foregroundStyle(.white)
+                            Text(
+                                "Prices and checkout need product metadata from App Store Connect, or a StoreKit Configuration file when running from Xcode. Product IDs must match exactly."
+                            )
+                            .font(.system(size: 11, weight: .medium))
+                            .foregroundStyle(.white.opacity(0.85))
+                            .fixedSize(horizontal: false, vertical: true)
+                            Button {
+                                Task { await premiumStore.loadProducts() }
+                            } label: {
+                                Text("Retry")
+                                    .font(.system(size: 13, weight: .semibold))
+                                    .foregroundStyle(.white)
+                                    .frame(maxWidth: .infinity)
+                                    .padding(.vertical, 10)
+                                    .background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color.orange.opacity(0.85)))
+                            }
+                            .buttonStyle(.plain)
+                        }
+                        .padding(14)
+                        .frame(maxWidth: .infinity, alignment: .leading)
+                        .background(
+                            RoundedRectangle(cornerRadius: 14, style: .continuous)
+                                .fill(Color.orange.opacity(0.18))
+                        )
+                        .overlay(
+                            RoundedRectangle(cornerRadius: 14, style: .continuous)
+                                .stroke(Color.orange.opacity(0.45), lineWidth: 1)
+                        )
+                    }
+
                     priceCard(
                         title: "Perpetual",
-                        price: "Rs 9,900.00",
+                        price: priceLine(for: .perpetual),
                         subtitle: "Own the software for lifetime",
                         badgeText: "One-time payment",
                         isSelected: selectedPlan == .perpetual
@@ -99,8 +174,8 @@ struct PremiumFeaturesView: View {
 
                     priceCard(
                         title: "Yearly",
-                        price: "Rs 7,900.00/year",
-                        subtitle: "Rs 658.33/mo",
+                        price: priceLine(for: .yearly),
+                        subtitle: "Billed annually",
                         badgeText: nil,
                         isSelected: selectedPlan == .yearly
                     )
@@ -108,18 +183,34 @@ struct PremiumFeaturesView: View {
                         selectedPlan = .yearly
                     }
 
-                    Button(action: { showingCheckout = true }) {
-                        Text("Continue")
-                            .font(.system(size: 16, weight: .bold))
-                            .foregroundStyle(.white)
-                            .frame(maxWidth: .infinity)
-                            .padding(.vertical, 12)
-                            .background(
-                                RoundedRectangle(cornerRadius: 16, style: .continuous)
-                                    .fill(Color.blue.opacity(0.9))
-                            )
+                    Button(action: {
+                        guard let product = selectedProduct else { return }
+                        Task {
+                            let ok = await premiumStore.purchase(product)
+                            if ok {
+                                showingSuccessAlert = true
+                            }
+                        }
+                    }) {
+                        HStack {
+                            if premiumStore.purchaseInProgress {
+                                ProgressView()
+                                    .controlSize(.small)
+                                    .tint(.white)
+                            }
+                            Text("Continue")
+                                .font(.system(size: 16, weight: .bold))
+                        }
+                        .foregroundStyle(.white)
+                        .frame(maxWidth: .infinity)
+                        .padding(.vertical, 12)
+                        .background(
+                            RoundedRectangle(cornerRadius: 16, style: .continuous)
+                                .fill(continueEnabled ? Color.blue.opacity(0.9) : Color.gray.opacity(0.55))
+                        )
                     }
                     .buttonStyle(.plain)
+                    .disabled(!continueEnabled)
                     .padding(.top, 6)
 
                     HStack {
@@ -138,16 +229,18 @@ struct PremiumFeaturesView: View {
                 .padding(16)
             }
         }
-        .sheet(isPresented: $showingCheckout) {
-            PaymentCheckoutView(
-                selectedPlan: selectedPlan,
-                onCancel: { showingCheckout = false },
-                onConfirm: {
-                    showingCheckout = false
-                    showingSuccessAlert = true
-                }
-            )
-            .frame(minWidth: 500, minHeight: 520)
+        .task {
+            await premiumStore.loadProducts()
+        }
+        .onAppear {
+            if premiumStore.isPremiumUnlocked {
+                dismiss()
+            }
+        }
+        .onChange(of: premiumStore.isPremiumUnlocked) { unlocked in
+            if unlocked {
+                dismiss()
+            }
         }
         .alert("Payment Successful", isPresented: $showingSuccessAlert) {
             Button("Done") { dismiss() }
@@ -156,6 +249,14 @@ struct PremiumFeaturesView: View {
         }
     }
 
+    private var productsUnavailable: Bool {
+        premiumStore.products.isEmpty && !premiumStore.isLoadingProducts
+    }
+
+    private var continueEnabled: Bool {
+        selectedProduct != nil && !premiumStore.purchaseInProgress && !premiumStore.isLoadingProducts
+    }
+
     private func featureCard(title: String, subtitle: String, color: Color) -> some View {
         HStack(alignment: .top, spacing: 10) {
             Circle()
@@ -232,128 +333,8 @@ struct PremiumFeaturesView: View {
     }
 }
 
-private struct PaymentCheckoutView: View {
-    let selectedPlan: PremiumFeaturesView.PlanOption
-    let onCancel: () -> Void
-    let onConfirm: () -> Void
-
-    @State private var cardholderName = ""
-    @State private var cardNumber = ""
-    @State private var expiry = ""
-    @State private var cvv = ""
-    @State private var saveCard = true
-
-    private var amountText: String {
-        switch selectedPlan {
-        case .perpetual:
-            return "Rs 9,900.00"
-        case .yearly:
-            return "Rs 7,900.00/year"
-        }
-    }
-
-    private var canSubmit: Bool {
-        !cardholderName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
-        cardNumber.filter(\.isNumber).count >= 12 &&
-        expiry.count >= 4 &&
-        cvv.filter(\.isNumber).count >= 3
-    }
-
-    var body: some View {
-        ZStack {
-            LinearGradient(
-                colors: [Color.black.opacity(0.95), Color(red: 0.09, green: 0.11, blue: 0.16)],
-                startPoint: .topLeading,
-                endPoint: .bottomTrailing
-            )
-            .ignoresSafeArea()
-
-            VStack(alignment: .leading, spacing: 16) {
-                HStack {
-                    Text("Add Credit Card")
-                        .font(.system(size: 22, weight: .bold))
-                        .foregroundStyle(.white)
-                    Spacer()
-                    Button("Cancel", action: onCancel)
-                        .buttonStyle(.plain)
-                        .foregroundStyle(.white.opacity(0.8))
-                }
-
-                Text("Selected plan: \(selectedPlan == .perpetual ? "Perpetual" : "Yearly")")
-                    .font(.system(size: 13, weight: .medium))
-                    .foregroundStyle(.white.opacity(0.8))
-
-                VStack(spacing: 12) {
-                    checkoutField(title: "Cardholder Name", text: $cardholderName, placeholder: "Name on card")
-                    checkoutField(title: "Card Number", text: $cardNumber, placeholder: "1234 5678 9012 3456")
-
-                    HStack(spacing: 12) {
-                        checkoutField(title: "Expiry (MM/YY)", text: $expiry, placeholder: "08/28")
-                        checkoutField(title: "CVV", text: $cvv, placeholder: "123")
-                    }
-                }
-
-                Toggle("Save card for future purchases", isOn: $saveCard)
-                    .toggleStyle(.checkbox)
-                    .foregroundStyle(.white.opacity(0.88))
-
-                HStack {
-                    Text("Total")
-                        .foregroundStyle(.white.opacity(0.85))
-                    Spacer()
-                    Text(amountText)
-                        .font(.system(size: 15, weight: .bold))
-                        .foregroundStyle(.white)
-                }
-                .padding(12)
-                .background(
-                    RoundedRectangle(cornerRadius: 10, style: .continuous)
-                        .fill(Color.white.opacity(0.08))
-                )
-
-                Button(action: onConfirm) {
-                    Text("Pay \(amountText)")
-                        .font(.system(size: 15, weight: .bold))
-                        .foregroundStyle(.white)
-                        .frame(maxWidth: .infinity)
-                        .padding(.vertical, 12)
-                        .background(
-                            RoundedRectangle(cornerRadius: 12, style: .continuous)
-                                .fill(canSubmit ? Color.blue.opacity(0.95) : Color.gray.opacity(0.6))
-                        )
-                }
-                .buttonStyle(.plain)
-                .disabled(!canSubmit)
-
-                Spacer(minLength: 0)
-            }
-            .padding(20)
-        }
-    }
-
-    private func checkoutField(title: String, text: Binding<String>, placeholder: String) -> some View {
-        VStack(alignment: .leading, spacing: 6) {
-            Text(title)
-                .font(.system(size: 12, weight: .semibold))
-                .foregroundStyle(.white.opacity(0.85))
-            TextField(placeholder, text: text)
-                .textFieldStyle(.plain)
-                .foregroundStyle(.white)
-                .padding(.horizontal, 10)
-                .padding(.vertical, 9)
-                .background(
-                    RoundedRectangle(cornerRadius: 10, style: .continuous)
-                        .fill(Color.white.opacity(0.1))
-                )
-                .overlay(
-                    RoundedRectangle(cornerRadius: 10, style: .continuous)
-                        .stroke(Color.white.opacity(0.13), lineWidth: 1)
-                )
-        }
-    }
-}
-
 #Preview {
     PremiumFeaturesView()
+        .environmentObject(PremiumStore.shared)
         .frame(width: 560, height: 690)
 }

+ 132 - 0
google_apps/PremiumStore.swift

@@ -0,0 +1,132 @@
+import Combine
+import Foundation
+import StoreKit
+
+/// Product identifiers — create matching Non-Consumable + Auto-Renewable Subscription in App Store Connect.
+enum PremiumProductID: String, CaseIterable {
+    case perpetual = "com.mqldev.googleapps.premium.perpetual"
+    case yearly = "com.mqldev.googleapps.premium.yearly"
+}
+
+@MainActor
+final class PremiumStore: ObservableObject {
+    static let shared = PremiumStore()
+
+    @Published private(set) var products: [Product] = []
+    @Published private(set) var isPremiumUnlocked = false
+    @Published private(set) var isLoadingProducts = false
+    @Published private(set) var loadError: String?
+    @Published private(set) var purchaseInProgress = false
+    @Published var purchaseError: String?
+
+    private var transactionListener: Task<Void, Never>?
+
+    private init() {
+        transactionListener = Task { await listenForTransactions() }
+        Task { await refreshEntitlements() }
+    }
+
+    deinit {
+        transactionListener?.cancel()
+    }
+
+    func product(for premiumID: PremiumProductID) -> Product? {
+        products.first { $0.id == premiumID.rawValue }
+    }
+
+    func displayPrice(for premiumID: PremiumProductID) -> String? {
+        product(for: premiumID)?.displayPrice
+    }
+
+    func loadProducts() async {
+        isLoadingProducts = true
+        loadError = nil
+        defer { isLoadingProducts = false }
+
+        do {
+            let ids = PremiumProductID.allCases.map(\.rawValue)
+            let loaded = try await Product.products(for: ids)
+            products = loaded.sorted { $0.id < $1.id }
+            if loaded.isEmpty {
+                loadError = """
+                No products returned for IDs: \(ids.joined(separator: ", ")).
+                If testing locally, confirm Product > Scheme > Edit Scheme > Run > Options > StoreKit Configuration = Products.storekit, then stop/run again.
+                """
+            } else {
+                let loadedIDs = Set(loaded.map(\.id))
+                let missing = ids.filter { !loadedIDs.contains($0) }
+                if !missing.isEmpty {
+                    loadError = "Store returned partial products. Missing IDs: \(missing.joined(separator: ", "))"
+                }
+            }
+        } catch {
+            let ids = PremiumProductID.allCases.map(\.rawValue).joined(separator: ", ")
+            loadError = "StoreKit error: \(error.localizedDescription). Requested IDs: \(ids)"
+        }
+    }
+
+    func purchase(_ product: Product) async -> Bool {
+        purchaseInProgress = true
+        purchaseError = nil
+        defer { purchaseInProgress = false }
+
+        do {
+            let result = try await product.purchase()
+            switch result {
+            case .success(let verification):
+                switch verification {
+                case .verified(let transaction):
+                    await transaction.finish()
+                    await refreshEntitlements()
+                    return true
+                case .unverified(_, let error):
+                    purchaseError = error.localizedDescription
+                    return false
+                }
+            case .userCancelled:
+                return false
+            case .pending:
+                purchaseError = "Purchase is pending (e.g. Ask to Buy)."
+                return false
+            @unknown default:
+                return false
+            }
+        } catch {
+            purchaseError = error.localizedDescription
+            return false
+        }
+    }
+
+    func restorePurchases() async {
+        loadError = nil
+        do {
+            try await AppStore.sync()
+            await refreshEntitlements()
+        } catch {
+            loadError = error.localizedDescription
+        }
+    }
+
+    func refreshEntitlements() async {
+        let validIDs = Set(PremiumProductID.allCases.map(\.rawValue))
+        var unlocked = false
+
+        for await result in Transaction.currentEntitlements {
+            guard case .verified(let transaction) = result else { continue }
+            if validIDs.contains(transaction.productID) {
+                unlocked = true
+                break
+            }
+        }
+
+        isPremiumUnlocked = unlocked
+    }
+
+    private func listenForTransactions() async {
+        for await result in Transaction.updates {
+            guard case .verified(let transaction) = result else { continue }
+            await transaction.finish()
+            await refreshEntitlements()
+        }
+    }
+}

+ 87 - 0
google_apps/Products.storekit

@@ -0,0 +1,87 @@
+{
+  "appPolicies" : {
+    "eula" : "",
+    "policies" : [
+      {
+        "locale" : "en_US",
+        "policyText" : "",
+        "policyURL" : ""
+      }
+    ]
+  },
+  "identifier" : "Products",
+  "nonRenewingSubscriptions" : [
+
+  ],
+  "products" : [
+    {
+      "displayPrice" : "99.00",
+      "familyShareable" : true,
+      "internalID" : "premium_perpetual",
+      "localizations" : [
+        {
+          "description" : "Unlock all premium features with a one-time purchase.",
+          "displayName" : "Premium — Lifetime",
+          "locale" : "en_US"
+        }
+      ],
+      "productID" : "com.mqldev.googleapps.premium.perpetual",
+      "referenceName" : "Premium Perpetual",
+      "type" : "NonConsumable"
+    }
+  ],
+  "settings" : {
+    "_askToBuyEnabled" : false,
+    "_failTransactionsEnabled" : false,
+    "_locale" : "en_US",
+    "_storefront" : "USA",
+    "_storeKitErrors" : [
+
+    ],
+    "_storeKitErrorsEnabled" : false,
+    "_timeRate" : 0
+  },
+  "subscriptionGroups" : [
+    {
+      "id" : "premium_group",
+      "localizations" : [
+
+      ],
+      "name" : "Premium",
+      "subscriptions" : [
+        {
+          "adHocOffers" : [
+
+          ],
+          "codeOffers" : [
+
+          ],
+          "displayPrice" : "79.00",
+          "familyShareable" : true,
+          "groupNumber" : 1,
+          "internalID" : "premium_yearly",
+          "introductoryOffer" : null,
+          "localizations" : [
+            {
+              "description" : "Unlock all premium features for one year.",
+              "displayName" : "Premium — Yearly",
+              "locale" : "en_US"
+            }
+          ],
+          "productID" : "com.mqldev.googleapps.premium.yearly",
+          "recurringSubscriptionPeriod" : "P1Y",
+          "referenceName" : "Premium Yearly",
+          "subscriptionGroupID" : "premium_group",
+          "type" : "RecurringSubscription",
+          "winbackOffers" : [
+
+          ]
+        }
+      ]
+    }
+  ],
+  "version" : {
+    "major" : 4,
+    "minor" : 0
+  }
+}

+ 6 - 3
google_apps/ViewController.swift

@@ -9,12 +9,15 @@ import Cocoa
 import SwiftUI
 
 class ViewController: NSViewController {
-    private var hostingView: NSHostingView<LauncherRootView>?
+    private var hostingView: NSHostingView<AnyView>?
 
     override func viewDidLoad() {
         super.viewDidLoad()
-        let launcher = LauncherRootView()
-        let hostingView = NSHostingView(rootView: launcher)
+        let root = AnyView(
+            LauncherRootView()
+                .environmentObject(PremiumStore.shared)
+        )
+        let hostingView = NSHostingView(rootView: root)
         hostingView.translatesAutoresizingMaskIntoConstraints = false
         view.addSubview(hostingView)