Full Code of betomoedano/quick-push for AI

main f5da1c5fcb9f cached
69 files
219.1 KB
56.4k tokens
1 requests
Download .txt
Showing preview only (239K chars total). Download the full file or copy to clipboard to get everything.
Repository: betomoedano/quick-push
Branch: main
Commit: f5da1c5fcb9f
Files: 69
Total size: 219.1 KB

Directory structure:
gitextract_jnez51gj/

├── LICENSE
├── QuickPush/
│   ├── Assets.xcassets/
│   │   ├── AccentColor.colorset/
│   │   │   └── Contents.json
│   │   ├── Contents.json
│   │   └── QuickPushIcon/
│   │       ├── Assets/
│   │       │   ├── Contents.json
│   │       │   └── bolt.brakesignal 1.imageset/
│   │       │       └── Contents.json
│   │       ├── Contents.json
│   │       └── icon.dataset/
│   │           ├── Contents.json
│   │           └── icon.json
│   ├── ContentView.swift
│   ├── Controllers/
│   │   ├── APNsService.swift
│   │   ├── FCMService.swift
│   │   ├── JWTSigner.swift
│   │   └── PushNotificationService.swift
│   ├── Models/
│   │   ├── APNsConfigStore.swift
│   │   ├── APNsConfiguration.swift
│   │   ├── FCMConfigStore.swift
│   │   ├── FCMConfiguration.swift
│   │   ├── FCMPayload.swift
│   │   ├── LiveActivityPayload.swift
│   │   ├── NativePushPayload.swift
│   │   ├── PushNotification.swift
│   │   ├── PushResponse.swift
│   │   ├── ReceiptResponse.swift
│   │   ├── SavedToken.swift
│   │   └── TokenToSave.swift
│   ├── Preview Content/
│   │   └── Preview Assets.xcassets/
│   │       └── Contents.json
│   ├── QuickPush.entitlements
│   ├── QuickPushApp.swift
│   ├── QuickPushIcon.icon/
│   │   └── icon.json
│   ├── Utilities/
│   │   ├── ColorHexConverter.swift
│   │   ├── FCMFileManager.swift
│   │   ├── FloatingPanel.swift
│   │   ├── SavedTokenStore.swift
│   │   ├── SecurityBookmarkManager.swift
│   │   ├── View+Extensions.swift
│   │   └── WindowManager.swift
│   ├── ViewModels/
│   │   ├── FCMViewModel.swift
│   │   ├── LiveActivityViewModel.swift
│   │   └── NativePushViewModel.swift
│   └── Views/
│       ├── APNsConfigurationView.swift
│       ├── APNsCurlCommandView.swift
│       ├── APNsResponseDetailView.swift
│       ├── APNsView.swift
│       ├── ColorPickerField.swift
│       ├── ExpoCurlCommandView.swift
│       ├── ExpoReceiptView.swift
│       ├── ExpoResponseDetailView.swift
│       ├── FCMConfigurationView.swift
│       ├── FCMCurlCommandView.swift
│       ├── FCMResponseDetailView.swift
│       ├── FCMView.swift
│       ├── FooterView.swift
│       ├── JSONImportExportView.swift
│       ├── KeyValueInputView.swift
│       ├── LiveActivityAlertSection.swift
│       ├── LiveActivityAttributesSection.swift
│       ├── LiveActivityContentStateSection.swift
│       ├── LiveActivityView.swift
│       ├── MainContentView.swift
│       ├── PushNotificationView.swift
│       ├── SaveTokenSheet.swift
│       └── SavedTokenRowView.swift
├── QuickPush.xcodeproj/
│   ├── project.pbxproj
│   ├── project.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   ├── xcshareddata/
│   │   │   └── swiftpm/
│   │   │       └── Package.resolved
│   │   └── xcuserdata/
│   │       └── beto.xcuserdatad/
│   │           └── UserInterfaceState.xcuserstate
│   └── xcuserdata/
│       └── beto.xcuserdatad/
│           ├── xcdebugger/
│           │   └── Breakpoints_v2.xcbkptlist
│           └── xcschemes/
│               └── xcschememanagement.plist
└── README.md

================================================
FILE CONTENTS
================================================

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 Code with Beto LLC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: QuickPush/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
  "colors" : [
    {
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: QuickPush/Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: QuickPush/Assets.xcassets/QuickPushIcon/Assets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: QuickPush/Assets.xcassets/QuickPushIcon/Assets/bolt.brakesignal 1.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "bolt.brakesignal 1.svg",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: QuickPush/Assets.xcassets/QuickPushIcon/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: QuickPush/Assets.xcassets/QuickPushIcon/icon.dataset/Contents.json
================================================
{
  "data" : [
    {
      "filename" : "icon.json",
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: QuickPush/Assets.xcassets/QuickPushIcon/icon.dataset/icon.json
================================================
{
  "fill" : "automatic",
  "groups" : [
    {
      "layers" : [
        {
          "fill-specializations" : [
            {
              "appearance" : "dark",
              "value" : {
                "solid" : "extended-gray:1.00000,1.00000"
              }
            }
          ],
          "glass" : true,
          "image-name" : "bolt.brakesignal 1.svg",
          "name" : "bolt.brakesignal 1",
          "position" : {
            "scale" : 6.81,
            "translation-in-points" : [
              0,
              0
            ]
          }
        }
      ],
      "position" : {
        "scale" : 1,
        "translation-in-points" : [
          5,
          0
        ]
      },
      "shadow" : {
        "kind" : "neutral",
        "opacity" : 0.5
      },
      "translucency" : {
        "enabled" : true,
        "value" : 0.5
      }
    }
  ],
  "supported-platforms" : {
    "circles" : [
      "watchOS"
    ],
    "squares" : "shared"
  }
}

================================================
FILE: QuickPush/ContentView.swift
================================================
//
//  ContentView.swift
//  QuickPush
//
//  Created by beto on 2/20/25.
//

import SwiftUI

enum AppTab: String, CaseIterable {
  case pushNotification = "Expo Notification"
  case liveActivity = "Live Activity"
  case apnsPush = "APNs"
  case fcm = "FCM"

  var icon: String {
    switch self {
    case .pushNotification: return "bell.badge"
    case .liveActivity: return "waveform"
    case .apnsPush: return "antenna.radiowaves.left.and.right"
    case .fcm: return "server.rack"
    }
  }

  /// Label shown in the segmented picker, embedding the ⌘N shortcut hint.
  var tabLabel: String {
    switch self {
    case .pushNotification: return "Expo Notification  ⌘1"
    case .liveActivity: return "Live Activity  ⌘2"
    case .apnsPush: return "APNs  ⌘3"
    case .fcm: return "FCM  ⌘4"
    }
  }
}

struct ContentView: View {
  @State private var selectedTab: AppTab = .pushNotification

  var body: some View {
    VStack(spacing: 0) {
      // Tab Picker
      Picker("", selection: $selectedTab) {
        ForEach(AppTab.allCases, id: \.self) { tab in
          Label(tab.tabLabel, systemImage: tab.icon).tag(tab)
        }
      }
      .pickerStyle(.segmented)
      .padding(.top, 12)
      .padding(.bottom, 4)

      // Tab Content — all views stay alive so @State is preserved
      PushNotificationView(isActive: selectedTab == .pushNotification)
        .opacity(selectedTab == .pushNotification ? 1 : 0)
        .frame(height: selectedTab == .pushNotification ? nil : 0)
        .allowsHitTesting(selectedTab == .pushNotification)

      LiveActivityView(isActive: selectedTab == .liveActivity)
        .opacity(selectedTab == .liveActivity ? 1 : 0)
        .frame(height: selectedTab == .liveActivity ? nil : 0)
        .allowsHitTesting(selectedTab == .liveActivity)

      APNsView(isActive: selectedTab == .apnsPush)
        .opacity(selectedTab == .apnsPush ? 1 : 0)
        .frame(height: selectedTab == .apnsPush ? nil : 0)
        .allowsHitTesting(selectedTab == .apnsPush)

      FCMView(isActive: selectedTab == .fcm)
        .opacity(selectedTab == .fcm ? 1 : 0)
        .frame(height: selectedTab == .fcm ? nil : 0)
        .allowsHitTesting(selectedTab == .fcm)
    }
    .frame(minHeight: 410)
    // ⌘1/2/3/4 to switch tabs
    .background(
      Group {
        Button("") { selectedTab = .pushNotification }
          .keyboardShortcut("1", modifiers: .command)
        Button("") { selectedTab = .liveActivity }
          .keyboardShortcut("2", modifiers: .command)
        Button("") { selectedTab = .apnsPush }
          .keyboardShortcut("3", modifiers: .command)
        Button("") { selectedTab = .fcm }
          .keyboardShortcut("4", modifiers: .command)
      }
      .frame(width: 0, height: 0)
      .opacity(0)
    )
  }
}

// MARK: - Toast Types and View
enum ToastType {
  case success
  case error

  var backgroundColor: Color {
    switch self {
    case .success: return Color.green.opacity(0.9)
    case .error: return Color.red.opacity(0.9)
    }
  }

  var icon: String {
    switch self {
    case .success: return "checkmark.circle.fill"
    case .error: return "exclamationmark.circle.fill"
    }
  }
}

struct ToastView: View {
  let message: String
  let type: ToastType
  @Binding var isPresented: Bool

  private var dismissDelay: TimeInterval {
    type == .error ? 5 : 3
  }

  var body: some View {
    VStack {
      Spacer()
      if isPresented {
        HStack(spacing: 12) {
          Image(systemName: type.icon)
          Text(message)
            .foregroundColor(.white)
        }
        .padding()
        .background(type.backgroundColor)
        .cornerRadius(8)
        .padding(.bottom, 20)
        .onAppear {
          DispatchQueue.main.asyncAfter(deadline: .now() + dismissDelay) {
            withAnimation {
              isPresented = false
            }
          }
        }
      }
    }
  }
}

// MARK: - Reusable InputField with Tooltip
struct InputField: View {
  let label: String
  @Binding var text: String
  let helpText: String
  var isRequired: Bool = false
  var showError: Bool = false

  var body: some View {
    HStack {
      Text("\(label):")
      TextField(label, text: $text)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .overlay(
          RoundedRectangle(cornerRadius: 6)
            .stroke(showError ? Color.red : Color.clear, lineWidth: 1)
        )
      HelpButton(helpText: helpText)
    }
  }
}

// MARK: - Help Button with Popover
struct HelpButton: View {
  let helpText: String
  @State private var showHelp = false

  var body: some View {
    Button(action: { showHelp.toggle() }) {
      Image(systemName: "questionmark.circle")
        .foregroundColor(.secondary)
    }
    .popover(isPresented: $showHelp) {
      VStack(alignment: .leading, spacing: 8) {
        Text(attributedHelpText)
          .textSelection(.enabled)
      }
      .padding()
      .frame(width: 300)
    }
    .buttonStyle(PlainButtonStyle())
    .focusable(false)
  }

  private var attributedHelpText: AttributedString {
    var attributedString = AttributedString(helpText)
    // Find all URLs and make them clickable
    var searchStart = helpText.startIndex
    while let urlRange = helpText.range(of: "https://[^\\s]+", options: .regularExpression, range: searchStart..<helpText.endIndex),
          let url = URL(string: String(helpText[urlRange])),
          let attrRange = attributedString.range(of: String(helpText[urlRange])) {
      attributedString[attrRange].link = url
      attributedString[attrRange].foregroundColor = .blue
      attributedString[attrRange].underlineStyle = .single
      searchStart = urlRange.upperBound
    }
    return attributedString
  }
}

#Preview {
  ContentView()
}


================================================
FILE: QuickPush/Controllers/APNsService.swift
================================================
//
//  APNsService.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import Foundation

/// Full diagnostic info returned after every APNs request.
struct APNsResponse {
  let statusCode: Int
  let reason: String?
  let apnsId: String?
  let apnsUniqueId: String?
  let environment: APNsEnvironment
  let topic: String
  let timestamp: Int
  let hostname: String
  let attributesType: String?
  let event: String

  var isSuccess: Bool { statusCode == 200 }

  /// Short summary shown in the toast.
  var summary: String {
    let envLabel = environment.rawValue.capitalized
    if isSuccess {
      return "200 OK (\(envLabel))"
    } else {
      return "\(statusCode): \(reason ?? "Unknown") (\(envLabel))"
    }
  }

  /// Multi-line diagnostic block for the detail view.
  var diagnosticDetails: String {
    var lines: [String] = []
    lines.append("Status:         \(statusCode) \(isSuccess ? "OK" : "Error")")
    if let reason { lines.append("Reason:         \(reason)") }
    lines.append("Environment:    \(environment.rawValue.capitalized) (\(hostname))")
    lines.append("Topic:          \(topic)")
    lines.append("Event:          \(event)")
    if let attributesType { lines.append("Attributes Type: \(attributesType)") }
    lines.append("Timestamp:      \(timestamp) (Unix seconds)")
    if let apnsId { lines.append("apns-id:        \(apnsId)") }
    if let apnsUniqueId { lines.append("apns-unique-id: \(apnsUniqueId)") }
    return lines.joined(separator: "\n")
  }
}

class APNsService {
  static let shared = APNsService()

  private var cachedToken: String?
  private var tokenGeneratedAt: Date?
  private let tokenLifetime: TimeInterval = 50 * 60 // 50 minutes (APNs allows 60)

  enum APNsError: Error, LocalizedError {
    case invalidConfiguration
    case cannotReadP8File
    case requestFailed(response: APNsResponse)
    case networkError(String)

    var errorDescription: String? {
      switch self {
      case .invalidConfiguration:
        return "APNs configuration is incomplete. Please fill in all fields."
      case .cannotReadP8File:
        return "Cannot read .p8 key file. Please re-select the file."
      case .requestFailed(let response):
        return "APNs error: \(response.summary)"
      case .networkError(let message):
        return "Network error: \(message)"
      }
    }
  }

  func sendLiveActivityPush(
    payload: LiveActivityAPNsPayload,
    token: String,
    configuration: APNsConfiguration,
    completion: @escaping (Result<APNsResponse, Error>) -> Void
  ) {
    guard configuration.isValid else {
      completion(.failure(APNsError.invalidConfiguration))
      return
    }

    guard let p8Contents = configuration.p8Contents, !p8Contents.isEmpty else {
      completion(.failure(APNsError.cannotReadP8File))
      return
    }

    let jwt: String
    do {
      jwt = try getOrRefreshToken(
        teamId: configuration.teamId,
        keyId: configuration.keyId,
        p8Contents: p8Contents
      )
    } catch {
      completion(.failure(error))
      return
    }

    let cleanToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
    let urlString = "https://\(configuration.hostname)/3/device/\(cleanToken)"
    guard let url = URL(string: urlString) else {
      completion(.failure(APNsError.networkError("Invalid URL")))
      return
    }

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization")
    request.setValue(configuration.topic, forHTTPHeaderField: "apns-topic")
    request.setValue("liveactivity", forHTTPHeaderField: "apns-push-type")
    request.setValue("10", forHTTPHeaderField: "apns-priority")
    request.setValue("application/json", forHTTPHeaderField: "content-type")

    let payloadTimestamp = payload.aps.timestamp

    do {
      let encoder = JSONEncoder()
      request.httpBody = try encoder.encode(payload)
    } catch {
      completion(.failure(error))
      return
    }

    URLSession.shared.dataTask(with: request) { data, response, error in
      if let error = error {
        completion(.failure(APNsError.networkError(error.localizedDescription)))
        return
      }

      guard let httpResponse = response as? HTTPURLResponse else {
        completion(.failure(APNsError.networkError("Invalid response")))
        return
      }

      let apnsId = httpResponse.value(forHTTPHeaderField: "apns-id")
      let apnsUniqueId = httpResponse.value(forHTTPHeaderField: "apns-unique-id")

      var reason: String?
      if let data = data,
         let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
         let r = json["reason"] as? String {
        reason = r
      }

      let apnsResponse = APNsResponse(
        statusCode: httpResponse.statusCode,
        reason: reason,
        apnsId: apnsId,
        apnsUniqueId: apnsUniqueId,
        environment: configuration.environment,
        topic: configuration.topic,
        timestamp: payloadTimestamp,
        hostname: configuration.hostname,
        attributesType: payload.aps.attributesType,
        event: payload.aps.event.rawValue
      )

      if httpResponse.statusCode == 200 {
        completion(.success(apnsResponse))
      } else {
        completion(.failure(APNsError.requestFailed(response: apnsResponse)))
      }
    }.resume()
  }

  func sendNativePush(
    payload: NativePushPayload,
    token: String,
    pushType: NativePushType,
    priority: Int,
    configuration: APNsConfiguration,
    completion: @escaping (Result<APNsResponse, Error>) -> Void
  ) {
    guard configuration.isValid else {
      completion(.failure(APNsError.invalidConfiguration))
      return
    }

    guard let p8Contents = configuration.p8Contents, !p8Contents.isEmpty else {
      completion(.failure(APNsError.cannotReadP8File))
      return
    }

    let jwt: String
    do {
      jwt = try getOrRefreshToken(
        teamId: configuration.teamId,
        keyId: configuration.keyId,
        p8Contents: p8Contents
      )
    } catch {
      completion(.failure(error))
      return
    }

    let cleanToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
    let urlString = "https://\(configuration.hostname)/3/device/\(cleanToken)"
    guard let url = URL(string: urlString) else {
      completion(.failure(APNsError.networkError("Invalid URL")))
      return
    }

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization")
    request.setValue(configuration.topic, forHTTPHeaderField: "apns-topic")
    request.setValue(pushType.rawValue, forHTTPHeaderField: "apns-push-type")
    request.setValue(String(priority), forHTTPHeaderField: "apns-priority")
    request.setValue("application/json", forHTTPHeaderField: "content-type")

    let timestamp = Int(Date().timeIntervalSince1970)

    do {
      let encoder = JSONEncoder()
      request.httpBody = try encoder.encode(payload)
    } catch {
      completion(.failure(error))
      return
    }

    URLSession.shared.dataTask(with: request) { data, response, error in
      if let error = error {
        completion(.failure(APNsError.networkError(error.localizedDescription)))
        return
      }

      guard let httpResponse = response as? HTTPURLResponse else {
        completion(.failure(APNsError.networkError("Invalid response")))
        return
      }

      let apnsId = httpResponse.value(forHTTPHeaderField: "apns-id")
      let apnsUniqueId = httpResponse.value(forHTTPHeaderField: "apns-unique-id")

      var reason: String?
      if let data = data,
         let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
         let r = json["reason"] as? String {
        reason = r
      }

      let apnsResponse = APNsResponse(
        statusCode: httpResponse.statusCode,
        reason: reason,
        apnsId: apnsId,
        apnsUniqueId: apnsUniqueId,
        environment: configuration.environment,
        topic: configuration.topic,
        timestamp: timestamp,
        hostname: configuration.hostname,
        attributesType: nil,
        event: pushType.rawValue
      )

      if httpResponse.statusCode == 200 {
        completion(.success(apnsResponse))
      } else {
        completion(.failure(APNsError.requestFailed(response: apnsResponse)))
      }
    }.resume()
  }

  private func getOrRefreshToken(teamId: String, keyId: String, p8Contents: String) throws -> String {
    if let cached = cachedToken,
       let generatedAt = tokenGeneratedAt,
       Date().timeIntervalSince(generatedAt) < tokenLifetime {
      return cached
    }

    let token = try JWTSigner.generateToken(teamId: teamId, keyId: keyId, p8Contents: p8Contents)
    cachedToken = token
    tokenGeneratedAt = Date()
    return token
  }

  func invalidateToken() {
    cachedToken = nil
    tokenGeneratedAt = nil
  }
}


================================================
FILE: QuickPush/Controllers/FCMService.swift
================================================
//
//  FCMService.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation
import Security

/// Full diagnostic info returned after every FCM request.
struct FCMResponse {
  let statusCode: Int
  let messageId: String?
  let errorCode: String?
  let errorMessage: String?
  let projectId: String
  let token: String
  let timestamp: Int

  var isSuccess: Bool { statusCode == 200 }

  var summary: String {
    if isSuccess {
      return "200 OK"
    } else {
      return "\(statusCode): \(errorCode ?? errorMessage ?? "Unknown")"
    }
  }

  var diagnosticDetails: String {
    var lines: [String] = []
    lines.append("Status:      \(statusCode) \(isSuccess ? "OK" : "Error")")
    if let messageId { lines.append("Message ID:  \(messageId)") }
    if let errorCode { lines.append("Error Code:  \(errorCode)") }
    if let errorMessage { lines.append("Error Msg:   \(errorMessage)") }
    lines.append("Project ID:  \(projectId)")
    lines.append("Token:       \(token.prefix(20))…")
    lines.append("Timestamp:   \(timestamp) (Unix seconds)")
    return lines.joined(separator: "\n")
  }
}

class FCMService {
  static let shared = FCMService()

  private var cachedAccessToken: String?
  private var tokenExpiresAt: Date?
  private let tokenLifetime: TimeInterval = 55 * 60 // 55 min (Google tokens valid 60 min)

  enum FCMError: Error, LocalizedError {
    case invalidConfiguration
    case cannotReadServiceAccount
    case invalidPrivateKey
    case signingFailed
    case oauthFailed(String)
    case requestFailed(response: FCMResponse)
    case networkError(String)

    var errorDescription: String? {
      switch self {
      case .invalidConfiguration:
        return "FCM configuration is incomplete. Please fill in all fields."
      case .cannotReadServiceAccount:
        return "Cannot read service account JSON. Please re-select the file."
      case .invalidPrivateKey:
        return "Invalid private key in service account JSON."
      case .signingFailed:
        return "Failed to sign OAuth JWT."
      case .oauthFailed(let msg):
        return "OAuth token exchange failed: \(msg)"
      case .requestFailed(let response):
        return "FCM error: \(response.summary)"
      case .networkError(let message):
        return "Network error: \(message)"
      }
    }
  }

  // MARK: - Send

  func send(
    message: FCMMessage,
    configuration: FCMConfiguration,
    completion: @escaping (Result<FCMResponse, Error>) -> Void
  ) {
    guard configuration.isValid else {
      completion(.failure(FCMError.invalidConfiguration))
      return
    }
    guard let serviceAccountContents = configuration.serviceAccountContents,
          !serviceAccountContents.isEmpty else {
      completion(.failure(FCMError.cannotReadServiceAccount))
      return
    }

    getOrRefreshAccessToken(serviceAccountContents: serviceAccountContents, clientEmail: configuration.clientEmail) { result in
      switch result {
      case .failure(let error):
        completion(.failure(error))
      case .success(let accessToken):
        self.sendWithToken(
          accessToken: accessToken,
          message: message,
          projectId: configuration.projectId,
          completion: completion
        )
      }
    }
  }

  private func sendWithToken(
    accessToken: String,
    message: FCMMessage,
    projectId: String,
    completion: @escaping (Result<FCMResponse, Error>) -> Void
  ) {
    let urlString = "https://fcm.googleapis.com/v1/projects/\(projectId)/messages:send"
    guard let url = URL(string: urlString) else {
      completion(.failure(FCMError.networkError("Invalid FCM URL")))
      return
    }

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let body = FCMRequestBody(message: message)
    let encoder = JSONEncoder()
    do {
      request.httpBody = try encoder.encode(body)
    } catch {
      completion(.failure(error))
      return
    }

    let timestamp = Int(Date().timeIntervalSince1970)

    URLSession.shared.dataTask(with: request) { data, response, error in
      if let error = error {
        completion(.failure(FCMError.networkError(error.localizedDescription)))
        return
      }
      guard let httpResponse = response as? HTTPURLResponse else {
        completion(.failure(FCMError.networkError("Invalid response")))
        return
      }

      var messageId: String?
      var errorCode: String?
      var errorMessage: String?

      if let data = data,
         let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
        messageId = json["name"] as? String
        if let errorObj = json["error"] as? [String: Any] {
          errorCode = errorObj["status"] as? String
          errorMessage = errorObj["message"] as? String
          // Also check nested details for FCM-specific error codes
          if errorCode == nil,
             let details = errorObj["details"] as? [[String: Any]] {
            for detail in details {
              if let code = detail["errorCode"] as? String {
                errorCode = code
                break
              }
            }
          }
        }
      }

      let fcmResponse = FCMResponse(
        statusCode: httpResponse.statusCode,
        messageId: messageId,
        errorCode: errorCode,
        errorMessage: errorMessage,
        projectId: projectId,
        token: message.token,
        timestamp: timestamp
      )

      if httpResponse.statusCode == 200 {
        completion(.success(fcmResponse))
      } else {
        completion(.failure(FCMError.requestFailed(response: fcmResponse)))
      }
    }.resume()
  }

  // MARK: - OAuth Token

  private func getOrRefreshAccessToken(
    serviceAccountContents: String,
    clientEmail: String,
    completion: @escaping (Result<String, Error>) -> Void
  ) {
    if let token = cachedAccessToken,
       let expiresAt = tokenExpiresAt,
       Date() < expiresAt {
      completion(.success(token))
      return
    }

    do {
      let jwt = try buildOAuthJWT(serviceAccountContents: serviceAccountContents, clientEmail: clientEmail)
      exchangeJWTForAccessToken(jwt: jwt, completion: { result in
        switch result {
        case .success(let token):
          self.cachedAccessToken = token
          self.tokenExpiresAt = Date().addingTimeInterval(self.tokenLifetime)
          completion(.success(token))
        case .failure(let error):
          completion(.failure(error))
        }
      })
    } catch {
      completion(.failure(error))
    }
  }

  private func exchangeJWTForAccessToken(
    jwt: String,
    completion: @escaping (Result<String, Error>) -> Void
  ) {
    guard let url = URL(string: "https://oauth2.googleapis.com/token") else {
      completion(.failure(FCMError.networkError("Invalid OAuth URL")))
      return
    }

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

    let body = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=\(jwt)"
    request.httpBody = body.data(using: .utf8)

    URLSession.shared.dataTask(with: request) { data, response, error in
      if let error = error {
        completion(.failure(FCMError.oauthFailed(error.localizedDescription)))
        return
      }
      guard let data = data,
            let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
        completion(.failure(FCMError.oauthFailed("Invalid response")))
        return
      }
      if let token = json["access_token"] as? String {
        completion(.success(token))
      } else {
        let errDesc = (json["error_description"] as? String) ?? (json["error"] as? String) ?? "Unknown OAuth error"
        completion(.failure(FCMError.oauthFailed(errDesc)))
      }
    }.resume()
  }

  // MARK: - RS256 JWT for OAuth

  private func buildOAuthJWT(serviceAccountContents: String, clientEmail: String) throws -> String {
    guard let privateKeyPEM = FCMFileManager.parsePrivateKey(from: serviceAccountContents) else {
      throw FCMError.cannotReadServiceAccount
    }

    let privateKey = try loadRSAPrivateKey(pem: privateKeyPEM)

    let header = ["alg": "RS256", "typ": "JWT"]
    let now = Int(Date().timeIntervalSince1970)
    let claims: [String: Any] = [
      "iss": clientEmail,
      "sub": clientEmail,
      "aud": "https://oauth2.googleapis.com/token",
      "scope": "https://www.googleapis.com/auth/firebase.messaging",
      "iat": now,
      "exp": now + 3600
    ]

    let headerData = try JSONSerialization.data(withJSONObject: header)
    let claimsData = try JSONSerialization.data(withJSONObject: claims)

    let headerB64 = base64urlEncode(headerData)
    let claimsB64 = base64urlEncode(claimsData)
    let signingInput = "\(headerB64).\(claimsB64)"

    guard let signingData = signingInput.data(using: .utf8) else {
      throw FCMError.signingFailed
    }

    var error: Unmanaged<CFError>?
    guard let signatureData = SecKeyCreateSignature(
      privateKey,
      .rsaSignatureMessagePKCS1v15SHA256,
      signingData as CFData,
      &error
    ) as Data? else {
      throw FCMError.signingFailed
    }

    let signatureB64 = base64urlEncode(signatureData)
    return "\(signingInput).\(signatureB64)"
  }

  // MARK: - RSA Key Loading (PKCS#8 → PKCS#1)

  private func loadRSAPrivateKey(pem: String) throws -> SecKey {
    // Strip PEM headers and decode base64
    let lines = pem.components(separatedBy: "\n")
    let keyLines = lines.filter { !$0.hasPrefix("-----") && !$0.isEmpty }
    let base64Key = keyLines.joined()
    guard let pkcs8DER = Data(base64Encoded: base64Key) else {
      throw FCMError.invalidPrivateKey
    }

    // Google service accounts use PKCS#8 format. SecKey requires PKCS#1.
    // We must strip the PKCS#8 wrapper to get the inner PKCS#1 RSAPrivateKey.
    let pkcs1DER = try extractPKCS1FromPKCS8(pkcs8DER)

    let attributes: [String: Any] = [
      kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
      kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
      kSecAttrKeySizeInBits as String: 2048
    ]

    var error: Unmanaged<CFError>?
    guard let secKey = SecKeyCreateWithData(pkcs1DER as CFData, attributes as CFDictionary, &error) else {
      throw FCMError.invalidPrivateKey
    }
    return secKey
  }

  /// Strips the PKCS#8 AlgorithmIdentifier wrapper to extract the inner PKCS#1 RSAPrivateKey bytes.
  ///
  /// PKCS#8 DER layout:
  ///   SEQUENCE {
  ///     INTEGER (version = 0)
  ///     SEQUENCE { OID(rsaEncryption), NULL }   ← algorithm identifier
  ///     OCTET STRING { <PKCS#1 RSAPrivateKey> }
  ///   }
  private func extractPKCS1FromPKCS8(_ der: Data) throws -> Data {
    let bytes = Array(der)
    var index = 0

    // Helper to skip a DER length field and return the content length.
    func readLength() throws -> Int {
      guard index < bytes.count else { throw FCMError.invalidPrivateKey }
      if bytes[index] & 0x80 == 0 {
        let len = Int(bytes[index]); index += 1; return len
      }
      let numBytes = Int(bytes[index] & 0x7F); index += 1
      guard index + numBytes <= bytes.count else { throw FCMError.invalidPrivateKey }
      var len = 0
      for _ in 0..<numBytes { len = (len << 8) | Int(bytes[index]); index += 1 }
      return len
    }

    // Outer SEQUENCE
    guard index < bytes.count, bytes[index] == 0x30 else { throw FCMError.invalidPrivateKey }
    index += 1
    _ = try readLength() // skip outer SEQUENCE length

    // version INTEGER (0)
    guard index < bytes.count, bytes[index] == 0x02 else { throw FCMError.invalidPrivateKey }
    index += 1
    let versionLen = try readLength()
    index += versionLen

    // AlgorithmIdentifier SEQUENCE — skip it
    guard index < bytes.count, bytes[index] == 0x30 else { throw FCMError.invalidPrivateKey }
    index += 1
    let algoLen = try readLength()
    index += algoLen

    // OCTET STRING containing the PKCS#1 key
    guard index < bytes.count, bytes[index] == 0x04 else { throw FCMError.invalidPrivateKey }
    index += 1
    let octetLen = try readLength()

    guard index + octetLen <= bytes.count else { throw FCMError.invalidPrivateKey }
    return Data(bytes[index..<(index + octetLen)])
  }

  // MARK: - Helpers

  private func base64urlEncode(_ data: Data) -> String {
    data.base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }

  func invalidateToken() {
    cachedAccessToken = nil
    tokenExpiresAt = nil
  }
}


================================================
FILE: QuickPush/Controllers/JWTSigner.swift
================================================
//
//  JWTSigner.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import Foundation
import CryptoKit

class JWTSigner {
  enum JWTError: Error, LocalizedError {
    case invalidP8File
    case signingFailed
    case invalidKeyData

    var errorDescription: String? {
      switch self {
      case .invalidP8File: return "Invalid .p8 key file"
      case .signingFailed: return "Failed to sign JWT token"
      case .invalidKeyData: return "Invalid key data in .p8 file"
      }
    }
  }

  static func generateToken(teamId: String, keyId: String, p8Contents: String) throws -> String {
    let privateKey = try parseP8Key(p8Contents)

    let header = [
      "alg": "ES256",
      "kid": keyId,
      "typ": "JWT"
    ]

    let now = Int(Date().timeIntervalSince1970)
    let claims: [String: Any] = [
      "iss": teamId,
      "iat": now
    ]

    let headerData = try JSONSerialization.data(withJSONObject: header)
    let claimsData = try JSONSerialization.data(withJSONObject: claims)

    let headerBase64 = base64urlEncode(headerData)
    let claimsBase64 = base64urlEncode(claimsData)

    let signingInput = "\(headerBase64).\(claimsBase64)"
    guard let signingData = signingInput.data(using: .utf8) else {
      throw JWTError.signingFailed
    }

    let signature = try privateKey.signature(for: signingData)
    let signatureBase64 = base64urlEncode(signature.rawRepresentation)

    return "\(signingInput).\(signatureBase64)"
  }

  private static func parseP8Key(_ p8Contents: String) throws -> P256.Signing.PrivateKey {
    let lines = p8Contents.components(separatedBy: "\n")
    let keyLines = lines.filter { line in
      !line.hasPrefix("-----") && !line.isEmpty
    }
    let base64Key = keyLines.joined()

    guard let keyData = Data(base64Encoded: base64Key) else {
      throw JWTError.invalidKeyData
    }

    do {
      return try P256.Signing.PrivateKey(derRepresentation: keyData)
    } catch {
      throw JWTError.invalidP8File
    }
  }

  private static func base64urlEncode(_ data: Data) -> String {
    data.base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}


================================================
FILE: QuickPush/Controllers/PushNotificationService.swift
================================================
//
//  PushNotificationService.swift
//  QuickPush
//
//  Created by beto on 2/20/25.
//

import Foundation

class PushNotificationService {
  static let shared = PushNotificationService() // Singleton instance

  private let expoPushEndpoint = "https://exp.host/--/api/v2/push/send"
  private let expoReceiptsEndpoint = "https://exp.host/--/api/v2/push/getReceipts"

  /// Result containing the decoded response, HTTP status code, and raw JSON string.
  struct SendResult {
    let response: PushResponse
    let httpStatusCode: Int?
    let rawJSON: String?
  }

  struct ReceiptResult {
    let response: ReceiptResponse
    let httpStatusCode: Int?
    let rawJSON: String?
  }

  func sendPushNotification(notification: PushNotification, accessToken: String? = nil, completion: @escaping (Result<SendResult, Error>) -> Void) {
    guard let url = URL(string: expoPushEndpoint) else {
      completion(.failure(APIError.invalidURL))
      return
    }

    // Prepare request
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("exp.host", forHTTPHeaderField: "host")
    request.setValue("application/json", forHTTPHeaderField: "accept")
    request.setValue("gzip, deflate", forHTTPHeaderField: "accept-encoding")
    request.setValue("application/json", forHTTPHeaderField: "content-type")

    // Add authorization header if access token is provided
    if let accessToken = accessToken, !accessToken.isEmpty {
      request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
    }

    do {
      let jsonData = try JSONEncoder().encode(notification)
      request.httpBody = jsonData
    } catch {
      completion(.failure(APIError.encodingFailed))
      return
    }

    // Perform network request
    URLSession.shared.dataTask(with: request) { data, response, error in
      // Handle network errors
      if let error = error {
        completion(.failure(error))
        return
      }

      let httpStatusCode = (response as? HTTPURLResponse)?.statusCode

      // Ensure we have valid data
      guard let data = data else {
        completion(.failure(APIError.noData))
        return
      }

      let rawJSON = Self.prettyPrint(data)

      // Decode API response
      do {
        let responseObject = try JSONDecoder().decode(PushResponse.self, from: data)

        // UNAUTHORIZED REQUESTS CHECK - Possibly no Access Token
        if let errors = responseObject.errors,
           errors.contains(where: { $0.code == "UNAUTHORIZED" }) {
          completion(.failure(APIError.insufficientPermissions))
          return
        }

        completion(.success(SendResult(response: responseObject, httpStatusCode: httpStatusCode, rawJSON: rawJSON)))
      } catch {
        completion(.failure(APIError.decodingFailed))
      }
    }.resume()
  }

  func checkReceipts(
    ids: [String],
    accessToken: String? = nil,
    completion: @escaping (Result<ReceiptResult, Error>) -> Void
  ) {
    guard let url = URL(string: expoReceiptsEndpoint) else {
      completion(.failure(APIError.invalidURL))
      return
    }

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("exp.host", forHTTPHeaderField: "host")
    request.setValue("application/json", forHTTPHeaderField: "accept")
    request.setValue("gzip, deflate", forHTTPHeaderField: "accept-encoding")
    request.setValue("application/json", forHTTPHeaderField: "content-type")

    if let accessToken = accessToken, !accessToken.isEmpty {
      request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
    }

    do {
      let body = ["ids": ids]
      request.httpBody = try JSONSerialization.data(withJSONObject: body)
    } catch {
      completion(.failure(APIError.encodingFailed))
      return
    }

    URLSession.shared.dataTask(with: request) { data, response, error in
      if let error = error {
        completion(.failure(error))
        return
      }

      let httpStatusCode = (response as? HTTPURLResponse)?.statusCode

      guard let data = data else {
        completion(.failure(APIError.noData))
        return
      }

      let rawJSON = Self.prettyPrint(data)

      do {
        let responseObject = try JSONDecoder().decode(ReceiptResponse.self, from: data)
        completion(.success(ReceiptResult(response: responseObject, httpStatusCode: httpStatusCode, rawJSON: rawJSON)))
      } catch {
        completion(.failure(APIError.decodingFailed))
      }
    }.resume()
  }

  /// Pretty-print raw JSON data for display.
  private static func prettyPrint(_ data: Data) -> String? {
    guard let json = try? JSONSerialization.jsonObject(with: data),
          let pretty = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) else {
      return String(data: data, encoding: .utf8)
    }
    return String(data: pretty, encoding: .utf8)
  }
}

// MARK: - API Error Enum
enum APIError: Error, LocalizedError {
  case invalidURL
  case encodingFailed
  case noData
  case decodingFailed
  case insufficientPermissions
  
  var errorDescription: String? {
    switch self {
    case .invalidURL: return "Invalid API URL"
    case .encodingFailed: return "Failed to encode request data"
    case .noData: return "No response data received"
    case .decodingFailed: return "Failed to decode API response"
    case .insufficientPermissions: return "Insufficient permissions. Push security may be enabled for this app - please provide a valid access token above."
    }
  }
}


================================================
FILE: QuickPush/Models/APNsConfigStore.swift
================================================
//
//  APNsConfigStore.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation

/// Shared APNs credentials store used by both the Live Activity and APNs tabs.
@Observable
class APNsConfigStore {
  static let shared = APNsConfigStore()

  var teamId: String = "" {
    didSet { UserDefaults.standard.set(teamId, forKey: "apns_teamId") }
  }
  var keyId: String = "" {
    didSet { UserDefaults.standard.set(keyId, forKey: "apns_keyId") }
  }
  var bundleId: String = "" {
    didSet { UserDefaults.standard.set(bundleId, forKey: "apns_bundleId") }
  }
  var p8FileName: String?
  var hasP8Key: Bool = false
  var environment: APNsEnvironment = .sandbox {
    didSet { UserDefaults.standard.set(environment.rawValue, forKey: "apns_environment") }
  }

  init() {
    teamId = UserDefaults.standard.string(forKey: "apns_teamId") ?? ""
    keyId = UserDefaults.standard.string(forKey: "apns_keyId") ?? ""
    bundleId = UserDefaults.standard.string(forKey: "apns_bundleId") ?? ""
    p8FileName = SecurityBookmarkManager.shared.storedP8Filename()
    hasP8Key = SecurityBookmarkManager.shared.storedP8Contents() != nil
    if let envString = UserDefaults.standard.string(forKey: "apns_environment"),
       let env = APNsEnvironment(rawValue: envString) {
      environment = env
    }
  }

  func selectP8File() {
    SecurityBookmarkManager.shared.selectP8File { [weak self] contents, filename in
      DispatchQueue.main.async {
        guard let self else { return }
        self.p8FileName = filename
        self.hasP8Key = contents != nil
      }
    }
  }

  /// Build an `APNsConfiguration` with an optional topic suffix appended to bundleId.
  /// - Parameter topicSuffix: e.g. `".push-type.liveactivity"` for Live Activity, `nil` for plain bundle ID.
  func apnsConfiguration(topicSuffix: String?) -> APNsConfiguration {
    let topicOverride: String? = topicSuffix.map { "\(bundleId)\($0)" }
    return APNsConfiguration(
      teamId: teamId,
      keyId: keyId,
      bundleId: bundleId,
      p8Contents: SecurityBookmarkManager.shared.storedP8Contents(),
      environment: environment,
      topicOverride: topicOverride
    )
  }
}


================================================
FILE: QuickPush/Models/APNsConfiguration.swift
================================================
//
//  APNsConfiguration.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import Foundation

struct APNsConfiguration {
  var teamId: String = ""
  var keyId: String = ""
  var bundleId: String = ""
  var p8Contents: String?
  var environment: APNsEnvironment = .sandbox
  var topicOverride: String? = nil

  var isValid: Bool {
    !teamId.isEmpty && !keyId.isEmpty && !bundleId.isEmpty &&
    p8Contents != nil && !p8Contents!.isEmpty
  }

  var hostname: String {
    environment.hostname
  }

  var topic: String {
    topicOverride ?? bundleId
  }
}

enum APNsEnvironment: String, CaseIterable {
  case sandbox
  case production

  var hostname: String {
    switch self {
    case .sandbox: return "api.sandbox.push.apple.com"
    case .production: return "api.push.apple.com"
    }
  }
}


================================================
FILE: QuickPush/Models/FCMConfigStore.swift
================================================
//
//  FCMConfigStore.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation

/// Shared FCM credentials store used by the FCM tab.
@Observable
class FCMConfigStore {
  static let shared = FCMConfigStore()

  var projectId: String = "" {
    didSet { UserDefaults.standard.set(projectId, forKey: "fcm_projectId") }
  }
  var clientEmail: String = "" {
    didSet { UserDefaults.standard.set(clientEmail, forKey: "fcm_clientEmail") }
  }
  var serviceAccountFilename: String?
  var hasServiceAccount: Bool = false
  var fileError: String?

  init() {
    projectId = UserDefaults.standard.string(forKey: "fcm_projectId") ?? ""
    clientEmail = UserDefaults.standard.string(forKey: "fcm_clientEmail") ?? ""
    serviceAccountFilename = FCMFileManager.shared.storedFilename()
    hasServiceAccount = FCMFileManager.shared.storedContents() != nil
  }

  func selectServiceAccountFile() {
    FCMFileManager.shared.selectServiceAccountFile { [weak self] result in
      DispatchQueue.main.async {
        guard let self else { return }
        switch result {
        case .success(let info):
          self.fileError = nil
          self.serviceAccountFilename = info.filename
          self.hasServiceAccount = true
          if let projectId = info.projectId, !projectId.isEmpty {
            self.projectId = projectId
          }
          if let clientEmail = info.clientEmail, !clientEmail.isEmpty {
            self.clientEmail = clientEmail
          }
        case .failure(let error):
          self.fileError = error.localizedDescription
        }
      }
    }
  }

  func fcmConfiguration() -> FCMConfiguration {
    FCMConfiguration(
      projectId: projectId,
      clientEmail: clientEmail,
      serviceAccountContents: FCMFileManager.shared.storedContents()
    )
  }
}


================================================
FILE: QuickPush/Models/FCMConfiguration.swift
================================================
//
//  FCMConfiguration.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation

struct FCMConfiguration {
  var projectId: String = ""
  var clientEmail: String = ""
  var serviceAccountContents: String?

  var isValid: Bool {
    !projectId.isEmpty && !clientEmail.isEmpty &&
    serviceAccountContents != nil && !serviceAccountContents!.isEmpty
  }
}

enum FCMMessageType: String, CaseIterable {
  case notification = "Notification"
  case data = "Data"
}


================================================
FILE: QuickPush/Models/FCMPayload.swift
================================================
//
//  FCMPayload.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation

struct FCMMessage: Encodable {
  var token: String
  var notification: FCMNotification?
  var android: FCMAndroidConfig?
  var data: [String: String]?

  enum CodingKeys: String, CodingKey {
    case token, notification, android, data
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(token, forKey: .token)
    try container.encodeIfPresent(notification, forKey: .notification)
    try container.encodeIfPresent(android, forKey: .android)
    if let data, !data.isEmpty {
      try container.encode(data, forKey: .data)
    }
  }
}

struct FCMNotification: Encodable {
  var title: String?
  var body: String?
  var image: String?

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encodeIfPresent(title, forKey: .title)
    try container.encodeIfPresent(body, forKey: .body)
    try container.encodeIfPresent(image, forKey: .image)
  }

  enum CodingKeys: String, CodingKey {
    case title, body, image
  }
}

struct FCMAndroidConfig: Encodable {
  var priority: String = "HIGH"
  var notification: FCMAndroidNotification?

  enum CodingKeys: String, CodingKey {
    case priority, notification
  }
}

struct FCMAndroidNotification: Encodable {
  var channelId: String?
  var sound: String?
  var color: String?

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encodeIfPresent(channelId, forKey: .channelId)
    try container.encodeIfPresent(sound, forKey: .sound)
    try container.encodeIfPresent(color, forKey: .color)
  }

  enum CodingKeys: String, CodingKey {
    case channelId = "channel_id"
    case sound
    case color
  }
}

