import SwiftUI import AppKit import UniformTypeIdentifiers import WebKit struct LauncherRootView: View { enum LayoutMode { case grid case panel } @EnvironmentObject private var premiumStore: PremiumStore @State private var query = "" @State private var selectedApp: LauncherApp? @State private var showingPremiumScreen = false @State private var showingCreateAppSheet = false @State private var previouslyPremiumUnlocked = false @State private var startupPaywallTask: Task? @State private var customApps: [LauncherApp] = [] @State private var appOrder: [UUID] = [] @State private var draggedAppID: UUID? @State private var lastDropDestinationAppID: UUID? @State private var layoutMode: LayoutMode = .grid @AppStorage("customLauncherAppsData") private var customAppsData = "" @AppStorage("launcherAppOrderData") private var appOrderData = "" @AppStorage("pinnedTileIDsData") private var pinnedTileIDsData = "" @AppStorage("statusBarAppIDsData") private var statusBarAppIDsData = "" @AppStorage("widgetAppIDsData") private var widgetAppIDsData = "" @AppStorage("hiddenLauncherAppIDsData") private var hiddenLauncherAppIDsData = "" private var apps: [LauncherApp] { let hiddenIDs = decodeUUIDSet(from: hiddenLauncherAppIDsData) let visibleApps = (LauncherApp.sampleApps + customApps).filter { !hiddenIDs.contains($0.id) } return visibleApps + [LauncherApp.createNewTile] } private var columns: [GridItem] { [GridItem(.adaptive(minimum: 120, maximum: 145), spacing: 22)] } private var filteredApps: [LauncherApp] { let q = query.trimmingCharacters(in: .whitespacesAndNewlines) guard !q.isEmpty else { return orderedApps } return orderedApps.filter { $0.name.localizedCaseInsensitiveContains(q) } } private var orderedApps: [LauncherApp] { guard !apps.isEmpty else { return [] } let defaultIndexByID = Dictionary( uniqueKeysWithValues: apps.enumerated().map { ($0.element.id, $0.offset) } ) let rankByID = Dictionary( uniqueKeysWithValues: appOrder.enumerated().map { ($0.element, $0.offset) } ) let pinnedIDs = decodeUUIDSet(from: pinnedTileIDsData) return apps.sorted { lhs, rhs in if lhs.isCreateNew != rhs.isCreateNew { return rhs.isCreateNew } let leftPinned = pinnedIDs.contains(lhs.id) let rightPinned = pinnedIDs.contains(rhs.id) if leftPinned != rightPinned { return leftPinned } let leftRank = rankByID[lhs.id] ?? Int.max let rightRank = rankByID[rhs.id] ?? Int.max if leftRank != rightRank { return leftRank < rightRank } return (defaultIndexByID[lhs.id] ?? Int.max) < (defaultIndexByID[rhs.id] ?? Int.max) } } private var hiddenApps: [LauncherApp] { let hiddenIDs = decodeUUIDSet(from: hiddenLauncherAppIDsData) guard !hiddenIDs.isEmpty else { return [] } let allApps = LauncherApp.sampleApps + customApps return allApps .filter { hiddenIDs.contains($0.id) } .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } var body: some View { ZStack { VisualEffectBlur(material: .hudWindow, blendingMode: .behindWindow) .ignoresSafeArea() Color.black.opacity(0.22) .ignoresSafeArea() VStack(spacing: 16) { SearchHeader( query: $query, isPanelMode: layoutMode == .panel, hiddenApps: hiddenApps, onToggleLayout: { layoutMode = layoutMode == .grid ? .panel : .grid }, onOpenPremium: { showingPremiumScreen = true }, onOpenGoogle: { if let url = URL(string: "https://www.google.com") { InAppBrowserWindowManager.shared.open(url: url, title: "Google") } }, onRestoreApp: { appID in restoreHiddenApp(id: appID) }, onRestoreAll: { restoreAllHiddenApps() } ) .padding(.top, 12) PromoBanner( isPremiumUnlocked: premiumStore.isPremiumUnlocked, onCloseTap: { if !premiumStore.isPremiumUnlocked { showingPremiumScreen = true } }, onUpgradeTap: { showingPremiumScreen = true }, onManageSubscriptionTap: { openManageSubscriptions() } ) .padding(.bottom, 8) .zIndex(1) appsPage } .padding(.horizontal, 10) .padding(.bottom, 8) } .sheet(item: $selectedApp) { app in AppDetailView(app: app) .frame(minWidth: 360, minHeight: 220) } .sheet(isPresented: $showingPremiumScreen) { PremiumFeaturesView() .environmentObject(premiumStore) .frame(minWidth: 560, minHeight: 690) } .sheet(isPresented: $showingCreateAppSheet) { CreateAppSheetView { input in addCustomApp(name: input.name, url: input.url) } .frame(minWidth: 760, minHeight: 420) } .task { await premiumStore.refreshEntitlements() } .onChange(of: showingPremiumScreen) { isShowing in guard !isShowing else { return } Task { await premiumStore.refreshEntitlements() } } .onChange(of: premiumStore.isPremiumUnlocked) { unlocked in if previouslyPremiumUnlocked && !unlocked { showingPremiumScreen = true } previouslyPremiumUnlocked = unlocked } .onAppear { previouslyPremiumUnlocked = premiumStore.isPremiumUnlocked loadCustomAppsFromStorage() loadAppOrderFromStorage() normalizeAppOrderAndPersist() DesktopWidgetWindowManager.shared.restore(appProvider: { id in orderedApps.first { $0.id == id } }) NotificationCenter.default.addObserver( forName: .openWidgetsPage, object: nil, queue: .main ) { notification in let id = notification.userInfo?["appID"] as? UUID WidgetsWindowManager.shared.open( apps: orderedApps.filter { !$0.isCreateNew }, selectedAppID: id ) } startupPaywallTask?.cancel() startupPaywallTask = Task { try? await Task.sleep(nanoseconds: 2_000_000_000) guard !Task.isCancelled else { return } await premiumStore.refreshEntitlements() guard !premiumStore.isPremiumUnlocked else { return } guard !showingPremiumScreen else { return } showingPremiumScreen = true } } .onDisappear { startupPaywallTask?.cancel() startupPaywallTask = nil } } @ViewBuilder private var appsPage: some View { if filteredApps.isEmpty { VStack(spacing: 10) { Image(systemName: "magnifyingglass") .font(.system(size: 28, weight: .semibold)) .foregroundStyle(.white.opacity(0.75)) Text("No apps found") .font(.title3.weight(.semibold)) .foregroundStyle(.white.opacity(0.9)) Text("Try a different search term.") .font(.subheadline) .foregroundStyle(.white.opacity(0.7)) } .frame(maxWidth: .infinity, maxHeight: .infinity) } else { if layoutMode == .panel { IconPanelView( apps: filteredApps, onAppTap: { app in handleAppTap(app) }, draggedAppID: $draggedAppID, lastDropDestinationAppID: $lastDropDestinationAppID, onMove: moveApp, onHideApp: hideApp ) } else { ScrollView { LazyVGrid(columns: columns, spacing: 20) { ForEach(filteredApps) { app in AppTileView( app: app, onTap: { handleAppTap(app) }, onHide: { hideApp(id: app.id) } ) .onDrag { draggedAppID = app.id lastDropDestinationAppID = nil return NSItemProvider(object: app.id.uuidString as NSString) } .onDrop( of: [UTType.text], delegate: TileReorderDropDelegate( destinationAppID: app.id, draggedAppID: $draggedAppID, lastDropDestinationAppID: $lastDropDestinationAppID, onMove: moveApp ) ) } } .padding(.top, 10) .padding(.horizontal, 14) .padding(.bottom, 18) } } } } private func handleAppTap(_ app: LauncherApp) { if app.isCreateNew { showingCreateAppSheet = true return } if let webURL = app.webURL { InAppBrowserWindowManager.shared.open(url: webURL, title: app.name) return } selectedApp = app } private func addCustomApp(name: String, url: URL) { let newApp = LauncherApp( name: name, assetIconName: "", fallbackSymbolName: "globe", description: "Custom app shortcut.", isCreateNew: false, webURL: url ) customApps.append(newApp) saveCustomAppsToStorage() normalizeAppOrderAndPersist() } private func loadCustomAppsFromStorage() { guard !customAppsData.isEmpty else { customApps = [] return } guard let data = customAppsData.data(using: .utf8) else { customApps = [] return } do { let persisted = try JSONDecoder().decode([PersistedCustomApp].self, from: data) customApps = persisted.compactMap { item in guard let parsedURL = URL(string: item.urlString) else { return nil } return LauncherApp( id: item.id, name: item.name, assetIconName: "", fallbackSymbolName: "globe", description: "Custom app shortcut.", isCreateNew: false, webURL: parsedURL ) } } catch { customApps = [] } } private func saveCustomAppsToStorage() { let persisted = customApps.compactMap { app -> PersistedCustomApp? in guard !app.isCreateNew, let urlString = app.webURL?.absoluteString else { return nil } return PersistedCustomApp(id: app.id, name: app.name, urlString: urlString) } do { let encoded = try JSONEncoder().encode(persisted) customAppsData = String(decoding: encoded, as: UTF8.self) } catch { // Keep UI responsive even if persistence fails. } } private func moveApp(from sourceID: UUID, to destinationID: UUID) { guard sourceID != destinationID else { return } guard let sourceIndex = appOrder.firstIndex(of: sourceID), let destinationIndex = appOrder.firstIndex(of: destinationID) else { return } var updated = appOrder let movedID = updated.remove(at: sourceIndex) let insertionIndex = destinationIndex > sourceIndex ? destinationIndex - 1 : destinationIndex updated.insert(movedID, at: insertionIndex) appOrder = updated saveAppOrderToStorage() } private func hideApp(id: UUID) { var hiddenIDs = decodeUUIDSet(from: hiddenLauncherAppIDsData) guard hiddenIDs.insert(id).inserted else { return } hiddenLauncherAppIDsData = encodeUUIDSetGlobal(hiddenIDs) removeIDFromLinkedCollections(id) } private func restoreHiddenApp(id: UUID) { var hiddenIDs = decodeUUIDSet(from: hiddenLauncherAppIDsData) guard hiddenIDs.remove(id) != nil else { return } hiddenLauncherAppIDsData = encodeUUIDSetGlobal(hiddenIDs) } private func restoreAllHiddenApps() { hiddenLauncherAppIDsData = "" } private func removeIDFromLinkedCollections(_ id: UUID) { var pinnedIDs = decodeUUIDSet(from: pinnedTileIDsData) pinnedIDs.remove(id) pinnedTileIDsData = encodeUUIDSetGlobal(pinnedIDs) var statusBarIDs = decodeUUIDSet(from: statusBarAppIDsData) if statusBarIDs.remove(id) != nil { statusBarAppIDsData = encodeUUIDSetGlobal(statusBarIDs) LauncherApp.notifyStatusBarShortcutIdentifiersChanged() } var widgetIDs = decodeUUIDSet(from: widgetAppIDsData) widgetIDs.remove(id) widgetAppIDsData = encodeUUIDSetGlobal(widgetIDs) } private func loadAppOrderFromStorage() { guard !appOrderData.isEmpty else { appOrder = [] return } guard let data = appOrderData.data(using: .utf8) else { appOrder = [] return } do { let decoded = try JSONDecoder().decode([String].self, from: data) appOrder = decoded.compactMap { UUID(uuidString: $0) } } catch { appOrder = [] } } private func normalizeAppOrderAndPersist() { let currentIDs = apps.map(\.id) var seen = Set() var normalized: [UUID] = [] for id in appOrder where currentIDs.contains(id) { if seen.insert(id).inserted { normalized.append(id) } } for id in currentIDs where seen.insert(id).inserted { normalized.append(id) } appOrder = normalized saveAppOrderToStorage() } private func saveAppOrderToStorage() { do { let payload = appOrder.map(\.uuidString) let encoded = try JSONEncoder().encode(payload) appOrderData = String(decoding: encoded, as: UTF8.self) } catch { // Ignore persistence failure and keep local order. } } private func decodeUUIDSet(from dataString: String) -> Set { guard !dataString.isEmpty, let data = dataString.data(using: .utf8) else { return [] } do { let strings = try JSONDecoder().decode([String].self, from: data) return Set(strings.compactMap(UUID.init(uuidString:))) } catch { return [] } } private func openManageSubscriptions() { let appStoreURL = URL(string: "macappstore://apps.apple.com/account/subscriptions")! if NSWorkspace.shared.open(appStoreURL) { return } let webURL = URL(string: "https://apps.apple.com/account/subscriptions")! _ = NSWorkspace.shared.open(webURL) } } private struct TileReorderDropDelegate: DropDelegate { let destinationAppID: UUID @Binding var draggedAppID: UUID? @Binding var lastDropDestinationAppID: UUID? let onMove: (UUID, UUID) -> Void func dropEntered(info: DropInfo) { guard let draggedAppID else { return } guard draggedAppID != destinationAppID else { return } // SwiftUI may call `dropEntered` multiple times while hovering the same tile. // Avoid reordering repeatedly to keep drag-to-reorder stable. guard lastDropDestinationAppID != destinationAppID else { return } lastDropDestinationAppID = destinationAppID onMove(draggedAppID, destinationAppID) } func performDrop(info: DropInfo) -> Bool { draggedAppID = nil lastDropDestinationAppID = nil return true } } private struct SearchHeader: View { @Binding var query: String let isPanelMode: Bool let hiddenApps: [LauncherApp] let onToggleLayout: () -> Void let onOpenPremium: () -> Void let onOpenGoogle: () -> Void let onRestoreApp: (UUID) -> Void let onRestoreAll: () -> Void var body: some View { HStack { HStack(spacing: 10) { Image(systemName: "magnifyingglass") .foregroundStyle(.white.opacity(0.82)) TextField("Search", text: $query) .textFieldStyle(.plain) .foregroundStyle(.white.opacity(0.94)) } .padding(.horizontal, 12) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: 15, style: .continuous) .fill(Color.black.opacity(0.28)) ) .overlay( RoundedRectangle(cornerRadius: 15, style: .continuous) .stroke(Color.white.opacity(0.08), lineWidth: 1) ) Spacer(minLength: 8) HStack(spacing: 12) { Button(action: onToggleLayout) { Image(systemName: isPanelMode ? "square.grid.2x2" : "list.bullet") } .buttonStyle(.plain) Menu { Button("Clear Search") { query = "" } Button("Open Premium") { onOpenPremium() } Button("Open Google") { onOpenGoogle() } if !hiddenApps.isEmpty { Divider() Menu("Manage Hidden Apps") { ForEach(hiddenApps) { app in Button("Add Back \(app.name)") { onRestoreApp(app.id) } } Divider() Button("Restore All Hidden Apps") { onRestoreAll() } } } } label: { Image(systemName: "ellipsis") } .menuStyle(.borderlessButton) } .font(.system(size: 15, weight: .semibold)) .foregroundStyle(.white.opacity(0.82)) .padding(.trailing, 4) } } } private struct IconPanelView: View { let apps: [LauncherApp] let onAppTap: (LauncherApp) -> Void @Binding var draggedAppID: UUID? @Binding var lastDropDestinationAppID: UUID? let onMove: (UUID, UUID) -> Void let onHideApp: (UUID) -> Void var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { ForEach(apps) { app in PanelAppRowView( app: app, onAppTap: onAppTap, draggedAppID: $draggedAppID, lastDropDestinationAppID: $lastDropDestinationAppID, onMove: onMove, onHideApp: onHideApp ) } } .padding(.horizontal, 10) .padding(.bottom, 18) } } } private struct PanelAppRowView: View { let app: LauncherApp let onAppTap: (LauncherApp) -> Void @Binding var draggedAppID: UUID? @Binding var lastDropDestinationAppID: UUID? let onMove: (UUID, UUID) -> Void let onHideApp: (UUID) -> Void @ObservedObject private var premiumStore = PremiumStore.shared @AppStorage("pinnedTileIDsData") private var pinnedTileIDsData = "" @AppStorage("statusBarAppIDsData") private var statusBarAppIDsData = "" @AppStorage("widgetAppIDsData") private var widgetAppIDsData = "" @State private var isHovering = false private var pinnedIDs: Set { decodeUUIDSetGlobal(from: pinnedTileIDsData) } private var isPinned: Bool { pinnedIDs.contains(app.id) } private var statusBarIDs: Set { decodeUUIDSetGlobal(from: statusBarAppIDsData) } private var isInStatusBar: Bool { statusBarIDs.contains(app.id) } var body: some View { ZStack(alignment: .topTrailing) { Button(action: { onAppTap(app) }) { HStack(spacing: 14) { AppIconGlyph(app: app) Text(app.name) .font(.system(size: 18, weight: .medium)) .foregroundStyle(.white.opacity(0.94)) .lineLimit(1) .truncationMode(.tail) Spacer(minLength: 0) } .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } .buttonStyle(.plain) if isPinned && !app.isCreateNew { Image(systemName: "pin.fill") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(.white.opacity(0.92)) .frame(width: 26, height: 26) .background( Circle() .fill(Color.black.opacity(isHovering ? 0.35 : 0.2)) ) .overlay( Circle() .stroke(Color.white.opacity(isHovering ? 0.18 : 0.12), lineWidth: 1) ) .shadow(color: .black.opacity(0.22), radius: 10, x: 0, y: 6) .padding(8) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .allowsHitTesting(false) } if !app.isCreateNew { Menu { Button(isPinned ? "Unpin" : "Pin") { togglePin() } Divider() if isInStatusBar { Button("Remove from StatusBar") { removeFromStatusBar() } } else { Button("Add to StatusBar") { addToStatusBar() } .disabled(!premiumStore.isPremiumUnlocked) } Button("Add to desktop") { addToDesktop() } .disabled(!premiumStore.isPremiumUnlocked) Button("Add Widget") { addWidget() } Divider() Button("Remove from View") { onHideApp(app.id) } } label: { Image(systemName: "ellipsis") .font(.system(size: 13, weight: .semibold)) .rotationEffect(.degrees(90)) .foregroundStyle(.white.opacity(isHovering ? 0.92 : 0.75)) .frame(width: 26, height: 26) .background( Circle() .fill(Color.black.opacity(isHovering ? 0.35 : 0.2)) ) .overlay( Circle() .stroke(Color.white.opacity(isHovering ? 0.18 : 0.12), lineWidth: 1) ) .shadow(color: .black.opacity(0.25), radius: 10, x: 0, y: 6) } .menuStyle(.borderlessButton) .modifier(HideMenuIndicatorIfAvailable()) .padding(8) .opacity(isHovering ? 1 : 0.0) .animation(.easeOut(duration: 0.12), value: isHovering) } } .onHover { hovering in isHovering = hovering } .contextMenu { if !app.isCreateNew { Button(isPinned ? "Unpin" : "Pin") { togglePin() } Divider() if isInStatusBar { Button("Remove from StatusBar") { removeFromStatusBar() } } else { Button("Add to StatusBar") { addToStatusBar() } .disabled(!premiumStore.isPremiumUnlocked) } Button("Add to desktop") { addToDesktop() } .disabled(!premiumStore.isPremiumUnlocked) Button("Add Widget") { addWidget() } Divider() Button("Remove from View") { onHideApp(app.id) } } } .padding(.vertical, 3) .onDrag { draggedAppID = app.id lastDropDestinationAppID = nil return NSItemProvider(object: app.id.uuidString as NSString) } .onDrop( of: [UTType.text], delegate: TileReorderDropDelegate( destinationAppID: app.id, draggedAppID: $draggedAppID, lastDropDestinationAppID: $lastDropDestinationAppID, onMove: onMove ) ) } private func togglePin() { var updated = pinnedIDs if updated.contains(app.id) { updated.remove(app.id) } else { updated.insert(app.id) } pinnedTileIDsData = encodeUUIDSetGlobal(updated) } private func addToStatusBar() { var updated = decodeUUIDSetGlobal(from: statusBarAppIDsData) guard updated.insert(app.id).inserted else { showAlert(title: "Already added", message: "“\(app.name)” is already in the menu bar.") return } statusBarAppIDsData = encodeUUIDSetGlobal(updated) LauncherApp.notifyStatusBarShortcutIdentifiersChanged() showAlert( title: "Added to Status Bar", message: "“\(app.name)” now has its own icon in the menu bar. Click it to open, or right‑click to remove." ) } private func removeFromStatusBar() { var updated = decodeUUIDSetGlobal(from: statusBarAppIDsData) guard updated.remove(app.id) != nil else { return } statusBarAppIDsData = encodeUUIDSetGlobal(updated) LauncherApp.notifyStatusBarShortcutIdentifiersChanged() } private func addWidget() { var updated = decodeUUIDSetGlobal(from: widgetAppIDsData) updated.insert(app.id) widgetAppIDsData = encodeUUIDSetGlobal(updated) NotificationCenter.default.post( name: .openWidgetsPage, object: nil, userInfo: ["appID": app.id] ) } private func addToDesktop() { guard let webURL = app.webURL else { showAlert(title: "Unavailable", message: "Desktop shortcuts need a web address.") return } DesktopWeblocShortcutPresenter.beginSavePanel(forAppNamed: app.name, webURL: webURL) } private func showAlert(title: String, message: String) { let alert = NSAlert() alert.messageText = title alert.informativeText = message alert.addButton(withTitle: "OK") alert.alertStyle = .informational alert.runModal() } } private struct AppIconGlyph: View { let app: LauncherApp var body: some View { ZStack { RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(Color.white.opacity(0.08)) .frame(width: 48, height: 48) if let webURL = app.webURL { WebSiteFaviconView( webURL: webURL, size: 32, assetIconName: app.assetIconName, fallbackSymbolName: app.fallbackSymbolName, iconPaddingFactor: 0.08 ) } else if let icon = NSImage(named: app.assetIconName) { Image(nsImage: icon) .resizable() .scaledToFit() .frame(width: 32, height: 32) } else { Image(systemName: app.fallbackSymbolName) .font(.system(size: 20, weight: .semibold)) .foregroundStyle(.white) } } } } // MARK: - Pinned app persistence helpers (panel mode) private func decodeUUIDSetGlobal(from dataString: String) -> Set { guard !dataString.isEmpty, let data = dataString.data(using: .utf8) else { return [] } do { let strings = try JSONDecoder().decode([String].self, from: data) return Set(strings.compactMap(UUID.init(uuidString:))) } catch { return [] } } private func encodeUUIDSetGlobal(_ set: Set) -> String { do { let strings = set.map(\.uuidString).sorted() let data = try JSONEncoder().encode(strings) return String(decoding: data, as: UTF8.self) } catch { return "" } } private struct PromoBanner: View { let isPremiumUnlocked: Bool let onCloseTap: () -> Void let onUpgradeTap: () -> Void let onManageSubscriptionTap: () -> Void var body: some View { HStack(spacing: 10) { Button(action: onCloseTap) { Image(systemName: "xmark") .foregroundStyle(.blue.opacity(0.95)) .font(.system(size: 14, weight: .semibold)) } .buttonStyle(.plain) Text(isPremiumUnlocked ? "Premium is active. Manage your subscription any time." : "Update to Premium to unlock all features and remove ads") .font(.system(size: 13, weight: .medium)) .foregroundStyle(.blue.opacity(0.95)) .lineLimit(1) Spacer() Button(action: isPremiumUnlocked ? onManageSubscriptionTap : onUpgradeTap) { Text(isPremiumUnlocked ? "Manage Subscription" : "Upgrade") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.white) .padding(.horizontal, 12) .padding(.vertical, 6) .background( Capsule(style: .continuous) .fill(Color.blue.opacity(0.92)) ) } .buttonStyle(.plain) } .padding(.horizontal, 11) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(Color.white.opacity(0.03)) ) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.05), lineWidth: 1) ) } } private struct ManageSubscriptionSheet: View { @Environment(\.dismiss) private var dismiss let activePremiumProductID: PremiumProductID? let onUpgradeTap: () -> Void private let url = URL(string: "https://apps.apple.com/account/subscriptions")! var body: some View { VStack(spacing: 0) { HStack { Text("Manage Subscription") .font(.system(size: 15, weight: .semibold)) .foregroundStyle(.white.opacity(0.92)) Spacer() Button("Done") { dismiss() } .buttonStyle(.borderedProminent) .controlSize(.small) } .padding(12) .background(Color.black.opacity(0.25)) if activePremiumProductID == .yearly { HStack(spacing: 10) { Text("You are on the yearly plan. Upgrade to Lifetime for permanent access.") .font(.system(size: 12.5, weight: .medium)) .foregroundStyle(.white.opacity(0.9)) Spacer() Button("Upgrade Package") { onUpgradeTap() } .buttonStyle(.borderedProminent) .controlSize(.small) } .padding(.horizontal, 12) .padding(.vertical, 10) .background(Color.blue.opacity(0.18)) } SubscriptionWebView(url: url) } .background(Color.black.opacity(0.3)) } } private struct SubscriptionWebView: NSViewRepresentable { let url: URL func makeNSView(context: Context) -> WKWebView { let webView = WKWebView() webView.allowsBackForwardNavigationGestures = true webView.load(URLRequest(url: url)) return webView } func updateNSView(_ webView: WKWebView, context: Context) {} } private struct HideMenuIndicatorIfAvailable: ViewModifier { func body(content: Content) -> some View { if #available(macOS 13.0, *) { content.menuIndicator(.hidden) } else { content } } } #Preview { LauncherRootView() .environmentObject(PremiumStore.shared) }