struct FCMRequestBody: Encodable {
  var message: FCMMessage
}


================================================
FILE: QuickPush/Models/LiveActivityPayload.swift
================================================
//
//  LiveActivityPayload.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import Foundation

struct LiveActivityAPNsPayload: Codable {
  let aps: LiveActivityAPS

  enum CodingKeys: String, CodingKey {
    case aps
  }
}

struct LiveActivityAPS: Codable {
  let timestamp: Int
  let event: LiveActivityEvent
  let contentState: LiveActivityContentState
  let attributesType: String?
  let attributes: LiveActivityAttributes?
  let alert: LiveActivityAlert?
  let dismissalDate: Int?
  let relevanceScore: Double?
  let staleDate: Int?

  enum CodingKeys: String, CodingKey {
    case timestamp, event
    case contentState = "content-state"
    case attributesType = "attributes-type"
    case attributes, alert
    case dismissalDate = "dismissal-date"
    case relevanceScore = "relevance-score"
    case staleDate = "stale-date"
  }
}

enum LiveActivityEvent: String, Codable, CaseIterable {
  case start
  case update
  case end
}

// Matches ExpoLiveActivityAttributes.ContentState exactly
struct LiveActivityContentState: Codable {
  var title: String
  var subtitle: String?
  var timerEndDateInMilliseconds: Double?
  var progress: Double?
  var imageName: String?
  var dynamicIslandImageName: String?
}

// Matches ExpoLiveActivityAttributes exactly
struct LiveActivityAttributes: Codable {
  var name: String
  var backgroundColor: String?
  var titleColor: String?
  var subtitleColor: String?
  var progressViewTint: String?
  var progressViewLabelColor: String?
  var deepLinkUrl: String?
  var timerType: String?
  var padding: Int?
  var paddingDetails: LiveActivityPaddingDetails?
  var imagePosition: String?
  var imageWidth: Int?
  var imageHeight: Int?
}

struct LiveActivityPaddingDetails: Codable {
  var top: Int?
  var bottom: Int?
  var left: Int?
  var right: Int?
  var vertical: Int?
  var horizontal: Int?
}

struct LiveActivityAlert: Codable {
  var title: String?
  var body: String?
  var sound: String?
}


================================================
FILE: QuickPush/Models/NativePushPayload.swift
================================================
//
//  NativePushPayload.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation

enum NativePushType: String, CaseIterable {
  case alert
  case background
}

enum NativeInterruptionLevel: String, CaseIterable {
  case active
  case passive
  case timeSensitive = "time-sensitive"
  case critical

  var displayName: String {
    switch self {
    case .active: return "Active"
    case .passive: return "Passive"
    case .timeSensitive: return "Time Sensitive"
    case .critical: return "Critical"
    }
  }
}

struct NativeAlert: Codable {
  var title: String?
  var subtitle: String?
  var body: String?
}

struct NativeAPS: Encodable {
  var alert: NativeAlert?
  var badge: Int?
  var sound: String?
  var contentAvailable: Int?
  var mutableContent: Int?
  var threadId: String?
  var category: String?
  var interruptionLevel: String?

  enum CodingKeys: String, CodingKey {
    case alert
    case badge
    case sound
    case contentAvailable = "content-available"
    case mutableContent = "mutable-content"
    case threadId = "thread-id"
    case category
    case interruptionLevel = "interruption-level"
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encodeIfPresent(alert, forKey: .alert)
    try container.encodeIfPresent(badge, forKey: .badge)
    try container.encodeIfPresent(sound, forKey: .sound)
    try container.encodeIfPresent(contentAvailable, forKey: .contentAvailable)
    try container.encodeIfPresent(mutableContent, forKey: .mutableContent)
    try container.encodeIfPresent(threadId, forKey: .threadId)
    try container.encodeIfPresent(category, forKey: .category)
    try container.encodeIfPresent(interruptionLevel, forKey: .interruptionLevel)
  }
}

private struct RichBody: Encodable {
  struct RichContent: Encodable { let image: String }
  let _richContent: RichContent
  enum CodingKeys: String, CodingKey { case _richContent = "_richContent" }
}

struct NativePushPayload: Encodable {
  var aps: NativeAPS
  var customData: [String: String]
  var imageUrl: String?

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: DynamicCodingKey.self)
    try container.encode(aps, forKey: DynamicCodingKey(stringValue: "aps"))
    if let imageUrl, !imageUrl.isEmpty {
      let richBody = RichBody(_richContent: .init(image: imageUrl))
      try container.encode(richBody, forKey: DynamicCodingKey(stringValue: "body"))
    }
    for (key, value) in customData {
      try container.encode(value, forKey: DynamicCodingKey(stringValue: key))
    }
  }
}

private struct DynamicCodingKey: CodingKey {
  var stringValue: String
  init(stringValue: String) { self.stringValue = stringValue }
  var intValue: Int? { nil }
  init?(intValue: Int) { return nil }
}


================================================
FILE: QuickPush/Models/PushNotification.swift
================================================
//
//  PushNotification.swift
//  QuickPush
//
//  Created by beto on 2/20/25.
//

import Foundation

struct PushNotification: Codable {
  let to: [String]  // Supports both single and multiple recipients
  let title: String
  let body: String
  let data: [String: String]?
  let ttl: Int?
  let expiration: Int?
  let priority: Priority?
  let subtitle: String?
  let sound: String?
  let badge: Int?
  let interruptionLevel: InterruptionLevel?
  let channelId: String?
  let categoryId: String?
  let mutableContent: Bool?
  let contentAvailable: Bool?
  let richContent: RichContent?

  enum Priority: String, Codable {
    case `default`, normal, high
  }

  enum InterruptionLevel: String, Codable {
    case active, critical, passive, timeSensitive = "time-sensitive"
  }

  struct RichContent: Codable {
    let image: String
  }
  
  enum CodingKeys: String, CodingKey {
    case to, title, body, data, ttl, expiration, priority, subtitle, sound, badge
    case interruptionLevel = "interruptionLevel"
    case channelId = "channelId"
    case categoryId = "categoryId"
    case mutableContent = "mutableContent"
    case contentAvailable = "_contentAvailable"
    case richContent
  }
  
  init(
    to: [String],
    title: String,
    body: String,
    data: [String: String]? = nil,
    ttl: Int? = nil,
    expiration: Int? = nil,
    priority: Priority? = .default,
    subtitle: String? = nil,
    sound: String? = "default",
    badge: Int? = nil,
    interruptionLevel: InterruptionLevel? = nil,
    channelId: String? = nil,
    categoryId: String? = nil,
    mutableContent: Bool? = false,
    contentAvailable: Bool? = nil,
    richContent: RichContent? = nil
  ) {
    self.to = to
    self.title = title
    self.body = body
    self.data = data
    self.ttl = ttl
    self.expiration = expiration
    self.priority = priority
    self.subtitle = subtitle
    self.sound = sound
    self.badge = badge
    self.interruptionLevel = interruptionLevel
    self.channelId = channelId
    self.categoryId = categoryId
    self.mutableContent = mutableContent
    self.contentAvailable = contentAvailable
    self.richContent = richContent
  }
}


================================================
FILE: QuickPush/Models/PushResponse.swift
================================================
//
//  PushResponse.swift
//  QuickPush
//
//  Created by beto on 2/20/25.
//

import Foundation

struct PushResponse: Codable {
  let data: [PushTicket]?
  let errors: [PushError]?
  
  struct PushTicket: Codable {
    let status: String
    let id: String?  // Present only if status == "ok"
    let message: String?  // Present only if status == "error"
    let details: [String: String]?  // JSON details object, can vary
  }
  
  struct PushError: Codable {
    let code: String
    let message: String
    let type: String?
    let isTransient: Bool?
    let metadata: ErrorMetadata?
    let requestId: String?
    
    struct ErrorMetadata: Codable {
      let appId: String?
      let experienceId: String?
    }
  }
}


================================================
FILE: QuickPush/Models/ReceiptResponse.swift
================================================
//
//  ReceiptResponse.swift
//  QuickPush
//
//  Created by beto on 2/20/26.
//

import Foundation

struct ReceiptResponse: Codable {
  let data: [String: PushReceipt]?
  let errors: [PushResponse.PushError]?

  struct PushReceipt: Codable {
    let status: String          // "ok" or "error"
    let message: String?
    let details: ReceiptDetails?

    struct ReceiptDetails: Codable {
      let error: String?        // e.g. "DeviceNotRegistered"
    }
  }
}


================================================
FILE: QuickPush/Models/SavedToken.swift
================================================
//
//  SavedToken.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation

struct SavedToken: Codable, Identifiable, Equatable {
  let id: UUID
  var label: String
  var token: String
  var isEnabled: Bool

  init(id: UUID = UUID(), label: String, token: String, isEnabled: Bool = true) {
    self.id = id
    self.label = label
    self.token = token
    self.isEnabled = isEnabled
  }
}


================================================
FILE: QuickPush/Models/TokenToSave.swift
================================================
//
//  TokenToSave.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation

/// Identifiable wrapper for the `.sheet(item:)` token-save presentation.
struct TokenToSave: Identifiable {
  let id = UUID()
  let index: Int
  let token: String
}


================================================
FILE: QuickPush/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: QuickPush/QuickPush.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.files.user-selected.read-only</key>
	<true/>
	<key>com.apple.security.network.client</key>
	<true/>
	<key>com.apple.security.files.bookmarks.app-scope</key>
	<true/>
</dict>
</plist>


================================================
FILE: QuickPush/QuickPushApp.swift
================================================
//
//  QuickPushApp.swift
//  QuickPush
//
//  Created by beto on 2/20/25.
//

import SwiftUI

@main
struct QuickPushApp: App {
  @State private var windowManager = WindowManager()

  var body: some Scene {
    MenuBarExtra("QuickPush", systemImage: "bolt.brakesignal") {
      MainContentView()
        .environment(windowManager)
        .onAppear {
          // Register the global hotkey after the app has launched.
          windowManager.startMonitoring()

          // If the panel is already pinned, bring it to front
          // instead of showing the popover content.
          if windowManager.isPinned {
            windowManager.bringPanelToFront()
          }
        }
    }
    .menuBarExtraStyle(.window)
  }
}


================================================
FILE: QuickPush/QuickPushIcon.icon/icon.json
================================================
{
  "fill" : "automatic",
  "groups" : [
    {
      "layers" : [
        {
          "fill-specializations" : [
            {
              "appearance" : "dark",
              "value" : {
                "solid" : "extended-gray:1.00000,1.00000"
              }
            }
          ],
          "glass" : true,
          "image-name" : "bolt.brakesignal 1.svg",
          "name" : "bolt.brakesignal 1",
          "position" : {
            "scale" : 6.81,
            "translation-in-points" : [
              0,
              0
            ]
          }
        }
      ],
      "position" : {
        "scale" : 1,
        "translation-in-points" : [
          5,
          0
        ]
      },
      "shadow" : {
        "kind" : "neutral",
        "opacity" : 0.5
      },
      "translucency" : {
        "enabled" : true,
        "value" : 0.5
      }
    }
  ],
  "supported-platforms" : {
    "circles" : [
      "watchOS"
    ],
    "squares" : "shared"
  }
}

================================================
FILE: QuickPush/Utilities/ColorHexConverter.swift
================================================
//
//  ColorHexConverter.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI
import AppKit

extension Color {
  init(hex: String) {
    let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
    var int: UInt64 = 0
    Scanner(string: hex).scanHexInt64(&int)

    let r, g, b, a: Double
    switch hex.count {
    case 6:
      (r, g, b, a) = (
        Double((int >> 16) & 0xFF) / 255,
        Double((int >> 8) & 0xFF) / 255,
        Double(int & 0xFF) / 255,
        1
      )
    case 8:
      (r, g, b, a) = (
        Double((int >> 24) & 0xFF) / 255,
        Double((int >> 16) & 0xFF) / 255,
        Double((int >> 8) & 0xFF) / 255,
        Double(int & 0xFF) / 255
      )
    default:
      (r, g, b, a) = (0, 0, 0, 1)
    }

    self.init(.sRGB, red: r, green: g, blue: b, opacity: a)
  }

  func toHexString() -> String {
    guard let nsColor = NSColor(self).usingColorSpace(.sRGB) else {
      return "000000"
    }
    let r = Int(round(nsColor.redComponent * 255))
    let g = Int(round(nsColor.greenComponent * 255))
    let b = Int(round(nsColor.blueComponent * 255))
    return String(format: "%02X%02X%02X", r, g, b)
  }

  func toHexStringWithAlpha() -> String {
    guard let nsColor = NSColor(self).usingColorSpace(.sRGB) else {
      return "000000FF"
    }
    let r = Int(round(nsColor.redComponent * 255))
    let g = Int(round(nsColor.greenComponent * 255))
    let b = Int(round(nsColor.blueComponent * 255))
    let a = Int(round(nsColor.alphaComponent * 255))
    return String(format: "%02X%02X%02X%02X", r, g, b, a)
  }
}


================================================
FILE: QuickPush/Utilities/FCMFileManager.swift
================================================
//
//  FCMFileManager.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation
import AppKit

/// Manages selection and persistent storage of the Firebase service account JSON.
///
/// We read the file contents once when selected and store them directly in
/// UserDefaults (service account JSON is ~3–5 KB, well within limits).
class FCMFileManager {
  static let shared = FCMFileManager()

  private let contentsKey = "fcm_serviceAccountContents"
  private let filenameKey = "fcm_serviceAccountFilename"
  private let projectIdKey = "fcm_projectId"
  private let clientEmailKey = "fcm_clientEmail"

  // MARK: - File Picker

  enum SelectionError: Error {
    case notServiceAccount
    case unreadable

    var localizedDescription: String {
      switch self {
      case .notServiceAccount:
        return "This looks like a google-services.json (client config), not a service account key. Go to Firebase Console → Project Settings → Service Accounts → Generate new private key to get the correct file."
      case .unreadable:
        return "Could not read the selected file."
      }
    }
  }

  func selectServiceAccountFile(completion: @escaping (Result<(contents: String, filename: String, projectId: String?, clientEmail: String?), SelectionError>) -> Void) {
    let panel = NSOpenPanel()
    panel.title = "Select Firebase Service Account JSON"
    panel.allowedContentTypes = [.json]
    panel.allowsMultipleSelection = false
    panel.canChooseDirectories = false

    panel.begin { response in
      guard response == .OK, let url = panel.url else { return }

      guard let contents = try? String(contentsOf: url, encoding: .utf8) else {
        completion(.failure(.unreadable))
        return
      }

      // Validate this is a service account file, not google-services.json
      guard Self.parsePrivateKey(from: contents) != nil else {
        completion(.failure(.notServiceAccount))
        return
      }

      let filename = url.lastPathComponent
      let (projectId, clientEmail) = Self.parse(serviceAccountContents: contents)

      self.save(contents: contents, filename: filename, projectId: projectId ?? "", clientEmail: clientEmail ?? "")
      completion(.success((contents, filename, projectId, clientEmail)))
    }
  }

  // MARK: - Parsing

  static func parse(serviceAccountContents: String) -> (projectId: String?, clientEmail: String?) {
    guard let data = serviceAccountContents.data(using: .utf8),
          let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
      return (nil, nil)
    }
    let projectId = json["project_id"] as? String
    let clientEmail = json["client_email"] as? String
    return (projectId, clientEmail)
  }

  static func parsePrivateKey(from serviceAccountContents: String) -> String? {
    guard let data = serviceAccountContents.data(using: .utf8),
          let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
      return nil
    }
    return json["private_key"] as? String
  }

  // MARK: - Storage

  func save(contents: String, filename: String, projectId: String, clientEmail: String) {
    UserDefaults.standard.set(contents, forKey: contentsKey)
    UserDefaults.standard.set(filename, forKey: filenameKey)
    UserDefaults.standard.set(projectId, forKey: projectIdKey)
    UserDefaults.standard.set(clientEmail, forKey: clientEmailKey)
  }

  func storedContents() -> String? {
    UserDefaults.standard.string(forKey: contentsKey)
  }

  func storedFilename() -> String? {
    UserDefaults.standard.string(forKey: filenameKey)
  }

  func storedProjectId() -> String? {
    UserDefaults.standard.string(forKey: projectIdKey)
  }

  func storedClientEmail() -> String? {
    UserDefaults.standard.string(forKey: clientEmailKey)
  }

  func clearStored() {
    UserDefaults.standard.removeObject(forKey: contentsKey)
    UserDefaults.standard.removeObject(forKey: filenameKey)
    UserDefaults.standard.removeObject(forKey: projectIdKey)
    UserDefaults.standard.removeObject(forKey: clientEmailKey)
  }
}


================================================
FILE: QuickPush/Utilities/FloatingPanel.swift
================================================
//
//  FloatingPanel.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import AppKit

/// A floating NSPanel that stays on top of other windows.
/// Used for the "Pin Window" feature so the app persists
/// even when the user clicks outside.
final class FloatingPanel: NSPanel {

  /// Called when the user closes the panel via the title-bar X button.
  /// The WindowManager sets this so it can run `unpin()` cleanly.
  var onClose: (() -> Void)?

  init(contentRect: NSRect) {
    super.init(
      contentRect: contentRect,
      styleMask: [
        .titled,
        .closable,
        .miniaturizable,
        .resizable,
      ],
      backing: .buffered,
      defer: false
    )

    level = .floating
    isFloatingPanel = true
    hidesOnDeactivate = false
    collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
    titlebarAppearsTransparent = false
    titleVisibility = .hidden
    isMovableByWindowBackground = true
    title = "Quick Push"
    isReleasedWhenClosed = false
    animationBehavior = .utilityWindow
    isOpaque = false
    backgroundColor = .clear

    // Match the menu bar popover appearance
    let blurView = NSVisualEffectView()
    blurView.material = .popover
    blurView.blendingMode = .behindWindow
    blurView.state = .active
    contentView = blurView

    // Custom icon + title placed directly in the titlebar
    if let titlebarView = standardWindowButton(.closeButton)?.superview {
      let iconView = NSImageView()
      let config = NSImage.SymbolConfiguration(pointSize: 12, weight: .medium)
      iconView.image = NSImage(
        systemSymbolName: "bolt.brakesignal",
        accessibilityDescription: "Quick Push"
      )?.withSymbolConfiguration(config)
      iconView.contentTintColor = .secondaryLabelColor
      iconView.translatesAutoresizingMaskIntoConstraints = false

      let label = NSTextField(labelWithString: "Quick Push")
      label.font = .titleBarFont(ofSize: 13)
      label.textColor = .labelColor
      label.translatesAutoresizingMaskIntoConstraints = false

      let stack = NSStackView(views: [iconView, label])
      stack.orientation = .horizontal
      stack.spacing = 4
      stack.alignment = .centerY
      stack.translatesAutoresizingMaskIntoConstraints = false

      titlebarView.addSubview(stack)
      NSLayoutConstraint.activate([
        stack.centerYAnchor.constraint(equalTo: titlebarView.centerYAnchor),
        stack.centerXAnchor.constraint(equalTo: titlebarView.centerXAnchor),
      ])
    }

    // Minimum size to keep the UI usable
    minSize = NSSize(width: 570, height: 460)

    // Restore saved position or center on screen
    if let frameString = UserDefaults.standard.string(forKey: "FloatingPanelFrame"),
       !frameString.isEmpty {
      setFrame(NSRectFromString(frameString), display: true)
    } else {
      center()
    }
  }

  override var canBecomeKey: Bool { true }
  override var canBecomeMain: Bool { true }

  /// Swallow the Escape key so it doesn't close the pinned panel.
  override func cancelOperation(_ sender: Any?) { }

  /// Save the panel frame whenever it moves or resizes.
  func saveFrame() {
    UserDefaults.standard.set(NSStringFromRect(frame), forKey: "FloatingPanelFrame")
  }

  override func close() {
    saveFrame()
    // Route through the WindowManager so it can cleanly detach
    // SwiftUI views before the window hides.
    if let onClose {
      onClose()
    } else {
      super.close()
    }
  }
}


================================================
FILE: QuickPush/Utilities/SavedTokenStore.swift
================================================
//
//  SavedTokenStore.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import Foundation

/// Manages persistent storage of saved push tokens in UserDefaults.
class SavedTokenStore {
  /// Store for Expo push tokens (ExponentPushToken format).
  static let shared = SavedTokenStore()
  /// Store for native APNs device tokens (hex format).
  static let nativePush = SavedTokenStore(storageKey: "savedNativePushTokens")
  /// Store for FCM registration tokens.
  static let fcm = SavedTokenStore(storageKey: "savedFCMTokens")

  private let storageKey: String

  init(storageKey: String = "savedPushTokens") {
    self.storageKey = storageKey
  }

  // MARK: - Read

  func loadTokens() -> [SavedToken] {
    guard let data = UserDefaults.standard.data(forKey: storageKey) else {
      return []
    }
    return (try? JSONDecoder().decode([SavedToken].self, from: data)) ?? []
  }

  // MARK: - Write

  func saveTokens(_ tokens: [SavedToken]) {
    if let data = try? JSONEncoder().encode(tokens) {
      UserDefaults.standard.set(data, forKey: storageKey)
    }
  }

  func addToken(_ token: SavedToken) {
    var tokens = loadTokens()
    tokens.append(token)
    saveTokens(tokens)
  }

  func removeToken(id: UUID) {
    var tokens = loadTokens()
    tokens.removeAll { $0.id == id }
    saveTokens(tokens)
  }
}


================================================
FILE: QuickPush/Utilities/SecurityBookmarkManager.swift
================================================
//
//  SecurityBookmarkManager.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import Foundation
import AppKit

/// Manages selection and persistent storage of the .p8 APNs auth key.
///
/// Instead of relying on security-scoped bookmarks (which fail in sandbox
/// and break across Xcode rebuilds), we read the file contents once when
/// selected and store them directly in UserDefaults.
class SecurityBookmarkManager {
  static let shared = SecurityBookmarkManager()

  private let p8ContentsKey = "p8FileContents"
  private let p8FilenameKey = "p8FileName"

  // MARK: - File Picker

  func selectP8File(completion: @escaping (String?, String?) -> Void) {
    let panel = NSOpenPanel()
    panel.title = "Select APNs Auth Key (.p8)"
    panel.allowedContentTypes = [.init(filenameExtension: "p8")!]
    panel.allowsMultipleSelection = false
    panel.canChooseDirectories = false

    panel.begin { response in
      guard response == .OK, let url = panel.url else {
        completion(nil, nil)
        return
      }

      // Read the file contents immediately while NSOpenPanel access is active.
      let contents = try? String(contentsOf: url, encoding: .utf8)
      let filename = url.lastPathComponent

      if let contents {
        self.saveP8(contents: contents, filename: filename)
      }

      completion(contents, filename)
    }
  }

  // MARK: - Storage

  func saveP8(contents: String, filename: String) {
    UserDefaults.standard.set(contents, forKey: p8ContentsKey)
    UserDefaults.standard.set(filename, forKey: p8FilenameKey)
  }

  func storedP8Contents() -> String? {
    UserDefaults.standard.string(forKey: p8ContentsKey)
  }

  func storedP8Filename() -> String? {
    UserDefaults.standard.string(forKey: p8FilenameKey)
  }

  func clearStoredP8() {
    UserDefaults.standard.removeObject(forKey: p8ContentsKey)
    UserDefaults.standard.removeObject(forKey: p8FilenameKey)
  }
}


================================================
FILE: QuickPush/Utilities/View+Extensions.swift
================================================
import SwiftUI

extension View {
    @ViewBuilder
    func applying<V: View>(@ViewBuilder _ modifier: (Self) -> V) -> some View {
        modifier(self)
    }

    /// Present content as a `.sheet` when pinned, or `.popover` when in the MenuBarExtra.
    @ViewBuilder
    func adaptivePresentation<Content: View>(
        isPresented: Binding<Bool>,
        isPinned: Bool,
        @ViewBuilder content: @escaping () -> Content
    ) -> some View {
        if isPinned {
            self.sheet(isPresented: isPresented, content: content)
        } else {
            self.popover(isPresented: isPresented, arrowEdge: .top, content: content)
        }
    }

    /// Present content as a `.sheet` when pinned, or `.popover` when in the MenuBarExtra (item variant).
    @ViewBuilder
    func adaptivePresentation<Item: Identifiable, Content: View>(
        item: Binding<Item?>,
        isPinned: Bool,
        @ViewBuilder content: @escaping (Item) -> Content
    ) -> some View {
        if isPinned {
            self.sheet(item: item, content: content)
        } else {
            self.popover(item: item, arrowEdge: .top, content: content)
        }
    }
}


================================================
FILE: QuickPush/Utilities/WindowManager.swift
================================================
//
//  WindowManager.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

/// Manages the floating "pinned" window and the global keyboard shortcut.
@Observable
final class WindowManager {

  /// Whether the floating panel is currently visible.
  private(set) var isPinned: Bool = false

  private var panel: FloatingPanel?
  private var globalMonitor: Any?
  private var localMonitor: Any?
  private var hotkeyRegistered = false

  deinit {
    removeHotkey()
  }

  /// Call once after app has finished launching (e.g. from onAppear).
  func startMonitoring() {
    guard !hotkeyRegistered else { return }
    hotkeyRegistered = true
    registerHotkey()
  }

  // MARK: - Pin / Unpin

  func togglePin() {
    if isPinned {
      unpin()
    } else {
      pin()
    }
  }

  func pin() {
    guard !isPinned else { return }

    // Dismiss the MenuBarExtra popover by closing every visible window
    // that isn't our floating panel. The popover is backed by an internal
    // NSWindow — ordering it out hides it without destroying it.
    for window in NSApp.windows where window !== panel && window.isVisible {
      window.orderOut(nil)
    }

    // Create the floating panel if needed
    if panel == nil {
      let rect = NSRect(x: 0, y: 0, width: 570, height: 500)
      let newPanel = FloatingPanel(contentRect: rect)
      newPanel.onClose = { [weak self] in
        self?.unpin()
      }
      panel = newPanel
    }

    // Host the SwiftUI content inside the panel's blur view
    let content = MainContentView()
      .environment(self)

    let hostingView = NSHostingView(rootView: content)
    hostingView.sizingOptions = [.minSize]
    hostingView.translatesAutoresizingMaskIntoConstraints = false
    hostingView.layer?.backgroundColor = .clear

    if let blurView = panel?.contentView {
      // Remove any previous hosting view
      blurView.subviews.forEach { $0.removeFromSuperview() }
      blurView.addSubview(hostingView)
      NSLayoutConstraint.activate([
        hostingView.topAnchor.constraint(equalTo: blurView.topAnchor),
        hostingView.bottomAnchor.constraint(equalTo: blurView.bottomAnchor),
        hostingView.leadingAnchor.constraint(equalTo: blurView.leadingAnchor),
        hostingView.trailingAnchor.constraint(equalTo: blurView.trailingAnchor),
      ])
    }

    // Show the panel after a brief delay so the popover finishes closing.
    DispatchQueue.main.async { [weak self] in
      guard let self, let panel = self.panel else { return }

      // Temporarily become a regular app so macOS lets us show a window,
      // then switch back to accessory so we don't linger in the Dock.
      NSApp.setActivationPolicy(.regular)
      panel.makeKeyAndOrderFront(nil)

      DispatchQueue.main.async {
        NSApp.setActivationPolicy(.accessory)
      }

      self.isPinned = true
    }
  }

  func unpin() {
    guard isPinned else { return }

    guard let panel else {
      isPinned = false
      return
    }

    panel.saveFrame()

    // Detach the SwiftUI hosting view FIRST so it doesn't try to
    // re-render while the window disappears underneath it.
    // Keep the blur view alive — only remove the hosting subview.
    panel.contentView?.subviews.forEach { $0.removeFromSuperview() }

    // Now hide the panel safely.
    panel.orderOut(nil)

    isPinned = false
  }

  /// Called when the user clicks the menu bar icon while the panel is pinned.
  func bringPanelToFront() {
    guard isPinned, let panel else { return }
    panel.orderFrontRegardless()
    panel.makeKey()
  }

  // MARK: - Global Keyboard Shortcut (Cmd+Shift+P)

  private func registerHotkey() {
    // Global monitor – fires when our app is NOT focused
    globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in
      self?.handleHotkey(event)
    }

    // Local monitor – fires when our app IS focused
    localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
      if self?.handleHotkey(event) == true {
        return nil // swallow the event
      }
      return event
    }
  }

  private func removeHotkey() {
    if let globalMonitor { NSEvent.removeMonitor(globalMonitor) }
    if let localMonitor { NSEvent.removeMonitor(localMonitor) }
    globalMonitor = nil
    localMonitor = nil
  }

  /// Returns `true` if the event matched our shortcut and was handled.
  @discardableResult
  private func handleHotkey(_ event: NSEvent) -> Bool {
    // Cmd + Shift + P
    guard event.modifierFlags.contains([.command, .shift]),
          event.charactersIgnoringModifiers?.lowercased() == "p" else {
      return false
    }

    DispatchQueue.main.async { [weak self] in
      self?.togglePin()
    }
    return true
  }
}


================================================
FILE: QuickPush/ViewModels/FCMViewModel.swift
================================================
//
//  FCMViewModel.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

@Observable
class FCMViewModel {
  // MARK: - Shared FCM config
  var config: FCMConfigStore = .shared

  // MARK: - Tokens
  var tokens: [String] = [""]
  var savedTokens: [SavedToken] = []

  var allValidTokens: [String] {
    savedTokens.filter(\.isEnabled).map(\.token) + tokens.filter { !$0.isEmpty }
  }

  // MARK: - Message fields
  var messageType: FCMMessageType = .notification
  var title: String = ""
  var body: String = ""
  var imageUrl: String = ""
  var channelId: String = "default"
  var sound: String = "default"
  var color: String = ""
  var priority: String = "HIGH"
  var customData: [String: String] = [:]

  // MARK: - UI State
  var isSending: Bool = false
  var showToast: Bool = false
  var toastMessage: String = ""
  var toastType: ToastType = .success
  var showResponseSheet: Bool = false
  var showCurlSheet: Bool = false
  var lastResponse: FCMResponse?
  var tokenToSave: TokenToSave?
  var curlCommand: String = ""

  // MARK: - Init
  init() {
    savedTokens = SavedTokenStore.fcm.loadTokens()
  }

  // MARK: - Validation
  var canSend: Bool {
    !allValidTokens.isEmpty && config.fcmConfiguration().isValid && !isSending
  }

  // MARK: - Build Message
  func buildMessage(token: String) -> FCMMessage {
    var notification: FCMNotification?
    if messageType == .notification {
      notification = FCMNotification(
        title: title.isEmpty ? nil : title,
        body: body.isEmpty ? nil : body,
        image: imageUrl.isEmpty ? nil : imageUrl
      )
    }

    var androidNotification: FCMAndroidNotification?
    if messageType == .notification {
      androidNotification = FCMAndroidNotification(
        channelId: channelId.isEmpty ? nil : channelId,
        sound: sound.isEmpty ? nil : sound,
        color: color.isEmpty ? nil : "#\(color.trimmingCharacters(in: CharacterSet(charactersIn: "#")))"
      )
    }

    let androidConfig = FCMAndroidConfig(
      priority: priority,
      notification: androidNotification
    )

    let data = customData.isEmpty ? nil : customData

    return FCMMessage(
      token: token,
      notification: notification,
      android: androidConfig,
      data: data
    )
  }

  // MARK: - Send
  func send() {
    guard canSend else { return }

    if messageType == .notification && title.isEmpty && body.isEmpty {
      showToastMessage("Notification messages require at least a title or body.", type: .error)
      return
    }

    isSending = true

    let validTokens = allValidTokens
    let configuration = config.fcmConfiguration()

    let group = DispatchGroup()
    var responses: [FCMResponse] = []
    var firstError: Error?

    for token in validTokens {
      let message = buildMessage(token: token)
      group.enter()
      FCMService.shared.send(
        message: message,
        configuration: configuration
      ) { result in
        switch result {
        case .success(let response):
          responses.append(response)
        case .failure(let error):
          if firstError == nil { firstError = error }
          if case FCMService.FCMError.requestFailed(let response) = error {
            responses.append(response)
          }
        }
        group.leave()
      }
    }

    group.notify(queue: .main) { [weak self] in
      guard let self else { return }
      self.isSending = false
      self.lastResponse = responses.last
      if let error = firstError {
        self.showToastMessage(error.localizedDescription, type: .error)
      } else {
        let msg = responses.count == 1
          ? responses[0].summary
          : "\(responses.count) pushes sent"
        self.showToastMessage(msg, type: .success)
      }
    }
  }

  // MARK: - cURL
  func generateCurlCommand() -> String {
    let configuration = config.fcmConfiguration()
    let cleanToken = (allValidTokens.first ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
    let message = buildMessage(token: cleanToken.isEmpty ? "<FCM_REGISTRATION_TOKEN>" : cleanToken)

    let body = FCMRequestBody(message: message)
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
    let bodyString: String
    if let data = try? encoder.encode(body),
       let str = String(data: data, encoding: .utf8) {
      bodyString = str
    } else {
      bodyString = "{}"
    }

    var lines: [String] = []
    lines.append("# Replace <ACCESS_TOKEN> with a valid OAuth 2.0 bearer token.")
    lines.append("# Obtain one by running: gcloud auth print-access-token")
    if allValidTokens.count > 1 {
      lines.append("# cURL shown for the first token. Repeat for each additional token.")
    }
    lines.append("curl -X POST \\")
    lines.append("  https://fcm.googleapis.com/v1/projects/\(configuration.projectId)/messages:send \\")
    lines.append("  -H \"Authorization: Bearer <ACCESS_TOKEN>\" \\")
    lines.append("  -H \"Content-Type: application/json\" \\")
    lines.append("  -d '\(bodyString)'")

    return lines.joined(separator: "\n")
  }

  // MARK: - Clipboard
  func pasteToken(at index: Int) {
    guard let clipboardString = NSPasteboard.general.string(forType: .string) else { return }
    let trimmed = clipboardString.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !trimmed.isEmpty else {
      showToastMessage("Clipboard is empty.", type: .error)
      return
    }
    if index < tokens.count {
      tokens[index] = trimmed
    }
  }

  // MARK: - Toast
  func showToastMessage(_ message: String, type: ToastType) {
    toastMessage = message
    toastType = type
    showToast = true
  }
}


================================================
FILE: QuickPush/ViewModels/LiveActivityViewModel.swift
================================================
//
//  LiveActivityViewModel.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

@Observable
class LiveActivityViewModel {
  // MARK: - Event Type
  var eventType: LiveActivityEvent = .start

  // MARK: - Token
  var deviceToken: String = ""

  // MARK: - Content State
  var contentTitle: String = ""
  var contentSubtitle: String = ""
  var includeProgress: Bool = false
  var progress: Double = 0.5
  var includeTimerEnd: Bool = false
  var timerEndDate: Date = Date().addingTimeInterval(3600)
  var imageName: String = ""
  var dynamicIslandImageName: String = ""

  // MARK: - Attributes (Start only)
  var attributeName: String = ""
  var backgroundColor: Color = Color(hex: "001A72")
  var titleColor: Color = Color(hex: "EBEBF0")
  var subtitleColor: Color = Color(hex: "EBEBF599")
  var progressViewTint: Color = Color(hex: "FFFFFF")
  var progressViewLabelColor: Color = Color(hex: "EBEBF0")
  var deepLinkUrl: String = ""
  var timerType: String = "digital"
  var imagePosition: String = "left"
  var imageWidth: Int = 40
  var imageHeight: Int = 40
  var useCustomPadding: Bool = false
  var uniformPadding: Int = 16
  var paddingTop: Int = 16
  var paddingBottom: Int = 16
  var paddingLeft: Int = 16
  var paddingRight: Int = 16

  // MARK: - Alert (Start only)
  var includeAlert: Bool = false
  var alertTitle: String = ""
  var alertBody: String = ""
  var alertSound: String = "default"

  // MARK: - APNs Configuration (shared with APNs tab)
  var config: APNsConfigStore = .shared
  var attributesType: String = "LiveActivityAttributes" {
    didSet { UserDefaults.standard.set(attributesType, forKey: "apns_attributesType") }
  }

  // MARK: - UI State
  var isSending: Bool = false
  var showToast: Bool = false
  var toastMessage: String = ""
  var toastType: ToastType = .success
  var showJSONSheet: Bool = false
  var showResponseSheet: Bool = false
  var lastResponse: APNsResponse?

  // MARK: - Init
  init() {
    attributesType = UserDefaults.standard.string(forKey: "apns_attributesType") ?? "LiveActivityAttributes"
  }

  // MARK: - Validation
  var canSend: Bool {
    !deviceToken.isEmpty && apnsConfiguration.isValid && !isSending
  }

  var apnsConfiguration: APNsConfiguration {
    config.apnsConfiguration(topicSuffix: ".push-type.liveactivity")
  }

  var tokenLabel: String {
    eventType == .start ? "Push-to-Start Token" : "Activity Token"
  }

  // MARK: - Build Payload
  func buildPayload() -> LiveActivityAPNsPayload {
    let contentState = LiveActivityContentState(
      title: contentTitle.isEmpty ? "Activity" : contentTitle,
      subtitle: contentSubtitle.isEmpty ? nil : contentSubtitle,
      timerEndDateInMilliseconds: includeTimerEnd ? timerEndDate.timeIntervalSince1970 * 1000 : nil,
      progress: includeProgress ? progress : nil,
      imageName: imageName.isEmpty ? nil : imageName,
      dynamicIslandImageName: dynamicIslandImageName.isEmpty ? nil : dynamicIslandImageName
    )

    var attributes: LiveActivityAttributes?
    if eventType == .start {
      let paddingDetails: LiveActivityPaddingDetails?
      if useCustomPadding {
        paddingDetails = LiveActivityPaddingDetails(
          top: paddingTop,
          bottom: paddingBottom,
          left: paddingLeft,
          right: paddingRight
        )
      } else {
        paddingDetails = nil
      }

      attributes = LiveActivityAttributes(
        name: attributeName.isEmpty ? "LiveActivity" : attributeName,
        backgroundColor: backgroundColor.toHexString(),
        titleColor: titleColor.toHexString(),
        subtitleColor: subtitleColor.toHexStringWithAlpha(),
        progressViewTint: progressViewTint.toHexString(),
        progressViewLabelColor: progressViewLabelColor.toHexString(),
        deepLinkUrl: deepLinkUrl.isEmpty ? nil : deepLinkUrl,
        timerType: timerType,
        padding: useCustomPadding ? nil : uniformPadding,
        paddingDetails: paddingDetails,
        imagePosition: imagePosition,
        imageWidth: (!imageName.isEmpty || !dynamicIslandImageName.isEmpty) ? imageWidth : nil,
        imageHeight: (!imageName.isEmpty || !dynamicIslandImageName.isEmpty) ? imageHeight : nil
      )
    }

    var alert: LiveActivityAlert?
    if includeAlert && eventType == .start {
      alert = LiveActivityAlert(
        title: alertTitle.isEmpty ? nil : alertTitle,
        body: alertBody.isEmpty ? nil : alertBody,
        sound: alertSound.isEmpty ? nil : alertSound
      )
    }

    let aps = LiveActivityAPS(
      timestamp: Int(Date().addingTimeInterval(5).timeIntervalSince1970),
      event: eventType,
      contentState: contentState,
      attributesType: eventType == .start ? attributesType : nil,
      attributes: attributes,
      alert: alert,
      dismissalDate: eventType == .end ? Int(Date().addingTimeInterval(5).timeIntervalSince1970) : nil,
      relevanceScore: nil,
      staleDate: nil
    )

    return LiveActivityAPNsPayload(aps: aps)
  }

  // MARK: - Send
  func send() {
    guard canSend else { return }
    isSending = true

    let payload = buildPayload()

    APNsService.shared.sendLiveActivityPush(
      payload: payload,
      token: deviceToken,
      configuration: apnsConfiguration
    ) { [weak self] result in
      DispatchQueue.main.async {
        guard let self else { return }
        self.isSending = false
        switch result {
        case .success(let response):
          self.lastResponse = response
          self.showToastMessage(response.summary, type: .success)
        case .failure(let error):
          if case APNsService.APNsError.requestFailed(let response) = error {
            self.lastResponse = response
          }
          self.showToastMessage(error.localizedDescription, type: .error)
        }
      }
    }
  }

  // MARK: - JSON Export/Import
  func exportJSON() -> String {
    let payload = buildPayload()
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

    guard let data = try? encoder.encode(payload),
          let json = String(data: data, encoding: .utf8) else {
      return "{}"
    }
    return json
  }

  func importJSON(_ jsonString: String) -> Bool {
    guard let data = jsonString.data(using: .utf8) else { return false }

    do {
      let payload = try JSONDecoder().decode(LiveActivityAPNsPayload.self, from: data)
      applyPayload(payload)
      return true
    } catch {
      showToastMessage("Invalid JSON: \(error.localizedDescription)", type: .error)
      return false
    }
  }

  private func applyPayload(_ payload: LiveActivityAPNsPayload) {
    let aps = payload.aps
    eventType = aps.event

    // Content State
    let cs = aps.contentState
    contentTitle = cs.title
    contentSubtitle = cs.subtitle ?? ""
    if let p = cs.progress {
      includeProgress = true
      progress = p
    } else {
      includeProgress = false
    }
    if let timerEnd = cs.timerEndDateInMilliseconds {
      includeTimerEnd = true
      timerEndDate = Date(timeIntervalSince1970: timerEnd / 1000)
    } else {
      includeTimerEnd = false
    }
    imageName = cs.imageName ?? ""
    dynamicIslandImageName = cs.dynamicIslandImageName ?? ""

    // Attributes
    if let attrs = aps.attributes {
      attributeName = attrs.name
      if let bg = attrs.backgroundColor { backgroundColor = Color(hex: bg) }
      if let tc = attrs.titleColor { titleColor = Color(hex: tc) }
      if let sc = attrs.subtitleColor { subtitleColor = Color(hex: sc) }
      if let pt = attrs.progressViewTint { progressViewTint = Color(hex: pt) }
      if let pl = attrs.progressViewLabelColor { progressViewLabelColor = Color(hex: pl) }
      deepLinkUrl = attrs.deepLinkUrl ?? ""
      timerType = attrs.timerType ?? "digital"
      imagePosition = attrs.imagePosition ?? "left"
      if let w = attrs.imageWidth { imageWidth = w }
      if let h = attrs.imageHeight { imageHeight = h }
      if let p = attrs.padding {
        useCustomPadding = false
        uniformPadding = p
      }
      if let pd = attrs.paddingDetails {
        useCustomPadding = true
        paddingTop = pd.top ?? 16
        paddingBottom = pd.bottom ?? 16
        paddingLeft = pd.left ?? 16
        paddingRight = pd.right ?? 16
      }
    }

    // Alert
    if let alert = aps.alert {
      includeAlert = true
      alertTitle = alert.title ?? ""
      alertBody = alert.body ?? ""
      alertSound = alert.sound ?? "default"
    } else {
      includeAlert = false
    }
  }

  // MARK: - Toast
  private func showToastMessage(_ message: String, type: ToastType) {
    toastMessage = message
    toastType = type
    showToast = true
  }

  // MARK: - File Picker
  func selectP8File() {
    config.selectP8File()
  }

  // MARK: - Clipboard
  func pasteToken() {
    if let clipboardString = NSPasteboard.general.string(forType: .string) {
      let trimmed = clipboardString.trimmingCharacters(in: .whitespacesAndNewlines)
      let hexCharacterSet = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
      if trimmed.unicodeScalars.allSatisfy({ hexCharacterSet.contains($0) }) && !trimmed.isEmpty {
        deviceToken = trimmed
      } else {
        showToastMessage("Invalid token format. Expected hex string.", type: .error)
      }
    }
  }
}


================================================
FILE: QuickPush/ViewModels/NativePushViewModel.swift
================================================
//
//  NativePushViewModel.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

@Observable
class NativePushViewModel {
  // MARK: - Shared APNs config
  var config: APNsConfigStore = .shared

  // MARK: - Tokens
  var tokens: [String] = [""]
  var savedTokens: [SavedToken] = []

  var allValidTokens: [String] {
    savedTokens.filter(\.isEnabled).map(\.token) + tokens.filter { !$0.isEmpty }
  }

  // MARK: - Push fields
  var pushType: NativePushType = .alert
  var title: String = ""
  var subtitle: String = ""
  var body: String = ""
  var sound: String = "default"
  var badge: String = ""
  var threadId: String = ""
  var categoryId: String = ""
  var interruptionLevel: NativeInterruptionLevel = .active
  var imageUrl: String = ""
  var mutableContent: Bool = false
  var contentAvailable: Bool = false
  var priority: Int = 10
  var customData: [String: String] = [:]

  // MARK: - UI State
  var isSending: Bool = false
  var showToast: Bool = false
  var toastMessage: String = ""
  var toastType: ToastType = .success
  var showResponseSheet: Bool = false
  var showCurlSheet: Bool = false
  var lastResponse: APNsResponse?
  var tokenToSave: TokenToSave?
  var curlCommand: String = ""

  // MARK: - Init
  init() {
    savedTokens = SavedTokenStore.nativePush.loadTokens()
  }

  // MARK: - Validation
  var canSend: Bool {
    !allValidTokens.isEmpty && config.apnsConfiguration(topicSuffix: nil).isValid && !isSending
  }

  // MARK: - Build Payload
  func buildPayload() -> NativePushPayload {
    var alert: NativeAlert?
    if pushType == .alert {
      let t = title.isEmpty ? nil : title
      let s = subtitle.isEmpty ? nil : subtitle
      let b = body.isEmpty ? nil : body
      // Only include alert key if there is at least some visible content
      if t != nil || s != nil || b != nil {
        alert = NativeAlert(title: t, subtitle: s, body: b)
      }
    }

    let effectiveMutableContent = mutableContent || !imageUrl.isEmpty
    let aps = NativeAPS(
      alert: alert,
      badge: Int(badge),
      sound: (pushType == .alert && !sound.isEmpty) ? sound : nil,
      contentAvailable: contentAvailable ? 1 : nil,
      mutableContent: effectiveMutableContent ? 1 : nil,
      threadId: threadId.isEmpty ? nil : threadId,
      category: categoryId.isEmpty ? nil : categoryId,
      interruptionLevel: (pushType == .alert) ? interruptionLevel.rawValue : nil
    )

    return NativePushPayload(aps: aps, customData: customData, imageUrl: imageUrl.isEmpty ? nil : imageUrl)
  }

  // MARK: - Send
  func send() {
    guard canSend else { return }

    if pushType == .alert && title.isEmpty && body.isEmpty {
      showToastMessage("Alert notifications require at least a title or body.", type: .error)
      return
    }

    isSending = true

    let validTokens = allValidTokens
    let payload = buildPayload()
    let configuration = config.apnsConfiguration(topicSuffix: nil)

    let group = DispatchGroup()
    var responses: [APNsResponse] = []
    var firstError: Error?

    for token in validTokens {
      group.enter()
      APNsService.shared.sendNativePush(
        payload: payload,
        token: token,
        pushType: pushType,
        priority: priority,
        configuration: configuration
      ) { result in
        switch result {
        case .success(let response):
          responses.append(response)
        case .failure(let error):
          if firstError == nil { firstError = error }
          if case APNsService.APNsError.requestFailed(let response) = error {
            responses.append(response)
          }
        }
        group.leave()
      }
    }

    group.notify(queue: .main) { [weak self] in
      guard let self else { return }
      self.isSending = false
      self.lastResponse = responses.last
      if let error = firstError {
        self.showToastMessage(error.localizedDescription, type: .error)
      } else {
        let msg = responses.count == 1
          ? responses[0].summary
          : "\(responses.count) pushes sent"
        self.showToastMessage(msg, type: .success)
      }
    }
  }

  // MARK: - cURL
  func generateCurlCommand() -> String {
    let configuration = config.apnsConfiguration(topicSuffix: nil)
    let jwt: String
    if let p8Contents = configuration.p8Contents,
       let token = try? JWTSigner.generateToken(
         teamId: configuration.teamId,
         keyId: configuration.keyId,
         p8Contents: p8Contents
       ) {
      jwt = token
    } else {
      jwt = "<JWT>"
    }

    let cleanToken = (allValidTokens.first ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
    let payload = buildPayload()
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
    let bodyString: String
    if let data = try? encoder.encode(payload),
       let str = String(data: data, encoding: .utf8) {
      bodyString = str
    } else {
      bodyString = "{}"
    }

    var lines: [String] = []
    lines.append("# Note: JWT tokens expire after 1 hour.")
    if allValidTokens.count > 1 {
      lines.append("# cURL shown for the first token. Repeat for each additional token.")
    }
    lines.append("curl --http2 -X POST \\")
    lines.append("  https://\(configuration.hostname)/3/device/\(cleanToken) \\")
    lines.append("  -H \"authorization: bearer \(jwt)\" \\")
    lines.append("  -H \"apns-topic: \(configuration.topic)\" \\")
    lines.append("  -H \"apns-push-type: \(pushType.rawValue)\" \\")
    lines.append("  -H \"apns-priority: \(priority)\" \\")
    lines.append("  -H \"content-type: application/json\" \\")
    lines.append("  -d '\(bodyString)'")

    return lines.joined(separator: "\n")
  }

  // MARK: - Clipboard
  func pasteToken(at index: Int) {
    guard let clipboardString = NSPasteboard.general.string(forType: .string) else { return }
    let trimmed = clipboardString.trimmingCharacters(in: .whitespacesAndNewlines)
    let hexCharacterSet = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
    if trimmed.unicodeScalars.allSatisfy({ hexCharacterSet.contains($0) }) && !trimmed.isEmpty {
      if index < tokens.count {
        tokens[index] = trimmed
      }
    } else {
      showToastMessage("Invalid token format. Expected hex string.", type: .error)
    }
  }

  // MARK: - Toast
  func showToastMessage(_ message: String, type: ToastType) {
    toastMessage = message
    toastType = type
    showToast = true
  }
}


================================================
FILE: QuickPush/Views/APNsConfigurationView.swift
================================================
//
//  APNsConfigurationView.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

struct APNsConfigurationView: View {
  @Bindable var config: APNsConfigStore
  @State private var isExpanded: Bool = true

  private var isValid: Bool {
    !config.teamId.isEmpty && !config.keyId.isEmpty && !config.bundleId.isEmpty && config.hasP8Key
  }

  var body: some View {
    VStack(alignment: .leading, spacing: 8) {
      Button(action: { withAnimation { isExpanded.toggle() } }) {
        HStack {
          Text("APNs Configuration")
            .font(.subheadline)
            .fontWeight(.medium)
          Spacer()
          Circle()
            .fill(isValid ? Color.green : Color.orange)
            .frame(width: 8, height: 8)
          Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
            .foregroundColor(.secondary)
        }
      }
      .buttonStyle(.plain)

      if isExpanded {
        VStack(alignment: .leading, spacing: 8) {
          HStack {
            Text("Team ID:")
              .frame(width: 70, alignment: .leading)
            TextField("e.g. A1B2C3D4E5", text: $config.teamId)
              .textFieldStyle(.roundedBorder)
          }

          HStack {
            Text("Key ID:")
              .frame(width: 70, alignment: .leading)
            TextField("e.g. ABCDE12345", text: $config.keyId)
              .textFieldStyle(.roundedBorder)
          }

          HStack {
            Text("Bundle ID:")
              .frame(width: 70, alignment: .leading)
            TextField("e.g. com.example.app", text: $config.bundleId)
              .textFieldStyle(.roundedBorder)
          }

          HStack {
            Text(".p8 Key:")
              .frame(width: 70, alignment: .leading)
            if let name = config.p8FileName, config.hasP8Key {
              Text(name)
                .foregroundColor(.secondary)
                .lineLimit(1)
                .truncationMode(.middle)
            } else {
              Text("No file selected")
                .foregroundColor(.secondary)
            }
            Spacer()
            Button("Browse...") {
              config.selectP8File()
            }
            .controlSize(.small)
          }

          HStack {
            Text("Environment:")
            Picker("", selection: $config.environment) {
              Text("Sandbox").tag(APNsEnvironment.sandbox)
              Text("Production").tag(APNsEnvironment.production)
            }
            .pickerStyle(.segmented)
          }
        }
        .padding(.leading, 4)
      }
    }
  }
}


================================================
FILE: QuickPush/Views/APNsCurlCommandView.swift
================================================
//
//  APNsCurlCommandView.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

/// Sheet that displays a copyable APNs curl command with a pre-generated JWT.
struct APNsCurlCommandView: View {
  let curlCommand: String
  @Environment(\.dismiss) private var dismiss
  @State private var copied = false

  var body: some View {
    VStack(spacing: 12) {
      HStack {
        Text("cURL Command")
          .font(.headline)
        Spacer()
        Button("Close") { dismiss() }
          .keyboardShortcut(.cancelAction)
      }

      ScrollView {
        Text(curlCommand)
          .font(.system(.body, design: .monospaced))
          .textSelection(.enabled)
          .frame(maxWidth: .infinity, alignment: .leading)
          .padding(8)
      }
      .background(Color(nsColor: .controlBackgroundColor))
      .cornerRadius(8)
      .overlay(
        RoundedRectangle(cornerRadius: 8)
          .stroke(Color.secondary.opacity(0.2))
      )

      HStack {
        Button(copied ? "Copied!" : "Copy") {
          NSPasteboard.general.clearContents()
          NSPasteboard.general.setString(curlCommand, forType: .string)
          copied = true
          DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            copied = false
          }
        }
        .help("Copy curl command to clipboard")

        Spacer()

        Text("JWT expires after 1 hour")
          .font(.caption)
          .foregroundColor(.secondary)
      }
    }
    .padding()
    .frame(minWidth: 450, minHeight: 300)
  }
}


================================================
FILE: QuickPush/Views/APNsResponseDetailView.swift
================================================
//
//  APNsResponseDetailView.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

/// Sheet that displays the full APNs response diagnostics after a send attempt.
struct APNsResponseDetailView: View {
  let response: APNsResponse?
  @Environment(\.dismiss) private var dismiss

  var body: some View {
    VStack(spacing: 12) {
      HStack {
        Text("APNs Response")
          .font(.headline)
        Spacer()
        Button("Close") { dismiss() }
          .keyboardShortcut(.cancelAction)
      }

      if let response {
        VStack(alignment: .leading, spacing: 0) {
          // Status header
          HStack(spacing: 8) {
            Image(systemName: response.isSuccess ? "checkmark.circle.fill" : "xmark.circle.fill")
              .foregroundColor(response.isSuccess ? .green : .red)
              .font(.title2)
            Text(response.isSuccess ? "Push Accepted" : "Push Rejected")
              .font(.title3)
              .fontWeight(.semibold)
            Spacer()
          }
          .padding(.bottom, 12)

          // Diagnostic rows
          Group {
            ResponseRow(label: "Status", value: "\(response.statusCode)")
            if let reason = response.reason {
              ResponseRow(label: "Reason", value: reason, isError: true)
            }
            ResponseRow(label: "Event", value: response.event)
            ResponseRow(label: "Environment", value: "\(response.environment.rawValue.capitalized)")
            ResponseRow(label: "Hostname", value: response.hostname)
            ResponseRow(label: "Topic", value: response.topic)
            if let attributesType = response.attributesType {
              ResponseRow(label: "Attributes Type", value: attributesType, isHighlighted: true)
            }
            ResponseRow(label: "Timestamp", value: "\(response.timestamp) (Unix sec)")
            if let apnsId = response.apnsId {
              ResponseRow(label: "apns-id", value: apnsId)
            }
            if let apnsUniqueId = response.apnsUniqueId {
              ResponseRow(label: "apns-unique-id", value: apnsUniqueId)
            }
          }
        }
        .padding()
        .background(Color(nsColor: .controlBackgroundColor))
        .cornerRadius(8)
        .overlay(
          RoundedRectangle(cornerRadius: 8)
            .stroke(Color.secondary.opacity(0.2))
        )

        // Troubleshooting tips for common errors
        if let reason = response.reason {
          troubleshootingTip(for: reason)
        }

        // Warn about silent drops on 200 with start event
        if response.isSuccess && response.event == "start" {
          successButNoActivityTip(attributesType: response.attributesType)
        }

        HStack {
          Button("Copy Diagnostics") {
            NSPasteboard.general.clearContents()
            NSPasteboard.general.setString(response.diagnosticDetails, forType: .string)
          }
          .help("Copy full diagnostic info to clipboard")
          Spacer()
        }
      } else {
        Text("No response yet. Send a push first.")
          .foregroundColor(.secondary)
      }

      Spacer()
    }
    .padding()
    .frame(minWidth: 420, minHeight: 300)
  }

  @ViewBuilder
  private func troubleshootingTip(for reason: String) -> some View {
    let tip: String? = switch reason {
    case "BadDeviceToken":
      "The token is invalid. Check: wrong environment (sandbox vs production), token is stale, or token format is incorrect."
    case "DeviceTokenNotForTopic":
      "The device token doesn't match the topic (bundle ID). Verify your Bundle ID is correct and the token was generated for this app."
    case "Unregistered":
      "The token is no longer valid. The device may have uninstalled the app or the token has expired."
    case "TopicDisallowed":
      "Push notifications are not allowed for this topic. Check your provisioning profile and entitlements."
    case "InvalidProviderToken":
      "The JWT token is invalid. Verify your Team ID, Key ID, and .p8 key file."
    case "ExpiredProviderToken":
      "The JWT token has expired. It will be refreshed on the next send."
    case "MissingTopic":
      "The apns-topic header is missing. This is a QuickPush bug — please report it."
    default:
      nil
    }

    if let tip {
      TipBox(icon: "lightbulb.fill", color: .yellow, text: tip)
    }
  }

  private func successButNoActivityTip(attributesType: String?) -> some View {
    let tip = Self.buildSuccessTip(attributesType: attributesType)
    return TipBox(
      icon: "exclamationmark.triangle.fill",
      color: .orange,
      text: tip
    )
  }

  private static func buildSuccessTip(attributesType: String?) -> String {
    let attrType = attributesType ?? "unknown"
    let needsModulePrefix = !attrType.contains(".")

    var lines = """
    APNs returned 200 but the Live Activity didn't appear? Common causes:

    1. **attributes-type mismatch** — iOS expects the module-qualified name.
       Current value: "\(attrType)"
    """
    if needsModulePrefix {
      lines += "\n   Try: \"YourWidgetTarget.\(attrType)\""
    }
    lines += """

    2. **Timestamp too far from "now"** — must be current Unix seconds.

    3. **Token/environment mismatch** — sandbox token with production, or vice versa.

    To find the exact attributes-type, add this to your iOS app:
    `print(String(reflecting: YourAttributes.self))`
    """
    return lines
  }
}

/// Styled tip box used for troubleshooting hints.
private struct TipBox: View {
  let icon: String
  let color: Color
  let text: String

  var body: some View {
    HStack(alignment: .top, spacing: 8) {
      Image(systemName: icon)
        .foregroundColor(color)
      // Use .init() to enable Markdown rendering in the string
      Text(.init(text))
        .font(.callout)
        .foregroundColor(.secondary)
    }
    .padding(10)
    .background(color.opacity(0.1))
    .cornerRadius(6)
  }
}

/// A single label–value row in the response detail view.
private struct ResponseRow: View {
  let label: String
  let value: String
  var isError: Bool = false
  var isHighlighted: Bool = false

  var body: some View {
    HStack(alignment: .top) {
      Text(label)
        .foregroundColor(.secondary)
        .frame(width: 110, alignment: .trailing)
      Text(value)
        .fontWeight((isError || isHighlighted) ? .semibold : .regular)
        .foregroundColor(isError ? .red : isHighlighted ? .orange : .primary)
        .textSelection(.enabled)
      Spacer()
    }
    .padding(.vertical, 3)
  }
}


================================================
FILE: QuickPush/Views/APNsView.swift
================================================
//
//  APNsView.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

struct APNsView: View {
  var isActive: Bool = true
  @Environment(WindowManager.self) var windowManager
  @State private var viewModel = NativePushViewModel()

  var body: some View {
    VStack(spacing: 16) {
      // Title bar
      HStack {
        Text("APNs")
          .font(.headline)
        Spacer()
        if viewModel.lastResponse != nil {
          Button {
            viewModel.showResponseSheet = true
          } label: {
            HStack(spacing: 4) {
              Circle()
                .fill(viewModel.lastResponse?.isSuccess == true ? Color.green : Color.red)
                .frame(width: 6, height: 6)
              Text("Response")
            }
          }
          .controlSize(.small)
        }
        Button("cURL") {
          viewModel.curlCommand = viewModel.generateCurlCommand()
          viewModel.showCurlSheet = true
        }
        .controlSize(.small)
        .disabled(!viewModel.canSend)
        Button {
          viewModel.send()
        } label: {
          HStack(spacing: 4) {
            Text("Send")
            HStack(spacing: 1) {
              Image(systemName: "command")
              Image(systemName: "return")
            }
            .font(.caption2)
            .opacity(0.7)
          }
        }
        .applying { view in
          if isActive {
            view.keyboardShortcut(.return, modifiers: .command)
          } else {
            view
          }
        }
        .applying { view in
          if #available(macOS 26.0, *) {
            view.buttonStyle(.glassProminent)
          } else {
            view.buttonStyle(.borderedProminent)
          }
        }
        .disabled(!viewModel.canSend)
      }

      ScrollView(.vertical, showsIndicators: false) {
        VStack(alignment: .leading, spacing: 16) {
          // Token Section
          VStack(alignment: .leading, spacing: 8) {
            Text("Device Token:")
              .font(.subheadline)

            // Saved tokens
            ForEach(viewModel.savedTokens) { saved in
              SavedTokenRowView(
                savedToken: saved,
                onToggle: {
                  if let index = viewModel.savedTokens.firstIndex(where: { $0.id == saved.id }) {
                    viewModel.savedTokens[index].isEnabled.toggle()
                    SavedTokenStore.nativePush.saveTokens(viewModel.savedTokens)
                  }
                },
                onCopy: {
                  NSPasteboard.general.clearContents()
                  NSPasteboard.general.setString(saved.token, forType: .string)
                  viewModel.showToastMessage("Token copied to clipboard", type: .success)
                },
                onRemove: {
                  SavedTokenStore.nativePush.removeToken(id: saved.id)
                  viewModel.savedTokens.removeAll { $0.id == saved.id }
                }
              )
            }

            // Unsaved token rows
            ForEach(viewModel.tokens.indices, id: \.self) { index in
              HStack(spacing: 8) {
                TextField("Hex device token", text: $viewModel.tokens[index])
                  .textFieldStyle(.roundedBorder)

                Button(action: { viewModel.pasteToken(at: index) }) {
                  Image(systemName: "doc.on.clipboard")
                    .foregroundColor(.secondary)
                }
                .buttonStyle(.plain)
                .focusable(false)
                .help("Paste hex token from clipboard")

                Button(action: {
                  viewModel.tokenToSave = TokenToSave(index: index, token: viewModel.tokens[index])
                }) {
                  Image(systemName: "square.and.arrow.down")
                    .foregroundColor(.secondary)
                }
                .buttonStyle(.plain)
                .focusable(false)
                .disabled(viewModel.tokens[index].trimmingCharacters(in: .whitespaces).isEmpty)
                .help("Save this token for future sessions")

                if viewModel.tokens.count > 1 || !viewModel.savedTokens.isEmpty {
                  Button(action: { viewModel.tokens.remove(at: index) }) {
                    Image(systemName: "minus.circle.fill")
                      .foregroundColor(.red)
                  }
                  .buttonStyle(.plain)
                  .focusable(false)
                }
              }
            }

            Button(action: { viewModel.tokens.append("") }) {
              HStack {
                Image(systemName: "plus.circle.fill")
                Text("Add Token")
              }
            }
            .buttonStyle(.borderless)
            .padding(.top, 5)
          }

          // APNs Configuration
          APNsConfigurationView(config: viewModel.config)

          Divider()

          // Push Type Picker
          HStack {
            Text("Push Type:")
            Picker("", selection: $viewModel.pushType) {
              Text("Alert").tag(NativePushType.alert)
              Text("Background").tag(NativePushType.background)
            }
            .pickerStyle(.segmented)
          }
          .onChange(of: viewModel.pushType) { _, newType in
            viewModel.priority = newType == .background ? 5 : 10
          }

          // Alert Fields
          if viewModel.pushType == .alert {
            VStack(alignment: .leading, spacing: 8) {
              InputField(label: "Title", text: $viewModel.title, helpText: "Title of the notification")
              InputField(label: "Subtitle", text: $viewModel.subtitle, helpText: "Secondary line below the title")
              InputField(label: "Body", text: $viewModel.body, helpText: "Main message content")
              InputField(label: "Sound", text: $viewModel.sound, helpText: "Sound name. Use \"default\" for the system sound, or provide a custom sound file name.")
              InputField(label: "Badge", text: $viewModel.badge, helpText: "Number to display on the app icon badge")
              InputField(
                label: "Image URL",
                text: $viewModel.imageUrl,
                helpText: "URL of an image to display in the notification. Requires a Notification Service Extension in the app. Automatically enables Mutable Content. Injected as body._richContent.image in the payload. Learn how to create an NSE: https://codewithbeto.dev/rnCourse/expoNotificationsExtension"
              )
            }
          }

          // Advanced
          DisclosureGroup("Advanced") {
            VStack(alignment: .leading, spacing: 8) {
              InputField(label: "Thread ID", text: $viewModel.threadId, helpText: "Groups related notifications in the notification center")
              InputField(label: "Category", text: $viewModel.categoryId, helpText: "Notification category for interactive actions")

              if viewModel.pushType == .alert {
                HStack {
                  Text("Interruption:")
                  Picker("", selection: $viewModel.interruptionLevel) {
                    ForEach(NativeInterruptionLevel.allCases, id: \.self) { level in
                      Text(level.displayName).tag(level)
                    }
                  }
                  .pickerStyle(.menu)
                  HelpButton(helpText: "Controls delivery timing:\n• Active — default, plays sound and lights screen\n• Passive — delivered quietly, no sound or badge\n• Time Sensitive — breaks through Focus modes\n• Critical — bypasses mute and Focus (requires entitlement)")
                }

                Toggle("Mutable Content", isOn: $viewModel.mutableContent)
                  .help("Allows a Notification Service Extension to modify the payload before display")
              }

              Toggle("Content Available", isOn: $viewModel.contentAvailable)
                .help("Wakes the app in the background to process the notification silently")

              HStack {
                Text("Priority:")
                Picker("", selection: $viewModel.priority) {
                  Text("High (10)").tag(10)
                  Text("Normal (5)").tag(5)
                }
                .pickerStyle(.segmented)
                HelpButton(helpText: "Priority 10 = immediate delivery (wakes device). Priority 5 = normal/background delivery. Background push type requires priority 5.")
              }
            }
            .padding(.top, 8)
          }

          // Custom Data
          KeyValueInputView(data: $viewModel.customData)
        }
      }
    }
    .padding(.horizontal)
    .padding(.top)
    .overlay(
      ToastView(message: viewModel.toastMessage, type: viewModel.toastType, isPresented: $viewModel.showToast)
        .animation(.easeInOut, value: viewModel.showToast)
    )
    .adaptivePresentation(isPresented: $viewModel.showResponseSheet, isPinned: windowManager.isPinned) {
      APNsResponseDetailView(response: viewModel.lastResponse)
    }
    .adaptivePresentation(isPresented: $viewModel.showCurlSheet, isPinned: windowManager.isPinned) {
      APNsCurlCommandView(curlCommand: viewModel.curlCommand)
    }
    .adaptivePresentation(item: $viewModel.tokenToSave, isPinned: windowManager.isPinned) { item in
      SaveTokenSheet(
        token: item.token,
        onSave: { label in
          let savedToken = SavedToken(label: label, token: item.token)
          SavedTokenStore.nativePush.addToken(savedToken)
          viewModel.savedTokens.append(savedToken)
          if item.index < viewModel.tokens.count {
            viewModel.tokens.remove(at: item.index)
          }
          if viewModel.tokens.isEmpty {
            viewModel.tokens.append("")
          }
        },
        warningText: "APNs device tokens may change when you reinstall the app or on OS updates."
      )
    }
  }
}


================================================
FILE: QuickPush/Views/ColorPickerField.swift
================================================
//
//  ColorPickerField.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

struct ColorPickerField: View {
  let label: String
  @Binding var color: Color

  var body: some View {
    HStack {
      Text("\(label):")
      Spacer()
      Text(color.toHexString())
        .font(.system(.body, design: .monospaced))
        .foregroundColor(.secondary)
      ColorPicker("", selection: $color, supportsOpacity: label.contains("Subtitle"))
        .labelsHidden()
        .frame(width: 30)
    }
  }
}


================================================
FILE: QuickPush/Views/ExpoCurlCommandView.swift
================================================
//
//  ExpoCurlCommandView.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

/// Sheet that displays a copyable curl command for the current Expo push notification.
struct ExpoCurlCommandView: View {
  let notification: PushNotification
  let accessToken: String?
  @Environment(\.dismiss) private var dismiss
  @State private var copied = false

  private var curlCommand: String {
    buildCurlCommand()
  }

  var body: some View {
    VStack(spacing: 12) {
      HStack {
        Text("cURL Command")
          .font(.headline)
        Spacer()
        Button("Close") { dismiss() }
          .keyboardShortcut(.cancelAction)
      }

      ScrollView {
        Text(curlCommand)
          .font(.system(.body, design: .monospaced))
          .textSelection(.enabled)
          .frame(maxWidth: .infinity, alignment: .leading)
          .padding(8)
      }
      .background(Color(nsColor: .controlBackgroundColor))
      .cornerRadius(8)
      .overlay(
        RoundedRectangle(cornerRadius: 8)
          .stroke(Color.secondary.opacity(0.2))
      )

      HStack {
        Button(copied ? "Copied!" : "Copy") {
          NSPasteboard.general.clearContents()
          NSPasteboard.general.setString(curlCommand, forType: .string)
          copied = true
          DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            copied = false
          }
        }
        .help("Copy curl command to clipboard")

        Spacer()
      }
    }
    .padding()
    .frame(minWidth: 450, minHeight: 300)
  }

  // MARK: - Build cURL

  private func buildCurlCommand() -> String {
    var lines: [String] = []
    lines.append("curl -X POST https://exp.host/--/api/v2/push/send \\")
    lines.append("  -H \"Content-Type: application/json\" \\")
    lines.append("  -H \"Accept: application/json\" \\")
    lines.append("  -H \"Host: exp.host\" \\")

    if let token = accessToken, !token.isEmpty {
      lines.append("  -H \"Authorization: Bearer \(token)\" \\")
    }

    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
    if let jsonData = try? encoder.encode(notification),
       let jsonString = String(data: jsonData, encoding: .utf8) {
      lines.append("  -d '\(jsonString)'")
    }

    return lines.joined(separator: "\n")
  }
}


================================================
FILE: QuickPush/Views/ExpoReceiptView.swift
================================================
//
//  ExpoReceiptView.swift
//  QuickPush
//
//  Created by beto on 2/20/26.
//

import SwiftUI

struct ExpoReceiptView: View {
  let initialTicketIds: [String]
  let accessToken: String?
  @Environment(\.dismiss) private var dismiss

  @State private var ids: [String]
  @State private var isLoading: Bool = false
  @State private var result: PushNotificationService.ReceiptResult?
  @State private var errorMessage: String?

  init(ticketIds: [String], accessToken: String?) {
    self.initialTicketIds = ticketIds
    self.accessToken = accessToken
    _ids = State(initialValue: ticketIds.isEmpty ? [""] : ticketIds)
  }

  var body: some View {
    VStack(spacing: 12) {
      // Header
      HStack {
        Text("Check Receipts")
          .font(.headline)
        Spacer()
        Button("Close") { dismiss() }
          .keyboardShortcut(.cancelAction)
      }

      // IDs input list
      VStack(alignment: .leading, spacing: 8) {
        Text("Ticket IDs:")
          .font(.subheadline)

        ForEach(ids.indices, id: \.self) { index in
          HStack(spacing: 8) {
            TextField("Ticket ID (e.g. abc123...)", text: $ids[index])
              .textFieldStyle(RoundedBorderTextFieldStyle())
              .font(.system(.caption, design: .monospaced))

            Button(action: {
              if let clip = NSPasteboard.general.string(forType: .string) {
                ids[index] = clip.trimmingCharacters(in: .whitespacesAndNewlines)
              }
            }) {
              Image(systemName: "doc.on.clipboard")
                .foregroundColor(.secondary)
            }
            .buttonStyle(.plain)
            .help("Paste from clipboard")

            if ids.count > 1 {
              Button(action: { ids.remove(at: index) }) {
                Image(systemName: "minus.circle.fill")
                  .foregroundColor(.red)
              }
              .buttonStyle(.plain)
            }
          }
        }

        Button(action: { ids.append("") }) {
          HStack {
            Image(systemName: "plus.circle.fill")
            Text("Add ID")
          }
        }
        .buttonStyle(.borderless)
      }

      // Check button + error
      HStack {
        if let errorMessage {
          Text(errorMessage)
            .font(.caption)
            .foregroundColor(.red)
        }
        Spacer()
        Button {
          checkReceipts()
        } label: {
          if isLoading {
            HStack(spacing: 6) {
              ProgressView()
                .scaleEffect(0.6)
                .frame(width: 14, height: 14)
              Text("Checking…")
            }
          } else {
            Text("Check Receipts")
          }
        }
        .buttonStyle(.borderedProminent)
        .disabled(validIds.isEmpty || isLoading)
      }

      Divider()

      // Results
      if let result {
        resultsSection(result)
      } else {
        Text("Enter ticket IDs and tap \"Check Receipts\".")
          .font(.callout)
          .foregroundColor(.secondary)
          .frame(maxWidth: .infinity, alignment: .leading)
      }

      Spacer()
    }
    .padding()
    .frame(minWidth: 460, minHeight: 340)
  }

  // MARK: - Results

  @ViewBuilder
  private func resultsSection(_ result: PushNotificationService.ReceiptResult) -> some View {
    ScrollView(.vertical, showsIndicators: false) {
      VStack(alignment: .leading, spacing: 10) {
        // API-level errors
        if let errors = result.response.errors, !errors.isEmpty {
          VStack(alignment: .leading, spacing: 6) {
            Label("API Error", systemImage: "xmark.circle.fill")
              .foregroundColor(.red)
              .font(.subheadline.weight(.semibold))

            ForEach(Array(errors.enumerated()), id: \.offset) { _, err in
              VStack(alignment: .leading, spacing: 2) {
                Text(err.code)
                  .font(.system(.caption, design: .monospaced))
                  .fontWeight(.semibold)
                  .foregroundColor(.red)
                Text(err.message)
                  .font(.caption)
                  .foregroundColor(.secondary)
              }
              .padding(8)
              .frame(maxWidth: .infinity, alignment: .leading)
              .background(Color.red.opacity(0.08))
              .cornerRadius(6)
            }
          }
        }

        // Per-receipt results
        if let receipts = result.response.data, !receipts.isEmpty {
          ForEach(receipts.sorted(by: { $0.key < $1.key }), id: \.key) { id, receipt in
            receiptRow(id: id, receipt: receipt)
          }
        } else if result.response.errors == nil {
          Text("No receipts returned for the given IDs. They may be expired or invalid.")
            .font(.callout)
            .foregroundColor(.secondary)
        }

        // Raw JSON disclosure
        if let raw = result.rawJSON, !raw.isEmpty {
          DisclosureGroup("Raw JSON") {
            ScrollView {
              Text(raw)
                .font(.system(.caption, design: .monospaced))
                .textSelection(.enabled)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            .frame(maxHeight: 140)
            .padding(8)
            .background(Color(nsColor: .controlBackgroundColor))
            .cornerRadius(6)
          }
        }
      }
    }
  }

  @ViewBuilder
  private func receiptRow(id: String, receipt: ReceiptResponse.PushReceipt) -> some View {
    let isOk = receipt.status == "ok"
    let errorCode = receipt.details?.error

    VStack(alignment: .leading, spacing: 6) {
      // Status badge + ID
      HStack(spacing: 6) {
        Circle()
          .fill(isOk ? Color.green : Color.red)
          .frame(width: 8, height: 8)
        Text(receipt.status)
          .font(.system(.caption, design: .monospaced))
          .fontWeight(.semibold)
          .foregroundColor(isOk ? .green : .red)
        Spacer()
        Text(id)
          .font(.system(.caption2, design: .monospaced))
          .foregroundColor(.secondary)
          .textSelection(.enabled)
          .lineLimit(1)
          .truncationMode(.middle)
      }

      // "ok" informational note
      if isOk {
        Label(
          "\"ok\" means FCM/APNs accepted the notification — not that the device displayed it.",
          systemImage: "info.circle"
        )
        .font(.caption)
        .foregroundColor(.secondary)
      }

      // Error message
      if let message = receipt.message {
        Text(message)
          .font(.caption)
          .foregroundColor(.red)
          .textSelection(.enabled)
      }

      // Error code + actionable guidance
      if let code = errorCode {
        VStack(alignment: .leading, spacing: 4) {
          HStack(spacing: 4) {
            Text("Error code:")
              .font(.caption)
              .foregroundColor(.secondary)
            Text(code)
              .font(.system(.caption, design: .monospaced))
              .fontWeight(.semibold)
              .foregroundColor(.orange)
          }
          Text(guidance(for: code))
            .font(.caption)
            .foregroundColor(.secondary)
            .fixedSize(horizontal: false, vertical: true)
        }
        .padding(6)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.orange.opacity(0.08))
        .cornerRadius(6)
      }
    }
    .padding(10)
    .frame(maxWidth: .infinity, alignment: .leading)
    .background(isOk ? Color.green.opacity(0.07) : Color.red.opacity(0.07))
    .cornerRadius(8)
  }

  // MARK: - Helpers

  private var validIds: [String] {
    ids.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }
  }

  private func checkReceipts() {
    errorMessage = nil
    isLoading = true
    result = nil

    PushNotificationService.shared.checkReceipts(
      ids: validIds,
      accessToken: accessToken
    ) { outcome in
      DispatchQueue.main.async {
        isLoading = false
        switch outcome {
        case .success(let receiptResult):
          result = receiptResult
        case .failure(let error):
          errorMessage = error.localizedDescription
        }
      }
    }
  }

  private func guidance(for errorCode: String) -> String {
    switch errorCode {
    case "DeviceNotRegistered":
      return "Remove this token — the device has opted out or uninstalled the app."
    case "MessageTooBig":
      return "Reduce the notification payload size."
    case "MessageRateExceeded":
      return "Back off — too many messages have been sent to this device recently."
    case "MismatchSenderId":
      return "This token belongs to a different FCM sender ID. Check your push credentials."
    case "InvalidCredentials":
      return "Your push credentials are invalid. Reconfigure them in the EAS Dashboard."
    default:
      return "Check the Expo push notifications documentation for details on this error."
    }
  }
}


================================================
FILE: QuickPush/Views/ExpoResponseDetailView.swift
================================================
//
//  ExpoResponseDetailView.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

/// Sheet that displays the full Expo Push API response in a developer-friendly way.
struct ExpoResponseDetailView: View {
  let response: PushResponse?
  let httpStatusCode: Int?
  let rawJSON: String?
  @Environment(\.dismiss) private var dismiss

  var body: some View {
    VStack(spacing: 12) {
      HStack {
        Text("Response")
          .font(.headline)
        Spacer()
        Button("Close") { dismiss() }
          .keyboardShortcut(.cancelAction)
      }

      if let response {
        // Top-level API errors (e.g. UNAUTHORIZED)
        if let errors = response.errors, !errors.isEmpty {
          apiErrorsSection(errors)
        }

        // Per-ticket results
        if let tickets = response.data, !tickets.isEmpty {
          ticketsSection(tickets)
        }

        // Raw JSON
        if let rawJSON, !rawJSON.isEmpty {
          rawJSONSection(rawJSON)
        }

        HStack {
          Button("Copy Response") {
            NSPasteboard.general.clearContents()
            NSPasteboard.general.setString(rawJSON ?? "", forType: .string)
          }
          .help("Copy raw JSON response to clipboard")
          .disabled(rawJSON == nil)
          Spacer()
        }
      } else {
        Text("No response yet. Send a push first.")
          .foregroundColor(.secondary)
      }

      Spacer()
    }
    .padding()
    .frame(minWidth: 420, minHeight: 300)
  }

  // MARK: - Sections

  @ViewBuilder
  private func apiErrorsSection(_ errors: [PushResponse.PushError]) -> some View {
    VStack(alignment: .leading, spacing: 8) {
      HStack(spacing: 8) {
        Image(systemName: "xmark.circle.fill")
          .foregroundColor(.red)
          .font(.title2)
        Text("API Error")
          .font(.title3)
          .fontWeight(.semibold)
        if let statusCode = httpStatusCode {
          Text("(\(statusCode))")
            .foregroundColor(.secondary)
        }
        Spacer()
      }

      ForEach(Array(errors.enumerated()), id: \.offset) { _, error in
        VStack(alignment: .leading, spacing: 4) {
          HStack {
            Text(error.code)
              .font(.system(.body, design: .monospaced))
              .fontWeight(.semibold)
              .foregroundColor(.red)
            if error.isTransient == true {
              Text("(transient)")
                .font(.caption)
                .foregroundColor(.orange)
            }
          }
          Text(error.message)
            .font(.callout)
            .foregroundColor(.secondary)
            .textSelection(.enabled)
        }
        .padding(10)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.red.opacity(0.08))
        .cornerRadius(6)
      }
    }
  }

  @ViewBuilder
  private func ticketsSection(_ tickets: [PushResponse.PushTicket]) -> some View {
    VStack(alignment: .leading, spacing: 8) {
      let successCount = tickets.filter { $0.status == "ok" }.count
      let errorCount = tickets.filter { $0.status == "error" }.count

      HStack(spacing: 8) {
        Image(systemName: errorCount == 0 ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
          .foregroundColor(errorCount == 0 ? .green : .orange)
          .font(.title2)
        Text(errorCount == 0 ? "All Delivered" : "\(successCount) OK, \(errorCount) Failed")
          .font(.title3)
          .fontWeight(.semibold)
        if let statusCode = httpStatusCode {
          Text("(\(statusCode))")
            .foregroundColor(.secondary)
        }
        Spacer()
      }

      ForEach(Array(tickets.enumerated()), id: \.offset) { index, ticket in
        VStack(alignment: .leading, spacing: 4) {
          HStack {
            Text("Ticket \(index + 1)")
              .font(.subheadline)
              .fontWeight(.medium)
            Circle()
              .fill(ticket.status == "ok" ? Color.green : Color.red)
              .frame(width: 8, height: 8)
            Text(ticket.status)
              .font(.system(.caption, design: .monospaced))
            Spacer()
          }

          if let id = ticket.id {
            HStack(spacing: 4) {
              Text("ID:")
                .foregroundColor(.secondary)
                .font(.caption)
              Text(id)
                .font(.system(.caption, design: .monospaced))
                .textSelection(.enabled)
            }
          }

          if let message = ticket.message {
            Text(message)
              .font(.callout)
              .foregroundColor(.red)
              .textSelection(.enabled)
          }

          if let details = ticket.details {
            ForEach(Array(details.sorted(by: { $0.key < $1.key })), id: \.key) { key, value in
              HStack(spacing: 4) {
                Text("\(key):")
                  .foregroundColor(.secondary)
                  .font(.caption)
                Text(value)
                  .font(.system(.caption, design: .monospaced))
                  .textSelection(.enabled)
              }
            }
          }
        }
        .padding(10)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(ticket.status == "ok" ? Color.green.opacity(0.08) : Color.red.opacity(0.08))
        .cornerRadius(6)
      }
    }
  }

  @ViewBuilder
  private func rawJSONSection(_ json: String) -> some View {
    DisclosureGroup("Raw JSON") {
      ScrollView {
        Text(json)
          .font(.system(.caption, design: .monospaced))
          .textSelection(.enabled)
          .frame(maxWidth: .infinity, alignment: .leading)
      }
      .frame(maxHeight: 150)
      .padding(8)
      .background(Color(nsColor: .controlBackgroundColor))
      .cornerRadius(6)
    }
  }
}


================================================
FILE: QuickPush/Views/FCMConfigurationView.swift
================================================
//
//  FCMConfigurationView.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

struct FCMConfigurationView: View {
  @Bindable var config: FCMConfigStore

  private var isValid: Bool {
    !config.projectId.isEmpty && !config.clientEmail.isEmpty && config.hasServiceAccount
  }

  @State private var isExpanded: Bool = false

  init(config: FCMConfigStore) {
    self.config = config
    // Start collapsed if already configured, expanded if not yet set up
    _isExpanded = State(initialValue: !config.hasServiceAccount)
  }

  var body: some View {
    VStack(alignment: .leading, spacing: 8) {
      Button(action: { withAnimation { isExpanded.toggle() } }) {
        HStack {
          Text("FCM Configuration")
            .font(.subheadline)
            .fontWeight(.medium)
          Spacer()
          Circle()
            .fill(isValid ? Color.green : Color.orange)
            .frame(width: 8, height: 8)
          Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
            .foregroundColor(.secondary)
        }
      }
      .buttonStyle(.plain)

      if isExpanded {
        VStack(alignment: .leading, spacing: 8) {
          HStack {
            Text("Service Account:")
              .frame(width: 110, alignment: .leading)
            if let name = config.serviceAccountFilename, config.hasServiceAccount {
              Text(name)
                .foregroundColor(.secondary)
                .lineLimit(1)
                .truncationMode(.middle)
            } else {
              Text("No file selected")
                .foregroundColor(.secondary)
            }
            Spacer()
            Button("Browse...") {
              config.selectServiceAccountFile()
            }
            .controlSize(.small)
          }

          if let error = config.fileError {
            HStack(alignment: .top, spacing: 6) {
              Image(systemName: "exclamationmark.triangle.fill")
                .foregroundColor(.orange)
                .font(.caption)
              Text(error)
                .font(.caption)
                .foregroundColor(.orange)
                .fixedSize(horizontal: false, vertical: true)
            }
            .padding(8)
            .background(Color.orange.opacity(0.1))
            .cornerRadius(6)
          }

          HStack {
            Text("Project ID:")
              .frame(width: 110, alignment: .leading)
            TextField("e.g. my-app-12345", text: $config.projectId)
              .textFieldStyle(.roundedBorder)
          }

          HStack {
            Text("Client Email:")
              .frame(width: 110, alignment: .leading)
            TextField("service-account@project.iam.gserviceaccount.com", text: $config.clientEmail)
              .textFieldStyle(.roundedBorder)
              .foregroundColor(.secondary)
          }
        }
        .padding(.leading, 4)
      }
    }
  }
}


================================================
FILE: QuickPush/Views/FCMCurlCommandView.swift
================================================
//
//  FCMCurlCommandView.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

/// Sheet that displays a copyable FCM curl command.
struct FCMCurlCommandView: View {
  let curlCommand: String
  @Environment(\.dismiss) private var dismiss
  @State private var copied = false

  var body: some View {
    VStack(spacing: 12) {
      HStack {
        Text("cURL Command")
          .font(.headline)
        Spacer()
        Button("Close") { dismiss() }
          .keyboardShortcut(.cancelAction)
      }

      ScrollView {
        Text(curlCommand)
          .font(.system(.body, design: .monospaced))
          .textSelection(.enabled)
          .frame(maxWidth: .infinity, alignment: .leading)
          .padding(8)
      }
      .background(Color(nsColor: .controlBackgroundColor))
      .cornerRadius(8)
      .overlay(
        RoundedRectangle(cornerRadius: 8)
          .stroke(Color.secondary.opacity(0.2))
      )

      HStack {
        Button(copied ? "Copied!" : "Copy") {
          NSPasteboard.general.clearContents()
          NSPasteboard.general.setString(curlCommand, forType: .string)
          copied = true
          DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            copied = false
          }
        }
        .help("Copy curl command to clipboard")

        Spacer()

        Text("Replace <ACCESS_TOKEN> with a valid OAuth 2.0 bearer token")
          .font(.caption)
          .foregroundColor(.secondary)
      }
    }
    .padding()
    .frame(minWidth: 500, minHeight: 300)
  }
}


================================================
FILE: QuickPush/Views/FCMResponseDetailView.swift
================================================
//
//  FCMResponseDetailView.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

/// Sheet that displays the full FCM response diagnostics after a send attempt.
struct FCMResponseDetailView: View {
  let response: FCMResponse?
  @Environment(\.dismiss) private var dismiss

  var body: some View {
    VStack(spacing: 12) {
      HStack {
        Text("FCM Response")
          .font(.headline)
        Spacer()
        Button("Close") { dismiss() }
          .keyboardShortcut(.cancelAction)
      }

      if let response {
        VStack(alignment: .leading, spacing: 0) {
          // Status header
          HStack(spacing: 8) {
            Image(systemName: response.isSuccess ? "checkmark.circle.fill" : "xmark.circle.fill")
              .foregroundColor(response.isSuccess ? .green : .red)
              .font(.title2)
            Text(response.isSuccess ? "Push Accepted" : "Push Rejected")
              .font(.title3)
              .fontWeight(.semibold)
            Spacer()
          }
          .padding(.bottom, 12)

          Group {
            FCMResponseRow(label: "Status", value: "\(response.statusCode)")
            if let messageId = response.messageId {
              FCMResponseRow(label: "Message ID", value: messageId)
            }
            if let errorCode = response.errorCode {
              FCMResponseRow(label: "Error Code", value: errorCode, isError: true)
            }
            if let errorMessage = response.errorMessage {
              FCMResponseRow(label: "Error Message", value: errorMessage, isError: true)
            }
            FCMResponseRow(label: "Project ID", value: response.projectId)
            FCMResponseRow(label: "Timestamp", value: "\(response.timestamp) (Unix sec)")
          }
        }
        .padding()
        .background(Color(nsColor: .controlBackgroundColor))
        .cornerRadius(8)
        .overlay(
          RoundedRectangle(cornerRadius: 8)
            .stroke(Color.secondary.opacity(0.2))
        )

        // Troubleshooting tips
        if let errorCode = response.errorCode {
          fcmTroubleshootingTip(for: errorCode)
        } else if let errorMessage = response.errorMessage, !response.isSuccess {
          fcmTroubleshootingTip(for: errorMessage)
        }

        HStack {
          Button("Copy Diagnostics") {
            NSPasteboard.general.clearContents()
            NSPasteboard.general.setString(response.diagnosticDetails, forType: .string)
          }
          .help("Copy full diagnostic info to clipboard")
          Spacer()
        }
      } else {
        Text("No response yet. Send a push first.")
          .foregroundColor(.secondary)
      }

      Spacer()
    }
    .padding()
    .frame(minWidth: 420, minHeight: 300)
  }

  @ViewBuilder
  private func fcmTroubleshootingTip(for code: String) -> some View {
    let tip: String? = switch code {
    case "INVALID_ARGUMENT":
      "The request payload is invalid. Check that the FCM registration token is correct and the message payload is well-formed."
    case "NOT_FOUND", "UNREGISTERED":
      "The FCM registration token is no longer valid. The device may have uninstalled the app or the token has expired. Remove this token from your records."
    case "SENDER_ID_MISMATCH":
      "The token was registered with a different sender (project). Ensure you are using the correct Firebase project and service account."
    case "QUOTA_EXCEEDED":
      "Sending rate exceeded. Slow down your message sending rate and retry with exponential backoff."
    case "UNAVAILABLE":
      "The FCM service is temporarily unavailable. Retry with exponential backoff."
    case "INTERNAL":
      "An internal error occurred on the FCM server. Retry with exponential backoff."
    default:
      nil
    }

    if let tip {
      FCMTipBox(icon: "lightbulb.fill", color: .yellow, text: tip)
    }
  }
}

private struct FCMTipBox: View {
  let icon: String
  let color: Color
  let text: String

  var body: some View {
    HStack(alignment: .top, spacing: 8) {
      Image(systemName: icon)
        .foregroundColor(color)
      Text(.init(text))
        .font(.callout)
        .foregroundColor(.secondary)
    }
    .padding(10)
    .background(color.opacity(0.1))
    .cornerRadius(6)
  }
}

private struct FCMResponseRow: View {
  let label: String
  let value: String
  var isError: Bool = false

  var body: some View {
    HStack(alignment: .top) {
      Text(label)
        .foregroundColor(.secondary)
        .frame(width: 110, alignment: .trailing)
      Text(value)
        .fontWeight(isError ? .semibold : .regular)
        .foregroundColor(isError ? .red : .primary)
        .textSelection(.enabled)
      Spacer()
    }
    .padding(.vertical, 3)
  }
}


================================================
FILE: QuickPush/Views/FCMView.swift
================================================
//
//  FCMView.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI
import AppKit

struct FCMView: View {
  var isActive: Bool = true
  @Environment(WindowManager.self) var windowManager
  @State private var viewModel = FCMViewModel()

  var body: some View {
    VStack(spacing: 16) {
      // Title bar
      HStack {
        Text("FCM")
          .font(.headline)
        Spacer()
        if viewModel.lastResponse != nil {
          Button {
            viewModel.showResponseSheet = true
          } label: {
            HStack(spacing: 4) {
              Circle()
                .fill(viewModel.lastResponse?.isSuccess == true ? Color.green : Color.red)
                .frame(width: 6, height: 6)
              Text("Response")
            }
          }
          .controlSize(.small)
        }
        Button("cURL") {
          viewModel.curlCommand = viewModel.generateCurlCommand()
          viewModel.showCurlSheet = true
        }
        .controlSize(.small)
        .disabled(!viewModel.canSend)
        Button {
          viewModel.send()
        } label: {
          HStack(spacing: 4) {
            Text("Send")
            HStack(spacing: 1) {
              Image(systemName: "command")
              Image(systemName: "return")
            }
            .font(.caption2)
            .opacity(0.7)
          }
        }
        .applying { view in
          if isActive {
            view.keyboardShortcut(.return, modifiers: .command)
          } else {
            view
          }
        }
        .applying { view in
          if #available(macOS 26.0, *) {
            view.buttonStyle(.glassProminent)
          } else {
            view.buttonStyle(.borderedProminent)
          }
        }
        .disabled(!viewModel.canSend)
      }

      ScrollView(.vertical, showsIndicators: false) {
        VStack(alignment: .leading, spacing: 16) {
          // Token Section
          VStack(alignment: .leading, spacing: 8) {
            Text("FCM Registration Token:")
              .font(.subheadline)

            // Saved tokens
            ForEach(viewModel.savedTokens) { saved in
              SavedTokenRowView(
                savedToken: saved,
                onToggle: {
                  if let index = viewModel.savedTokens.firstIndex(where: { $0.id == saved.id }) {
                    viewModel.savedTokens[index].isEnabled.toggle()
                    SavedTokenStore.fcm.saveTokens(viewModel.savedTokens)
                  }
                },
                onCopy: {
                  NSPasteboard.general.clearContents()
                  NSPasteboard.general.setString(saved.token, forType: .string)
                  viewModel.showToastMessage("Token copied to clipboard", type: .success)
                },
                onRemove: {
                  SavedTokenStore.fcm.removeToken(id: saved.id)
                  viewModel.savedTokens.removeAll { $0.id == saved.id }
                }
              )
            }

            // Unsaved token rows
            ForEach(viewModel.tokens.indices, id: \.self) { index in
              HStack(spacing: 8) {
                TextField("FCM registration token", text: $viewModel.tokens[index])
                  .textFieldStyle(.roundedBorder)

                Button(action: { viewModel.pasteToken(at: index) }) {
                  Image(systemName: "doc.on.clipboard")
                    .foregroundColor(.secondary)
                }
                .buttonStyle(.plain)
                .focusable(false)
                .help("Paste token from clipboard")

                Button(action: {
                  viewModel.tokenToSave = TokenToSave(index: index, token: viewModel.tokens[index])
                }) {
                  Image(systemName: "square.and.arrow.down")
                    .foregroundColor(.secondary)
                }
                .buttonStyle(.plain)
                .focusable(false)
                .disabled(viewModel.tokens[index].trimmingCharacters(in: .whitespaces).isEmpty)
                .help("Save this token for future sessions")

                if viewModel.tokens.count > 1 || !viewModel.savedTokens.isEmpty {
                  Button(action: { viewModel.tokens.remove(at: index) }) {
                    Image(systemName: "minus.circle.fill")
                      .foregroundColor(.red)
                  }
                  .buttonStyle(.plain)
                  .focusable(false)
                }
              }
            }

            Button(action: { viewModel.tokens.append("") }) {
              HStack {
                Image(systemName: "plus.circle.fill")
                Text("Add Token")
              }
            }
            .buttonStyle(.borderless)
            .padding(.top, 5)
          }

          // FCM Configuration
          FCMConfigurationView(config: viewModel.config)

          Divider()

          // Message Type Picker
          HStack {
            Text("Message Type:")
            Picker("", selection: $viewModel.messageType) {
              Text("Notification").tag(FCMMessageType.notification)
              Text("Data").tag(FCMMessageType.data)
            }
            .pickerStyle(.segmented)
            HelpButton(helpText: "Notification — displays a visible notification on the device.\nData — delivers a data payload to the app without showing a notification UI.")
          }

          // Notification Fields
          if viewModel.messageType == .notification {
            VStack(alignment: .leading, spacing: 8) {
              InputField(label: "Title", text: $viewModel.title, helpText: "Title of the notification")
              InputField(label: "Body", text: $viewModel.body, helpText: "Main message content")
              InputField(label: "Image URL", text: $viewModel.imageUrl, helpText: "URL of an image to display in the notification")

              Text("Android")
                .font(.subheadline)
                .fontWeight(.medium)
                .padding(.top, 4)
              InputField(label: "Channel ID", text: $viewModel.channelId, helpText: "Android notification channel ID. The app must create this channel. Use \"default\" for the default channel.")
              InputField(label: "Sound", text: $viewModel.sound, helpText: "Sound to play. Use \"default\" for the device default sound.")
              // Color row: hex input + presets + color picker
              HStack(spacing: 6) {
                Text("Color:")
                TextField("e.g. FF5733", text: $viewModel.color)
                  .textFieldStyle(.roundedBorder)
                ForEach(["FF3B30", "FF9500", "007AFF"], id: \.self) { preset in
                  Circle()
                    .fill(Color(hex: preset))
                    .frame(width: 18, height: 18)
                    .overlay(
                      Circle()
                        .stroke(
                          viewModel.color.uppercased() == preset
                            ? Color.primary.opacity(0.7) : Color.clear,
                          lineWidth: 2
                        )
                        .padding(-2)
                    )
                    .onTapGesture { viewModel.color = preset }
                }
                FCMColorWell(hex: $viewModel.color)
                  .frame(width: 22, height: 22)
                  .padding(.leading, 4)
                HelpButton(helpText: "Hex color that tints the notification's small icon in the status bar (e.g. FF5733 — no # prefix). This affects the icon accent color, not the notification background. Visibility depends on Android version and device.")
              }
            }
          }

          // Priority
          HStack {
            Text("Priority:")
            Picker("", selection: $viewModel.priority) {
              Text("HIGH").tag("HIGH")
              Text("NORMAL").tag("NORMAL")
            }
            .pickerStyle(.segmented)
            HelpButton(helpText: "HIGH — wakes the device immediately (use for user-visible notifications).\nNORMAL — may be delayed for battery optimization.")
          }

          // Custom Data
          KeyValueInputView(data: $viewModel.customData)
        }
      }
    }
    .padding(.horizontal)
    .padding(.top)
    .overlay(
      ToastView(message: viewModel.toastMessage, type: viewModel.toastType, isPresented: $viewModel.showToast)
        .animation(.easeInOut, value: viewModel.showToast)
    )
    .adaptivePresentation(isPresented: $viewModel.showResponseSheet, isPinned: windowManager.isPinned) {
      FCMResponseDetailView(response: viewModel.lastResponse)
    }
    .adaptivePresentation(isPresented: $viewModel.showCurlSheet, isPinned: windowManager.isPinned) {
      FCMCurlCommandView(curlCommand: viewModel.curlCommand)
    }
    .adaptivePresentation(item: $viewModel.tokenToSave, isPinned: windowManager.isPinned) { item in
      SaveTokenSheet(
        token: item.token,
        onSave: { label in
          let savedToken = SavedToken(label: label, token: item.token)
          SavedTokenStore.fcm.addToken(savedToken)
          viewModel.savedTokens.append(savedToken)
          if item.index < viewModel.tokens.count {
            viewModel.tokens.remove(at: item.index)
          }
          if viewModel.tokens.isEmpty {
            viewModel.tokens.append("")
          }
        },
        warningText: "FCM registration tokens may change when the user reinstalls the app or clears app data."
      )
    }
  }
}

// MARK: - Native color well (renders as a square swatch, opens system color panel)

private struct FCMColorWell: NSViewRepresentable {
  @Binding var hex: String

  func makeNSView(context: Context) -> NSColorWell {
    let well = NSColorWell()
    well.color = NSColor(Color(hex: hex))
    well.target = context.coordinator
    well.action = #selector(Coordinator.colorChanged(_:))
    return well
  }

  func updateNSView(_ well: NSColorWell, context: Context) {
    // Only update when the hex actually differs to avoid infinite loops
    if Color(nsColor: well.color).toHexString() != hex.uppercased() {
      well.color = NSColor(Color(hex: hex))
    }
  }

  func makeCoordinator() -> Coordinator { Coordinator(hex: $hex) }

  final class Coordinator: NSObject {
    @Binding var hex: String
    init(hex: Binding<String>) { _hex = hex }

    @objc func colorChanged(_ sender: NSColorWell) {
      hex = Color(nsColor: sender.color).toHexString()
    }
  }
}


================================================
FILE: QuickPush/Views/FooterView.swift
================================================
//
//  FooterView.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI
import LaunchAtLogin

/// Footer shown at the bottom of the app with launch-at-login toggle,
/// pin/unpin button, quit button, and attribution.
struct FooterView: View {
  @Environment(WindowManager.self) var windowManager
  @State private var showCopied = false

  private var appVersion: String {
    Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "–"
  }

  private let appStoreURL = URL(string: "https://apple.co/4tvT4wF")!

  var body: some View {
    VStack(spacing: 6) {
      // Main row: launch at login, utility icons, pin
      HStack(spacing: 12) {
        LaunchAtLogin.Toggle()

        Spacer()

        Button {
          NSPasteboard.general.clearContents()
          NSPasteboard.general.setString(appStoreURL.absoluteString, forType: .string)
          showCopied = true
          DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            showCopied = false
          }
        } label: {
          Image(systemName: showCopied ? "checkmark" : "square.and.arrow.up")
            .animation(.easeInOut, value: showCopied)
        }
        .help("Share QuickPush — copies App Store link to clipboard")

        Link(destination: URL(string: "https://github.com/betomoedano/quick-push")!) {
          Image(systemName: "book.closed")
        }
        .help("Open documentation on GitHub")

        Button {
          NSWorkspace.shared.open(appStoreURL)
        } label: {
          Image(systemName: "arrow.triangle.2.circlepath")
        }
        .help("Check for updates on the App Store")

        Button {
          windowManager.togglePin()
        } label: {
          Image(systemName: windowManager.isPinned ? "pin.slash.fill" : "pin.fill")
        }
        .help(windowManager.isPinned ? "Unpin Window" : "Pin Window (⌘⇧P)")
      }
      .buttonStyle(.borderless)
      .imageScale(.medium)
      .foregroundStyle(.secondary)

      // Bottom row: version left, attribution right
      HStack {
        Text("v\(appVersion)")
          .foregroundStyle(.tertiary)
        Spacer()
        HStack(spacing: 0) {
          Text("Made with ❤️ by ")
          Link("codewithbeto.dev", destination: URL(string: "https://codewithbeto.dev")!)
            .foregroundColor(.blue)
        }
      }
      .font(.caption)
    }
    .padding(.horizontal)
    .padding(.vertical, 8)
  }
}


================================================
FILE: QuickPush/Views/JSONImportExportView.swift
================================================
//
//  JSONImportExportView.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

struct JSONImportExportView: View {
  @Bindable var viewModel: LiveActivityViewModel
  @State private var jsonText: String = ""
  @Environment(\.dismiss) private var dismiss

  var body: some View {
    VStack(spacing: 12) {
      HStack {
        Text("JSON Payload")
          .font(.headline)
        Spacer()
        Button("Close") { dismiss() }
          .keyboardShortcut(.cancelAction)
      }

      TextEditor(text: $jsonText)
        .font(.system(.body, design: .monospaced))
        .frame(minHeight: 250)
        .border(Color.secondary.opacity(0.3))

      HStack {
        Button("Export") {
          jsonText = viewModel.exportJSON()
        }
        .help("Generate JSON from current form values")

        Button("Copy") {
          NSPasteboard.general.clearContents()
          NSPasteboard.general.setString(jsonText, forType: .string)
        }
        .help("Copy JSON to clipboard")
        .disabled(jsonText.isEmpty)

        Spacer()

        Button("Paste") {
          if let clipboardString = NSPasteboard.general.string(forType: .string) {
            jsonText = clipboardString
          }
        }
        .help("Paste JSON from clipboard")

        Button("Import") {
          if viewModel.importJSON(jsonText) {
            dismiss()
          }
        }
        .help("Apply JSON to form fields")
        .disabled(jsonText.isEmpty)
      }
    }
    .padding()
    .frame(minWidth: 400, minHeight: 350)
    .onAppear {
      jsonText = viewModel.exportJSON()
    }
  }
}


================================================
FILE: QuickPush/Views/KeyValueInputView.swift
================================================
//
//  KeyValueInputView.swift
//  QuickPush
//
//  Created by beto on 2/20/25.
//

import SwiftUI

struct KeyValueInputView: View {
    @Binding var data: [String: String]
    @State private var editingKeys: [String: String] = [:] // Temporary key storage

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Text("Custom Data:")
                    .font(.subheadline)
                HelpButton(helpText: "Add custom JSON data to be sent with the notification. Up to 4KB total payload size.")
            }
            
            if data.isEmpty {
                Text("No custom data")
                    .foregroundColor(.secondary)
                    .font(.subheadline)
                    .padding(.vertical, 4)
            }
            
            ForEach(Array(data.keys), id: \.self) { key in
                HStack(spacing: 8) {
                    // Key Input
                    TextField("Key", text: Binding(
                        get: { editingKeys[key] ?? key },
                        set: { newKey in
                            let sanitizedKey = sanitizeKey(newKey)
                            editingKeys[key] = sanitizedKey
                        }
                    ))
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .frame(width: 120)
                    .onSubmit {
                        finalizeKeyEdit(oldKey: key)
                    }

                    Text(":")
                        .foregroundColor(.secondary)
                    
                    // Value Input
                    TextField("Value", text: Binding(
                        get: { data[key] ?? "" },
                        set: { newValue in data[key] = newValue }
                    ))
                    .textFieldStyle(RoundedBorderTextFieldStyle())

                    // Remove Button
                    Button(action: { removeKey(key) }) {
                        Image(systemName: "minus.circle.fill")
                            .foregroundColor(.red)
                    }
                    .buttonStyle(PlainButtonStyle())
                    .help("Remove this key-value pair")
                }
            }
            
            // Add Button
            Button(action: { addNewKeyValue() }) {
                HStack {
                    Image(systemName: "plus.circle.fill")
                    Text("Add Key-Value Pair")
                }
            }
            .buttonStyle(.borderless)
            .disabled(data.count >= 10) // Prevent too many items
            .help(data.count >= 10 ? "Maximum number of custom data pairs reached" : "Add a new key-value pair")
            .padding(.top, 4)
        }
        .padding(.vertical, 4)
    }
    
    // MARK: - Helper Functions
    
    /// Sanitizes a key: lowercase, replaces spaces with "-", removes invalid characters
    private func sanitizeKey(_ key: String) -> String {
        let allowedCharacters = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-"))
        return key
            .lowercased()
            .replacingOccurrences(of: " ", with: "-") // Replace spaces with "-"
            .components(separatedBy: allowedCharacters.inverted) // Remove special characters
            .joined()
    }

    /// Finalizes key edit: prevents empty or duplicate keys
    private func finalizeKeyEdit(oldKey: String) {
        if let newKey = editingKeys[oldKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
           !newKey.isEmpty,
           newKey != oldKey,
           !data.keys.contains(newKey) { // Prevent duplicate keys
            if let value = data.removeValue(forKey: oldKey) {
                data[newKey] = value
            }
        }
        editingKeys[oldKey] = nil // Clear temp key storage
    }

    /// Adds a new key-value pair with a default key
    private func addNewKeyValue() {
        guard data.count < 10 else { return } // Safety check
        
        let baseKey = "key"
        var newKey = baseKey
        var counter = 1

        while data.keys.contains(newKey) {
            newKey = "\(baseKey)\(counter)"
            counter += 1
        }

        data[newKey] = ""
        editingKeys[newKey] = newKey
    }

    /// Removes a key-value pair
    private func removeKey(_ key: String) {
        data.removeValue(forKey: key)
        editingKeys.removeValue(forKey: key)
    }
}


================================================
FILE: QuickPush/Views/LiveActivityAlertSection.swift
================================================
//
//  LiveActivityAlertSection.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

struct LiveActivityAlertSection: View {
  @Bindable var viewModel: LiveActivityViewModel

  var body: some View {
    VStack(alignment: .leading, spacing: 8) {
      Toggle("Include Alert", isOn: $viewModel.includeAlert)

      if viewModel.includeAlert {
        VStack(alignment: .leading, spacing: 8) {
          HStack {
            Text("Alert Title:")
            TextField("Alert title", text: $viewModel.alertTitle)
              .textFieldStyle(.roundedBorder)
          }

          HStack {
            Text("Alert Body:")
            TextField("Alert body", text: $viewModel.alertBody)
              .textFieldStyle(.roundedBorder)
          }

          HStack {
            Text("Sound:")
            TextField("default", text: $viewModel.alertSound)
              .textFieldStyle(.roundedBorder)
          }
        }
        .padding(.leading, 16)
      }
    }
  }
}


================================================
FILE: QuickPush/Views/LiveActivityAttributesSection.swift
================================================
//
//  LiveActivityAttributesSection.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

struct LiveActivityAttributesSection: View {
  @Bindable var viewModel: LiveActivityViewModel

  var body: some View {
    VStack(alignment: .leading, spacing: 8) {
      Text("Attributes")
        .font(.subheadline)
        .fontWeight(.medium)

      HStack {
        Text("Name:")
        TextField("Attribute name", text: $viewModel.attributeName)
          .textFieldStyle(.roundedBorder)
      }

      // Color Pickers
      ColorPickerField(label: "Background", color: $viewModel.backgroundColor)
      ColorPickerField(label: "Title Color", color: $viewModel.titleColor)
      ColorPickerField(label: "Subtitle Color", color: $viewModel.subtitleColor)
      ColorPickerField(label: "Progress Tint", color: $viewModel.progressViewTint)
      ColorPickerField(label: "Progress Label", color: $viewModel.progressViewLabelColor)

      HStack {
        Text("Deep Link URL:")
        TextField("e.g. /dashboard", text: $viewModel.deepLinkUrl)
          .textFieldStyle(.roundedBorder)
      }

      // Timer Type
      HStack {
        Text("Timer Type:")
        Picker("", selection: $viewModel.timerType) {
          Text("Digital").tag("digital")
          Text("Circular").tag("circular")
        }
        .pickerStyle(.segmented)
      }

      // Image Position
      HStack {
        Text("Image Position:")
        Picker("", selection: $viewModel.imagePosition) {
          Text("Left").tag("left")
          Text("Right").tag("right")
        }
        .pickerStyle(.segmented)
      }

      // Image Size
      HStack {
        Text("Image Size:")
        TextField("W", value: $viewModel.imageWidth, format: .number)
          .textFieldStyle(.roundedBorder)
          .frame(width: 60)
        Text("x")
        TextField("H", value: $viewModel.imageHeight, format: .number)
          .textFieldStyle(.roundedBorder)
          .frame(width: 60)
      }

      // Padding
      Toggle("Custom Padding", isOn: $viewModel.useCustomPadding)
      if viewModel.useCustomPadding {
        HStack {
          Text("T:")
          TextField("", value: $viewModel.paddingTop, format: .number)
            .textFieldStyle(.roundedBorder)
            .frame(width: 45)
          Text("B:")
          TextField("", value: $viewModel.paddingBottom, format: .number)
            .textFieldStyle(.roundedBorder)
            .frame(width: 45)
          Text("L:")
          TextField("", value: $viewModel.paddingLeft, format: .number)
            .textFieldStyle(.roundedBorder)
            .frame(width: 45)
          Text("R:")
          TextField("", value: $viewModel.paddingRight, format: .number)
            .textFieldStyle(.roundedBorder)
            .frame(width: 45)
        }
        .padding(.leading, 16)
      } else {
        HStack {
          Text("Padding:")
          TextField("", value: $viewModel.uniformPadding, format: .number)
            .textFieldStyle(.roundedBorder)
            .frame(width: 60)
        }
      }
    }
  }
}


================================================
FILE: QuickPush/Views/LiveActivityContentStateSection.swift
================================================
//
//  LiveActivityContentStateSection.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

struct LiveActivityContentStateSection: View {
  @Bindable var viewModel: LiveActivityViewModel

  var body: some View {
    VStack(alignment: .leading, spacing: 8) {
      Text("Content State")
        .font(.subheadline)
        .fontWeight(.medium)

      HStack {
        Text("Title:")
        TextField("Activity title", text: $viewModel.contentTitle)
          .textFieldStyle(.roundedBorder)
      }

      HStack {
        Text("Subtitle:")
        TextField("Activity subtitle", text: $viewModel.contentSubtitle)
          .textFieldStyle(.roundedBorder)
      }

      // Progress
      Toggle("Include Progress", isOn: $viewModel.includeProgress)
      if viewModel.includeProgress {
        HStack {
          Text("Progress:")
          Slider(value: $viewModel.progress, in: 0...1, step: 0.01)
          Text("\(Int(viewModel.progress * 100))%")
            .frame(width: 40, alignment: .trailing)
            .font(.system(.body, design: .monospaced))
        }
        .padding(.leading, 16)
      }

      // Timer End Date
      Toggle("Include Timer End Date", isOn: $viewModel.includeTimerEnd)
      if viewModel.includeTimerEnd {
        HStack {
          Text("Timer End:")
          DatePicker("", selection: $viewModel.timerEndDate)
            .labelsHidden()
        }
        .padding(.leading, 16)
      }

      HStack {
        Text("Image Name:")
        TextField("Optional image name", text: $viewModel.imageName)
          .textFieldStyle(.roundedBorder)
      }

      HStack {
        Text("DI Image:")
        TextField("Dynamic Island image", text: $viewModel.dynamicIslandImageName)
          .textFieldStyle(.roundedBorder)
      }
    }
  }
}


================================================
FILE: QuickPush/Views/LiveActivityView.swift
================================================
//
//  LiveActivityView.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

struct LiveActivityView: View {
  var isActive: Bool = true
  @Environment(WindowManager.self) var windowManager
  @State private var viewModel = LiveActivityViewModel()

  var body: some View {
    VStack(spacing: 16) {
      // Title bar
      HStack {
        Text("Live Activity")
          .font(.headline)
        Spacer()
        if viewModel.lastResponse != nil {
          Button {
            viewModel.showResponseSheet = true
          } label: {
            HStack(spacing: 4) {
              Circle()
                .fill(viewModel.lastResponse?.isSuccess == true ? Color.green : Color.red)
                .frame(width: 6, height: 6)
              Text("Response")
            }
          }
          .controlSize(.small)
        }
        Button("JSON") {
          viewModel.showJSONSheet = true
        }
        .controlSize(.small)
        Button {
          viewModel.send()
        } label: {
          HStack(spacing: 4) {
            Text("Send")
            HStack(spacing: 1) {
              Image(systemName: "command")
              Image(systemName: "return")
            }
            .font(.caption2)
            .opacity(0.7)
          }
        }
        .applying { view in
          if isActive {
            view.keyboardShortcut(.return, modifiers: .command)
          } else {
            view
          }
        }
        .applying { view in
          if #available(macOS 26.0, *) {
            view.buttonStyle(.glassProminent)
          } else {
            view.buttonStyle(.borderedProminent)
          }
        }
        .disabled(!viewModel.canSend)
      }

      ScrollView(.vertical, showsIndicators: false) {
        VStack(alignment: .leading, spacing: 16) {
          // Event Type Picker
          HStack {
            Text("Event:")
            Picker("", selection: $viewModel.eventType) {
              Text("Start").tag(LiveActivityEvent.start)
              Text("Update").tag(LiveActivityEvent.update)
              Text("End").tag(LiveActivityEvent.end)
            }
            .pickerStyle(.segmented)
          }

          // Token Field
          VStack(alignment: .leading, spacing: 8) {
            Text("\(viewModel.tokenLabel):")
              .font(.subheadline)
            HStack {
              TextField("Hex device token", text: $viewModel.deviceToken)
                .textFieldStyle(.roundedBorder)
              Button(action: { viewModel.pasteToken() }) {
                Image(systemName: "doc.on.clipboard")
                  .foregroundColor(.secondary)
              }
              .buttonStyle(.plain)
              .help("Paste hex token from clipboard")
            }
          }

          Divider()

          // APNs Configuration
          APNsConfigurationView(config: viewModel.config)

          HStack {
            Text("Attributes Type:")
              .frame(width: 95, alignment: .leading)
            TextField("e.g. MyWidget.LiveActivityAttributes", text: $viewModel.attributesType)
              .textFieldStyle(.roundedBorder)
            HelpButton(helpText: "Must match the fully module-qualified Swift type name of your ActivityAttributes struct. Run this in your iOS app to get the exact value:\n\nprint(String(reflecting: LiveActivityAttributes.self))\n\nCommon formats:\n• MyWidgetExtension.LiveActivityAttributes\n• LiveActivityAttributes\n\nA mismatch causes iOS to silently drop the push even if APNs returns 200.")
          }

          Divider()

          // Content State
          LiveActivityContentStateSection(viewModel: viewModel)

          // Attributes (Start only)
          if viewModel.eventType == .start {
            Divider()
            LiveActivityAttributesSection(viewModel: viewModel)
          }

          // Alert (Start only)
          if viewModel.eventType == .start {
            Divider()
            LiveActivityAlertSection(viewModel: viewModel)
          }
        }
      }
    }
    .padding(.horizontal)
    .padding(.top)
    .overlay(
      ToastView(message: viewModel.toastMessage, type: viewModel.toastType, isPresented: $viewModel.showToast)
        .animation(.easeInOut, value: viewModel.showToast)
    )
    .adaptivePresentation(isPresented: $viewModel.showJSONSheet, isPinned: windowManager.isPinned) {
      JSONImportExportView(viewModel: viewModel)
    }
    .adaptivePresentation(isPresented: $viewModel.showResponseSheet, isPinned: windowManager.isPinned) {
      APNsResponseDetailView(response: viewModel.lastResponse)
    }
  }
}


================================================
FILE: QuickPush/Views/MainContentView.swift
================================================
//
//  MainContentView.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

/// Shared root view used by both the MenuBarExtra popover and the floating panel.
struct MainContentView: View {
  @Environment(WindowManager.self) var windowManager

  var body: some View {
    VStack {
      ContentView()
      Divider()
      FooterView()
    }
    .frame(minWidth: 570)
  }
}


================================================
FILE: QuickPush/Views/PushNotificationView.swift
================================================
//
//  PushNotificationView.swift
//  QuickPush
//
//  Created by beto on 2/8/26.
//

import SwiftUI

struct PushNotificationView: View {
  var isActive: Bool = true
  @Environment(WindowManager.self) var windowManager
  @State private var tokens: [String] = [""]
  @State private var accessToken: String = ""
  @State private var title: String = ""
  @State private var notificationBody: String = ""
  @State private var sound: String = "default"
  @State private var priority: PushNotification.Priority = .default
  @State private var ttl: String = ""
  @State private var expiration: String = ""
  @State private var data: [String: String] = [:]
  @State private var showTitleError: Bool = false

  // Advanced fields
  @State private var showAdvancedSettings: Bool = true
  @State private var subtitle: String = ""
  @State private var badge: String = ""
  @State private var interruptionLevel: PushNotification.InterruptionLevel = .active
  @State private var channelId: String = ""
  @State private var categoryId: String = ""
  @State private var mutableContent: Bool = false
  @State private var contentAvailable: Bool = false
  @State private var imageUrl: String = ""

  // Toast notification state
  @State private var showToast: Bool = false
  @State private var toastMessage: String = ""
  @State private var toastType: ToastType = .success

  // Response & cURL sheet state
  @State private var lastResponse: PushResponse?
  @State private var lastHttpStatusCode: Int?
  @State private var lastRawJSON: String?
  @State private var showResponseSheet: Bool = false
  @State private var showCurlSheet: Bool = false
  @State private var showReceiptSheet: Bool = false

  // Saved tokens state
  @State private var savedTokens: [SavedToken] = []
  @State private var tokenToSave: TokenToSave? = nil

  var body: some View {
    VStack {
      // Title and Send Button
      HStack {
        Text("Expo Notification")
          .font(.headline)
        Spacer()
        if lastResponse != nil {
          Button {
            showResponseSheet = true
          } label: {
            HStack(spacing: 4) {
              Circle()
                .fill(responseHasErrors ? Color.red : Color.green)
                .frame(width: 6, height: 6)
              Text("Response")
            }
          }
          .controlSize(.small)
        }
        if !lastTicketIds.isEmpty {
          Button("Receipt") {
            showReceiptSheet = true
          }
          .controlSize(.small)
        }
        Button("cURL") {
          showCurlSheet = true
        }
        .controlSize(.small)
        .disabled(allValidTokens.isEmpty || title.isEmpty)
        Button {
          sendPushNotification()
        } label: {
          HStack(spacing: 4) {
            Text("Send Push")
            HStack(spacing: 1) {
              Image(systemName: "command")
              Image(systemName: "return")
            }
            .font(.caption2)
            .opacity(0.7)
          }
        }
        .applying { view in
          if isActive {
            view.keyboardShortcut(.return, modifiers: .command)
          } else {
            view
          }
        }
        .applying { view in
          if #available(macOS 26.0, *) {
            view.buttonStyle(.glassProminent)
          } else {
            view.buttonStyle(.borderedProminent)
          }
        }
        .disabled(allValidTokens.isEmpty)
      }

      // Main Content
      ScrollView(.vertical, showsIndicators: false) {
        VStack(alignment: .leading, spacing: 12) {
          // Basic Fields Section
          VStack(alignment: .leading, spacing: 12) {
            // Tokens Section
            Text("Expo Push Tokens:")
              .font(.subheadline)

            // Saved tokens (compact display)
            ForEach(savedTokens) { saved in
              SavedTokenRowView(
                savedToken: saved,
                onToggle: {
                  if let index = savedTokens.firstIndex(where: { $0.id == saved.id }) {
                    savedTokens[index].isEnabled.toggle()
                    SavedTokenStore.shared.saveTokens(savedTokens)
                  }
                },
                onCopy: {
                  NSPasteboard.general.clearContents()
                  NSPasteboard.general.setString(saved.token, forType: .string)
                  showToastNotification(message: "Token copied to clipboard", type: .success)
                },
                onRemove: {
                  SavedTokenStore.shared.removeToken(id: saved.id)
                  savedTokens.removeAll { $0.id == saved.id }
                }
              )
            }

            // Unsaved token rows (text fields)
            ForEach(tokens.indices, id: \.self) { index in
              HStack(spacing: 8) {
                TextField("e.g. ExponentPushToken[N1QHiEF4mnLGP8HeQrj9AR]", text: $tokens[index])
                  .textFieldStyle(RoundedBorderTextFieldStyle())
                  .padding(.leading, 4)

                // Paste from clipboard
                Button(action: {
                  if let clipboardString = NSPasteboard.general.string(forType: .string) {
                    let trimmed = clipboardString.trimmingCharacters(in: .whitespacesAndNewlines)
                    if trimmed.count <= 41 && trimmed.starts(with: "ExponentPushToken[") && trimmed.hasSuffix("]") {
                      tokens[index] = trimmed
                    } else {
                      showToastNotification(message: "Token should be in format: ExponentPushToken[xxxxxxxxxxxxxxxxxxxxx]", type: .error)
                    }
                  }
                }) {
                  Image(systemName: "doc.on.clipboard")
                    .foregroundColor(.secondary)
                }
                .buttonStyle(.plain)
                .help("Paste from clipboard (ExponentPushToken[xxxxxxxxxxxxxxxxxxxxx])")

                // Save token for future sessions
                Button(action: {
                  tokenToSave = TokenToSave(index: index, token: tokens[index])
                }) {
                  Image(systemName: "square.and.arrow.down")
                    .foregroundColor(.secondary)
                }
                .buttonStyle(.plain)
                .disabled(tokens[index].trimmingCharacters(in: .whitespaces).isEmpty)
                .help("Save this token for future sessions")

                // Delete token row
                if tokens.count > 1 || !savedTokens.isEmpty {
                  Button(action: { tokens.remove(at: index) }) {
                    Image(systemName: "minus.circle.fill")
                      .foregroundColor(.red)
                  }
                  .buttonStyle(PlainButtonStyle())
                }
              }
            }

            Button(action: { tokens.append("") }) {
              HStack {
                Image(systemName: "plus.circle.fill")
                Text("Add Token")
              }
            }
            .buttonStyle(.borderless)
            .padding(.top, 5)

            // Access Token Section
            VStack(alignment: .leading, spacing: 8) {
              HStack {
                Text("Access Token (Optional):")
                  .font(.subheadline)
                HelpButton(helpText: "Enhanced push security token. Required if you've enabled push security in your EAS Dashboard.")
              }

              HStack {
                SecureField("Access token for enhanced security", text: $accessToken)
                  .textFieldStyle(RoundedBorderTextFieldStyle())
                  .padding(.leading, 4)

                Button(action: {
                  if let clipboardString = NSPasteboard.general.string(forType: .string) {
                    let trimmed = clipboardString.trimmingCharacters(in: .whitespacesAndNewlines)
                    accessToken = trimmed
                  }
                }) {
                  Image(systemName: "doc.on.clipboard")
                    .foregroundColor(.secondary)
                }
                .buttonStyle(.plain)
                .help("Paste access token from clipboard")
              }
            }

            Divider()

            // Basic Notification Fields
            VStack(alignment: .leading, spacing: 8) {
              InputField(label: "Title", text: $title, helpText: "Title of the notification", isRequired: true, showError: showTitleError)
              InputField(label: "Body", text: $notificationBody, helpText: "Message content displayed in the notification")

              // Priority Picker
              HStack {
                Text("Priority:")
                Picker("", selection: $priority) {
                  Text("Default").tag(PushNotification.Priority.default)
                  Text("Normal").tag(PushNotification.Priority.normal)
                  Text("High").tag(PushNotification.Priority.high)
                }
                .pickerStyle(SegmentedPickerStyle())

                HelpButton(helpText: "Affects delivery timing. 'High' wakes sleeping devices.")
              }

              Divider()
              KeyValueInputView(data: $data)
            }
          }

          // Advanced Settings Toggle
          Button(action: { showAdvancedSettings.toggle() }) {
            HStack {
              Text("Advanced Settings")
                .font(.subheadline)
              Spacer()
              Image(systemName: showAdvancedSettings ? "chevron.up" : "chevron.down")
            }
          }
          .buttonStyle(.borderless)

          if showAdvancedSettings {
            VStack(alignment: .leading, spacing: 12) {
              // iOS Specific Settings
              Group {
                Text("iOS Specific")
                  .font(.subheadline)
                  .foregroundColor(.secondary)

                InputField(label: "Sound", text: $sound, helpText: "Specify 'default' or custom sound name (iOS only)")
                InputField(label: "Subtitle", text: $subtitle, helpText: "Additional text below the title (iOS only)")
                InputField(label: "Badge", text: $badge, helpText: "Number to display on app icon (iOS only)")

                // Interruption Level Picker
                HStack {
                  Text("Interruption Level:")
                  Picker("", selection: $interruptionLevel) {
                    Text("Active").tag(PushNotification.InterruptionLevel.active)
                    Text("Critical").tag(PushNotification.InterruptionLevel.critical)
                    Text("Passive").tag(PushNotification.InterruptionLevel.passive)
                    Text("Time Sensitive").tag(PushNotification.InterruptionLevel.timeSensitive)
                  }
                  .pickerStyle(MenuPickerStyle())

                  HelpButton(helpText: "Controls the delivery timing and importance of the notification")
                }

                Toggle("Mutable Content", isOn: $mutableContent)
                  .help("Allows notification content modification by the app")

                Toggle("Content Available", isOn: $contentAvailable)
                  .help("Triggers background fetch on delivery")
              }

              Divider()

              // Android Specific Settings
              Group {
                Text("Android Specific")
                  .font(.subheadline)
                  .foregroundColor(.secondary)

                InputField(label: "Channel ID", text: $channelId, helpText: "Android notification channel identifier")
              }

              Divider()

              // Common Advanced Settings
              Group {
                Text("Common Settings")
                  .font(.subheadline)
                  .foregroundColor(.secondary)

                InputField(label: "Image (richContent)", text: $imageUrl, helpText: "URL of image to display in rich notification. Android shows it out of the box. iOS requires a Notification Service Extension — learn how in this free lesson: https://codewithbeto.dev/rnCourse/expoNotificationsExtension See also https://github.com/expo/expo/pull/36202")
                InputField(label: "Category ID", text: $categoryId, helpText: "Notification category for interactive notifications")
                InputField(label: "TTL", text: $ttl, helpText: "Time-to-live in seconds")
                InputField(label: "Expiration", text: $expiration, helpText: "Unix timestamp for expiration")
              }
            }
          }
        }
      }
    }
    .padding(.horizontal)
    .padding(.top)
    .overlay(
      ToastView(message: toastMessage, type: toastType, isPresented: $showToast)
        .animation(.easeInOut, value: showToast)
    )
    .adaptivePresentation(isPresented: $showResponseSheet, isPinned: windowManager.isPinned) {
      ExpoResponseDetailView(
        response: lastResponse,
        httpStatusCode: lastHttpStatusCode,
        rawJSON: lastRawJSON
      )
    }
    .adaptivePresentation(isPresented: $showCurlSheet, isPinned: windowManager.isPinned) {
      ExpoCurlCommandView(
        notification: buildNotification(),
        accessToken: accessToken.isEmpty ? nil : accessToken
      )
    }
    .adaptivePresentation(isPresented: $showReceiptSheet, isPinned: windowManager.isPinned) {
      ExpoReceiptView(
        ticketIds: lastTicketIds,
        accessToken: accessToken.isEmpty ? nil : accessToken
      )
    }
    .adaptivePresentation(item: $tokenToSave, isPinned: windowManager.isPinned) { item in
      SaveTokenSheet(token: item.token) { label in
        let savedToken = SavedToken(label: label, token: item.token)
        SavedTokenStore.shared.addToken(savedToken)
        savedTokens.append(savedToken)

        // Move from unsaved to saved
        if item.index < tokens.count {
          tokens.remove(at: item.index)
        }
        if tokens.isEmpty {
          tokens.append("")
        }
      }
    }
    .onAppear {
      savedTokens = SavedTokenStore.shared.loadTokens()
    }
  }

  /// All valid tokens from both saved (enabled only) and unsaved sources.
  private var allValidTokens: [String] {
    savedTokens.filter(\.isEnabled).map(\.token) + tokens.filter { !$0.isEmpty }
  }

  /// Ticket IDs from the last successful send (status == "ok" tickets only).
  private var lastTicketIds: [String] {
    lastResponse?.data?.compactMap { $0.id } ?? []
  }

  /// Whether the last response contains errors (API-level or per-ticket).
  private var responseHasErrors: Bool {
    guard let response = lastResponse else { return false }
    if let errors = response.errors, !errors.isEmpty { return true }
    if let tickets = response.data, tickets.contains(where: { $0.status == "error" }) { return true }
    return false
  }

  /// Build a `PushNotification` from the current form values.
  private func buildNotification() -> PushNotification {
    let validTokens = allValidTokens
    let richContent: PushNotification.RichContent? = imageUrl.isEmpty ? nil : PushNotification.RichContent(image: imageUrl)

    return PushNotification(
      to: validTokens.isEmpty ? ["ExponentPushToken[...]"] : validTokens,
      title: title.isEmpty ? "Title" : title,
      body: notificationBody.isEmpty ? " " : notificationBody,
      data: data.isEmpty ? nil : data,
      ttl: Int(ttl),
      expiration: Int(expiration),
      priority: priority,
      subtitle: subtitle.isEmpty ? nil : subtitle,
      sound: sound.isEmpty ? nil : sound,
      badge: Int(badge),
      interruptionLevel: interruptionLevel,
      channelId: channelId.isEmpty ? nil : channelId,
      categoryId: categoryId.isEmpty ? nil : categoryId,
      mutableContent: mutableContent,
      contentAvailable: contentAvailable,
      richContent: richContent
    )
  }

  private func sendPushNotification() {
    let validTokens = allValidTokens

    guard !validTokens.isEmpty, !title.isEmpty else {
      showTitleError = title.isEmpty
      if title.isEmpty {
        showToastNotification(message: "Title is required", type: .error)
      }
      return
    }

    showTitleError = false

    let notification = buildNotification()

    PushNotificationService.shared.sendPushNotification(
      notification: notification,
      accessToken: accessToken.isEmpty ? nil : accessToken
    ) { result in
      DispatchQueue.main.async {
        switch result {
        case .success(let sendResult):
          lastResponse = sendResult.response
          lastHttpStatusCode = sendResult.httpStatusCode
          lastRawJSON = sendResult.rawJSON
          print("Push sent successfully: \(sendResult.response)")
          showToastNotification(message: "Push notification sent successfully!", type: .success)
        case .failure(let error):
          print("Failed to send push: \(error.localizedDescription)")
          showToastNotification(message: error.localizedDescription, type: .error)
        }
      }
    }
  }

  private func showToastNotification(message: String, type: ToastType) {
    toastMessage = message
    toastType = type
    showToast = true
  }
}



================================================
FILE: QuickPush/Views/SaveTokenSheet.swift
================================================
//
//  SaveTokenSheet.swift
//  QuickPush
//
//  Created by beto on 2/19/26.
//

import SwiftUI

/// Modal sheet for saving a push token with a user-defined label.
struct SaveTokenSheet: View {
  let token: String
  let onSave: (String) -> Void
  var warningText: String = "Expo push tokens may change when you reinstall the app on your device."
  @Environment(\.dismiss) private var dismiss
  @State private var label: String = ""

  var body: some View {
    VStack(spacing: 16) {
      // Header
      HStack {
        Text("Save Token")
          .font(.headline)
        Spacer(
Download .txt
gitextract_jnez51gj/

├── LICENSE
├── QuickPush/
│   ├── Assets.xcassets/
│   │   ├── AccentColor.colorset/
│   │   │   └── Contents.json
│   │   ├── Contents.json
│   │   └── QuickPushIcon/
│   │       ├── Assets/
│   │       │   ├── Contents.json
│   │       │   └── bolt.brakesignal 1.imageset/
│   │       │       └── Contents.json
│   │       ├── Contents.json
│   │       └── icon.dataset/
│   │           ├── Contents.json
│   │           └── icon.json
│   ├── ContentView.swift
│   ├── Controllers/
│   │   ├── APNsService.swift
│   │   ├── FCMService.swift
│   │   ├── JWTSigner.swift
│   │   └── PushNotificationService.swift
│   ├── Models/
│   │   ├── APNsConfigStore.swift
│   │   ├── APNsConfiguration.swift
│   │   ├── FCMConfigStore.swift
│   │   ├── FCMConfiguration.swift
│   │   ├── FCMPayload.swift
│   │   ├── LiveActivityPayload.swift
│   │   ├── NativePushPayload.swift
│   │   ├── PushNotification.swift
│   │   ├── PushResponse.swift
│   │   ├── ReceiptResponse.swift
│   │   ├── SavedToken.swift
│   │   └── TokenToSave.swift
│   ├── Preview Content/
│   │   └── Preview Assets.xcassets/
│   │       └── Contents.json
│   ├── QuickPush.entitlements
│   ├── QuickPushApp.swift
│   ├── QuickPushIcon.icon/
│   │   └── icon.json
│   ├── Utilities/
│   │   ├── ColorHexConverter.swift
│   │   ├── FCMFileManager.swift
│   │   ├── FloatingPanel.swift
│   │   ├── SavedTokenStore.swift
│   │   ├── SecurityBookmarkManager.swift
│   │   ├── View+Extensions.swift
│   │   └── WindowManager.swift
│   ├── ViewModels/
│   │   ├── FCMViewModel.swift
│   │   ├── LiveActivityViewModel.swift
│   │   └── NativePushViewModel.swift
│   └── Views/
│       ├── APNsConfigurationView.swift
│       ├── APNsCurlCommandView.swift
│       ├── APNsResponseDetailView.swift
│       ├── APNsView.swift
│       ├── ColorPickerField.swift
│       ├── ExpoCurlCommandView.swift
│       ├── ExpoReceiptView.swift
│       ├── ExpoResponseDetailView.swift
│       ├── FCMConfigurationView.swift
│       ├── FCMCurlCommandView.swift
│       ├── FCMResponseDetailView.swift
│       ├── FCMView.swift
│       ├── FooterView.swift
│       ├── JSONImportExportView.swift
│       ├── KeyValueInputView.swift
│       ├── LiveActivityAlertSection.swift
│       ├── LiveActivityAttributesSection.swift
│       ├── LiveActivityContentStateSection.swift
│       ├── LiveActivityView.swift
│       ├── MainContentView.swift
│       ├── PushNotificationView.swift
│       ├── SaveTokenSheet.swift
│       └── SavedTokenRowView.swift
├── QuickPush.xcodeproj/
│   ├── project.pbxproj
│   ├── project.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   ├── xcshareddata/
│   │   │   └── swiftpm/
│   │   │       └── Package.resolved
│   │   └── xcuserdata/
│   │       └── beto.xcuserdatad/
│   │           └── UserInterfaceState.xcuserstate
│   └── xcuserdata/
│       └── beto.xcuserdatad/
│           ├── xcdebugger/
│           │   └── Breakpoints_v2.xcbkptlist
│           └── xcschemes/
│               └── xcschememanagement.plist
└── README.md
Condensed preview — 69 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (240K chars).
[
  {
    "path": "LICENSE",
    "chars": 1075,
    "preview": "MIT License\n\nCopyright (c) 2025 Code with Beto LLC\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "QuickPush/Assets.xcassets/AccentColor.colorset/Contents.json",
    "chars": 123,
    "preview": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }"
  },
  {
    "path": "QuickPush/Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "QuickPush/Assets.xcassets/QuickPushIcon/Assets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "QuickPush/Assets.xcassets/QuickPushIcon/Assets/bolt.brakesignal 1.imageset/Contents.json",
    "chars": 316,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"bolt.brakesignal 1.svg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n  "
  },
  {
    "path": "QuickPush/Assets.xcassets/QuickPushIcon/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "QuickPush/Assets.xcassets/QuickPushIcon/icon.dataset/Contents.json",
    "chars": 153,
    "preview": "{\n  \"data\" : [\n    {\n      \"filename\" : \"icon.json\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : "
  },
  {
    "path": "QuickPush/Assets.xcassets/QuickPushIcon/icon.dataset/icon.json",
    "chars": 973,
    "preview": "{\n  \"fill\" : \"automatic\",\n  \"groups\" : [\n    {\n      \"layers\" : [\n        {\n          \"fill-specializations\" : [\n       "
  },
  {
    "path": "QuickPush/ContentView.swift",
    "chars": 5740,
    "preview": "//\n//  ContentView.swift\n//  QuickPush\n//\n//  Created by beto on 2/20/25.\n//\n\nimport SwiftUI\n\nenum AppTab: String, CaseI"
  },
  {
    "path": "QuickPush/Controllers/APNsService.swift",
    "chars": 8948,
    "preview": "//\n//  APNsService.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport Foundation\n\n/// Full diagnostic info"
  },
  {
    "path": "QuickPush/Controllers/FCMService.swift",
    "chars": 12808,
    "preview": "//\n//  FCMService.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\nimport Security\n\n/// Full"
  },
  {
    "path": "QuickPush/Controllers/JWTSigner.swift",
    "chars": 2233,
    "preview": "//\n//  JWTSigner.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport Foundation\nimport CryptoKit\n\nclass JWT"
  },
  {
    "path": "QuickPush/Controllers/PushNotificationService.swift",
    "chars": 5532,
    "preview": "//\n//  PushNotificationService.swift\n//  QuickPush\n//\n//  Created by beto on 2/20/25.\n//\n\nimport Foundation\n\nclass PushN"
  },
  {
    "path": "QuickPush/Models/APNsConfigStore.swift",
    "chars": 2168,
    "preview": "//\n//  APNsConfigStore.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\n\n/// Shared APNs cre"
  },
  {
    "path": "QuickPush/Models/APNsConfiguration.swift",
    "chars": 804,
    "preview": "//\n//  APNsConfiguration.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport Foundation\n\nstruct APNsConfigu"
  },
  {
    "path": "QuickPush/Models/FCMConfigStore.swift",
    "chars": 1808,
    "preview": "//\n//  FCMConfigStore.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\n\n/// Shared FCM crede"
  },
  {
    "path": "QuickPush/Models/FCMConfiguration.swift",
    "chars": 480,
    "preview": "//\n//  FCMConfiguration.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\n\nstruct FCMConfigur"
  },
  {
    "path": "QuickPush/Models/FCMPayload.swift",
    "chars": 1941,
    "preview": "//\n//  FCMPayload.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\n\nstruct FCMMessage: Encod"
  },
  {
    "path": "QuickPush/Models/LiveActivityPayload.swift",
    "chars": 1950,
    "preview": "//\n//  LiveActivityPayload.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport Foundation\n\nstruct LiveActiv"
  },
  {
    "path": "QuickPush/Models/NativePushPayload.swift",
    "chars": 2836,
    "preview": "//\n//  NativePushPayload.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\n\nenum NativePushTy"
  },
  {
    "path": "QuickPush/Models/PushNotification.swift",
    "chars": 2162,
    "preview": "//\n//  PushNotification.swift\n//  QuickPush\n//\n//  Created by beto on 2/20/25.\n//\n\nimport Foundation\n\nstruct PushNotific"
  },
  {
    "path": "QuickPush/Models/PushResponse.swift",
    "chars": 727,
    "preview": "//\n//  PushResponse.swift\n//  QuickPush\n//\n//  Created by beto on 2/20/25.\n//\n\nimport Foundation\n\nstruct PushResponse: C"
  },
  {
    "path": "QuickPush/Models/ReceiptResponse.swift",
    "chars": 464,
    "preview": "//\n//  ReceiptResponse.swift\n//  QuickPush\n//\n//  Created by beto on 2/20/26.\n//\n\nimport Foundation\n\nstruct ReceiptRespo"
  },
  {
    "path": "QuickPush/Models/SavedToken.swift",
    "chars": 410,
    "preview": "//\n//  SavedToken.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\n\nstruct SavedToken: Codab"
  },
  {
    "path": "QuickPush/Models/TokenToSave.swift",
    "chars": 263,
    "preview": "//\n//  TokenToSave.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\n\n/// Identifiable wrappe"
  },
  {
    "path": "QuickPush/Preview Content/Preview Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "QuickPush/QuickPush.entitlements",
    "chars": 431,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "QuickPush/QuickPushApp.swift",
    "chars": 729,
    "preview": "//\n//  QuickPushApp.swift\n//  QuickPush\n//\n//  Created by beto on 2/20/25.\n//\n\nimport SwiftUI\n\n@main\nstruct QuickPushApp"
  },
  {
    "path": "QuickPush/QuickPushIcon.icon/icon.json",
    "chars": 973,
    "preview": "{\n  \"fill\" : \"automatic\",\n  \"groups\" : [\n    {\n      \"layers\" : [\n        {\n          \"fill-specializations\" : [\n       "
  },
  {
    "path": "QuickPush/Utilities/ColorHexConverter.swift",
    "chars": 1594,
    "preview": "//\n//  ColorHexConverter.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\nimport AppKit\n\nextensi"
  },
  {
    "path": "QuickPush/Utilities/FCMFileManager.swift",
    "chars": 4074,
    "preview": "//\n//  FCMFileManager.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\nimport AppKit\n\n/// Ma"
  },
  {
    "path": "QuickPush/Utilities/FloatingPanel.swift",
    "chars": 3475,
    "preview": "//\n//  FloatingPanel.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport AppKit\n\n/// A floating NSPanel tha"
  },
  {
    "path": "QuickPush/Utilities/SavedTokenStore.swift",
    "chars": 1329,
    "preview": "//\n//  SavedTokenStore.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport Foundation\n\n/// Manages persist"
  },
  {
    "path": "QuickPush/Utilities/SecurityBookmarkManager.swift",
    "chars": 1926,
    "preview": "//\n//  SecurityBookmarkManager.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport Foundation\nimport AppKit"
  },
  {
    "path": "QuickPush/Utilities/View+Extensions.swift",
    "chars": 1162,
    "preview": "import SwiftUI\n\nextension View {\n    @ViewBuilder\n    func applying<V: View>(@ViewBuilder _ modifier: (Self) -> V) -> so"
  },
  {
    "path": "QuickPush/Utilities/WindowManager.swift",
    "chars": 4772,
    "preview": "//\n//  WindowManager.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\n/// Manages the floating "
  },
  {
    "path": "QuickPush/ViewModels/FCMViewModel.swift",
    "chars": 5666,
    "preview": "//\n//  FCMViewModel.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\n@Observable\nclass FCMView"
  },
  {
    "path": "QuickPush/ViewModels/LiveActivityViewModel.swift",
    "chars": 9282,
    "preview": "//\n//  LiveActivityViewModel.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\n@Observable\nclass"
  },
  {
    "path": "QuickPush/ViewModels/NativePushViewModel.swift",
    "chars": 6461,
    "preview": "//\n//  NativePushViewModel.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\n@Observable\nclass "
  },
  {
    "path": "QuickPush/Views/APNsConfigurationView.swift",
    "chars": 2587,
    "preview": "//\n//  APNsConfigurationView.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\nstruct APNsConfig"
  },
  {
    "path": "QuickPush/Views/APNsCurlCommandView.swift",
    "chars": 1539,
    "preview": "//\n//  APNsCurlCommandView.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\n/// Sheet that dis"
  },
  {
    "path": "QuickPush/Views/APNsResponseDetailView.swift",
    "chars": 6574,
    "preview": "//\n//  APNsResponseDetailView.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\n/// Sheet that d"
  },
  {
    "path": "QuickPush/Views/APNsView.swift",
    "chars": 9874,
    "preview": "//\n//  APNsView.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\nstruct APNsView: View {\n  var"
  },
  {
    "path": "QuickPush/Views/ColorPickerField.swift",
    "chars": 523,
    "preview": "//\n//  ColorPickerField.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\nstruct ColorPickerFiel"
  },
  {
    "path": "QuickPush/Views/ExpoCurlCommandView.swift",
    "chars": 2317,
    "preview": "//\n//  ExpoCurlCommandView.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\n/// Sheet that dis"
  },
  {
    "path": "QuickPush/Views/ExpoReceiptView.swift",
    "chars": 8975,
    "preview": "//\n//  ExpoReceiptView.swift\n//  QuickPush\n//\n//  Created by beto on 2/20/26.\n//\n\nimport SwiftUI\n\nstruct ExpoReceiptView"
  },
  {
    "path": "QuickPush/Views/ExpoResponseDetailView.swift",
    "chars": 5809,
    "preview": "//\n//  ExpoResponseDetailView.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\n/// Sheet that "
  },
  {
    "path": "QuickPush/Views/FCMConfigurationView.swift",
    "chars": 2913,
    "preview": "//\n//  FCMConfigurationView.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\nstruct FCMConfigu"
  },
  {
    "path": "QuickPush/Views/FCMCurlCommandView.swift",
    "chars": 1545,
    "preview": "//\n//  FCMCurlCommandView.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\n/// Sheet that disp"
  },
  {
    "path": "QuickPush/Views/FCMResponseDetailView.swift",
    "chars": 4756,
    "preview": "//\n//  FCMResponseDetailView.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\n/// Sheet that d"
  },
  {
    "path": "QuickPush/Views/FCMView.swift",
    "chars": 10447,
    "preview": "//\n//  FCMView.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\nimport AppKit\n\nstruct FCMView: "
  },
  {
    "path": "QuickPush/Views/FooterView.swift",
    "chars": 2432,
    "preview": "//\n//  FooterView.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\nimport LaunchAtLogin\n\n/// Foo"
  },
  {
    "path": "QuickPush/Views/JSONImportExportView.swift",
    "chars": 1616,
    "preview": "//\n//  JSONImportExportView.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\nstruct JSONImportE"
  },
  {
    "path": "QuickPush/Views/KeyValueInputView.swift",
    "chars": 4456,
    "preview": "//\n//  KeyValueInputView.swift\n//  QuickPush\n//\n//  Created by beto on 2/20/25.\n//\n\nimport SwiftUI\n\nstruct KeyValueInput"
  },
  {
    "path": "QuickPush/Views/LiveActivityAlertSection.swift",
    "chars": 990,
    "preview": "//\n//  LiveActivityAlertSection.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\nstruct LiveAct"
  },
  {
    "path": "QuickPush/Views/LiveActivityAttributesSection.swift",
    "chars": 3075,
    "preview": "//\n//  LiveActivityAttributesSection.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\nstruct Li"
  },
  {
    "path": "QuickPush/Views/LiveActivityContentStateSection.swift",
    "chars": 1799,
    "preview": "//\n//  LiveActivityContentStateSection.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\nstruct "
  },
  {
    "path": "QuickPush/Views/LiveActivityView.swift",
    "chars": 4601,
    "preview": "//\n//  LiveActivityView.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\nstruct LiveActivityVie"
  },
  {
    "path": "QuickPush/Views/MainContentView.swift",
    "chars": 395,
    "preview": "//\n//  MainContentView.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\n/// Shared root view us"
  },
  {
    "path": "QuickPush/Views/PushNotificationView.swift",
    "chars": 17080,
    "preview": "//\n//  PushNotificationView.swift\n//  QuickPush\n//\n//  Created by beto on 2/8/26.\n//\n\nimport SwiftUI\n\nstruct PushNotific"
  },
  {
    "path": "QuickPush/Views/SaveTokenSheet.swift",
    "chars": 2338,
    "preview": "//\n//  SaveTokenSheet.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\n/// Modal sheet for sav"
  },
  {
    "path": "QuickPush/Views/SavedTokenRowView.swift",
    "chars": 1758,
    "preview": "//\n//  SavedTokenRowView.swift\n//  QuickPush\n//\n//  Created by beto on 2/19/26.\n//\n\nimport SwiftUI\n\n/// Compact row disp"
  },
  {
    "path": "QuickPush.xcodeproj/project.pbxproj",
    "chars": 12368,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 77;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
  },
  {
    "path": "QuickPush.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "chars": 135,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef"
  },
  {
    "path": "QuickPush.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
    "chars": 412,
    "preview": "{\n  \"originHash\" : \"50c271549071e9a517f8a4da8966700ae5e4b29169d39bb1ae8c64c11eb330cd\",\n  \"pins\" : [\n    {\n      \"identit"
  },
  {
    "path": "QuickPush.xcodeproj/xcuserdata/beto.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist",
    "chars": 140,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Bucket\n   uuid = \"72746176-1CCB-456A-9A1D-8E6B67F9D05C\"\n   type = \"1\"\n   version"
  },
  {
    "path": "QuickPush.xcodeproj/xcuserdata/beto.xcuserdatad/xcschemes/xcschememanagement.plist",
    "chars": 344,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "README.md",
    "chars": 16566,
    "preview": "<p align=\"center\">\n  <picture >\n    <source height=\"96\" media=\"(prefers-color-scheme: dark)\" srcset=\"./.github/resources"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the betomoedano/quick-push GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 69 files (219.1 KB), approximately 56.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!