feat(diagnosis): add opt-in Firebase diagnostic data reporting

This commit is contained in:
Kaede Akino committed 2026-10-01 08:45:38 +08:00
1 parent b702ff8ee0
commit 22f09eaeee
23 files changed
+989 -13

No files matched your search

+195
View File
@@ -0,0 +1,195 @@
import Foundation
import TermindApplication
import TermindInfrastructure
import Testing
@Suite @MainActor
struct DiagnosisTests {
@Test
func firstLaunchWaitsForAnExplicitChoice() async throws {
try await withDefaults { defaults in
let reporter = RecordingReporter()
let controller = DiagnosisController(defaults: defaults, reporter: reporter)
#expect(controller.needsConsent)
#expect(!controller.isEnabled)
#expect(controller.consent == nil)
#expect(defaults.object(forKey: SettingsKey.diagnosisConsent) == nil)
#expect(reporter.values == [false])
}
}
@Test(arguments: [false, true])
func bothChoicesPersistWithoutAskingAgain(enabled: Bool) async throws {
try await withDefaults { defaults in
let reporter = RecordingReporter()
let controller = DiagnosisController(defaults: defaults, reporter: reporter)
controller.setEnabled(enabled)
controller.setEnabled(enabled)
#expect(!controller.needsConsent)
#expect(controller.isEnabled == enabled)
#expect(reporter.values == [false, enabled])
let nextLaunch = DiagnosisController(defaults: defaults, reporter: reporter)
#expect(!nextLaunch.needsConsent)
#expect(nextLaunch.consent == enabled)
#expect(reporter.values == [false, enabled, enabled])
}
}
@Test
func revocationPersistsBeforeApplyingItToTheSDK() async throws {
try await withDefaults { defaults in
defaults.set(true, forKey: "diagnostics.crashReportingConsent")
let reporter = RecordingReporter()
let controller = DiagnosisController(defaults: defaults, reporter: reporter)
reporter.didChange = { enabled in
#expect(defaults.object(forKey: SettingsKey.diagnosisConsent) as? Bool == enabled)
}
controller.setEnabled(false)
#expect(reporter.values == [true, false])
#expect(!DiagnosisController(defaults: defaults).needsConsent)
}
}
@Test
func testAndPreviewControllersDoNotPresentConsent() {
#expect(!DiagnosisController().needsConsent)
#expect(!DiagnosisController().isEnabled)
}
@Test
func unknownAndDeclinedConsentNeverInitializeFirebase() async throws {
try await withDefaults { defaults in
var initializations = 0
let reporter = FirebaseDiagnosticDataReporter(defaults: defaults) {
initializations += 1
return nil
}
let controller = DiagnosisController(defaults: defaults, reporter: reporter)
controller.setEnabled(false)
#expect(initializations == 0)
controller.setEnabled(true)
#expect(initializations == 1)
// A missing developer configuration does not lose the user's choice or block startup.
#expect(controller.isEnabled && !controller.needsConsent)
}
}
@Test
func optedInLaunchSendsPendingDiagnosticDataOnce() async throws {
try await withDefaults { defaults in
let client = DeferredDiagnosticDataClient()
let reporter = FirebaseDiagnosticDataReporter(defaults: defaults) { client }
reporter.setEnabled(true)
await client.waitForCheck()
client.finishCheck(hasData: true)
await client.waitForAction()
reporter.setEnabled(true)
#expect(client.sends == 1)
#expect(client.deletes == 0)
#expect(client.checks == 1)
}
}
@Test
func consentRevokedDuringSDKCheckDeletesInsteadOfUploading() async throws {
try await withDefaults { defaults in
let client = DeferredDiagnosticDataClient()
let reporter = FirebaseDiagnosticDataReporter(defaults: defaults) { client }
reporter.setEnabled(true)
await client.waitForCheck()
reporter.setEnabled(false)
client.finishCheck(hasData: true)
await client.waitForAction()
#expect(client.sends == 0)
#expect(client.deletes == 1)
}
}
@Test
func optingBackInDiscardsDiagnosticDataFromBeforeConsentWasRestored() async throws {
try await withDefaults { defaults in
let previousLaunch = FirebaseDiagnosticDataReporter(defaults: defaults) { nil }
previousLaunch.setEnabled(false)
#expect(defaults.bool(forKey: "diagnostics.discardPendingCrashReports"))
let client = DeferredDiagnosticDataClient()
let reporter = FirebaseDiagnosticDataReporter(defaults: defaults) { client }
reporter.setEnabled(true)
await client.waitForCheck()
client.finishCheck(hasData: true)
await client.waitForAction()
#expect(client.sends == 0 && client.deletes == 1)
let nextClient = DeferredDiagnosticDataClient()
let nextLaunch = FirebaseDiagnosticDataReporter(defaults: defaults) { nextClient }
nextLaunch.setEnabled(true)
await nextClient.waitForCheck()
nextClient.finishCheck(hasData: true)
await nextClient.waitForAction()
#expect(nextClient.sends == 1 && nextClient.deletes == 0)
}
}
private func withDefaults(_ body: (UserDefaults) async throws -> Void) async throws {
let domain = "DiagnosisTests.\(UUID())"
let defaults = try #require(UserDefaults(suiteName: domain))
defer { defaults.removePersistentDomain(forName: domain) }
try await body(defaults)
}
}
@MainActor
private final class RecordingReporter: DiagnosticDataReporting {
var values: [Bool] = []
var didChange: ((Bool) -> Void)?
func setEnabled(_ enabled: Bool) {
values.append(enabled)
didChange?(enabled)
}
}
@MainActor
private final class DeferredDiagnosticDataClient: DiagnosticDataClient {
var sends = 0
var deletes = 0
var checks = 0
private var pendingCheck: CheckedContinuation<Bool, Never>?
private var checkStarted: CheckedContinuation<Void, Never>?
private var actionFinished: CheckedContinuation<Void, Never>?
func hasUnsentDiagnosticData() async -> Bool {
checks += 1
return await withCheckedContinuation { continuation in
pendingCheck = continuation
checkStarted?.resume()
checkStarted = nil
}
}
func waitForCheck() async {
if pendingCheck != nil { return }
await withCheckedContinuation { checkStarted = $0 }
}
func finishCheck(hasData: Bool) {
pendingCheck?.resume(returning: hasData)
pendingCheck = nil
}
func sendUnsentDiagnosticData() {
sends += 1
actionFinished?.resume()
actionFinished = nil
}
func deleteUnsentDiagnosticData() {
deletes += 1
actionFinished?.resume()
actionFinished = nil
}
func waitForAction() async {
if sends + deletes > 0 { return }
await withCheckedContinuation { actionFinished = $0 }
}
}
+184
View File
@@ -3092,6 +3092,29 @@
}
}
},
"Allow Termind to send diagnostic data to Google? This includes crash details, app and operating system versions, device information, and diagnostic identifiers." : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Allow Termind to send diagnostic data to Google? This includes crash details, app and operating system versions, device information, and diagnostic identifiers."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "是否允许Termind向Google发送诊断数据?这些数据包含崩溃详情、应用和操作系统版本、设备信息及诊断标识符。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "是否允許Termind向Google傳送診斷資料?這些資料包含當機詳情、App與作業系統版本、裝置資訊及診斷識別碼。"
}
}
}
},
"Allow Termind to use Speech Recognition in Settings, then try again." : {
"extractionState" : "manual",
"localizations" : {
@@ -9547,6 +9570,29 @@
}
}
},
"Diagnosis" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Diagnosis"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "诊断"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "診斷"
}
}
}
},
"Did not run “%@”" : {
"extractionState" : "manual",
"localizations" : {
@@ -9981,6 +10027,29 @@
}
}
},
"Don't Send" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Don't Send"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "不发送"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "不要傳送"
}
}
}
},
"Done" : {
"extractionState" : "manual",
"localizations" : {
@@ -13148,6 +13217,29 @@
}
}
},
"Help make Termind more reliable" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Help make Termind more reliable"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "帮助提升Termind的稳定性"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "協助提升Termind的穩定性"
}
}
}
},
"Hermes Memory" : {
"extractionState" : "manual",
"localizations" : {
@@ -24850,6 +24942,29 @@
}
}
},
"Send Diagnostic Data" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Send Diagnostic Data"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "发送诊断数据"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "傳送診斷資料"
}
}
}
},
"Send Speed" : {
"extractionState" : "manual",
"localizations" : {
@@ -24890,6 +25005,29 @@
}
}
},
"Send diagnostic data to Google Firebase Crashlytics to help make Termind more reliable." : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Send diagnostic data to Google Firebase Crashlytics to help make Termind more reliable."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "向Google Firebase Crashlytics发送诊断数据以帮助提升Termind的稳定性。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "向Google Firebase Crashlytics傳送診斷資料以協助提升Termind的穩定性。"
}
}
}
},
"Send — ⌘⏎" : {
"extractionState" : "manual",
"localizations" : {
@@ -27468,6 +27606,29 @@
}
}
},
"Termind does not attach terminal output, chat content, passwords, or private keys to diagnostic data." : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Termind does not attach terminal output, chat content, passwords, or private keys to diagnostic data."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "Termind不会在诊断数据中附加终端输出、聊天内容、密码或私钥。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "Termind不會在診斷資料中附加終端輸出、聊天內容、密碼或私鑰。"
}
}
}
},
"Termind returns only a one-shot handle to the MCP client. The value is bound to this session, destination, ssh_exec, and purpose." : {
"extractionState" : "manual",
"localizations" : {
@@ -30529,6 +30690,29 @@
}
}
},
"This is optional. Your choice applies to this device, and you can change it in Settings > App." : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This is optional. Your choice applies to this device, and you can change it in Settings > App."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "此功能为可选项。您的选择仅适用于当前设备,可在“设置”>“应用”中更改。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "此功能為選用項目。您的選擇僅適用於目前裝置,可在「設定」>「App」中更改。"
}
}
}
},
"This key is not among the %lld trusted keys saved for %@. Verify it carefully before adding it." : {
"extractionState" : "manual",
"localizations" : {
+38 -1
View File
@@ -5,7 +5,44 @@
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeCrashData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeDeviceID</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
@@ -0,0 +1,33 @@
import Foundation
import Observation
@MainActor
public protocol DiagnosticDataReporting: AnyObject {
func setEnabled(_ enabled: Bool)
}
/// Consent belongs to this installation, never to the synced Settings snapshot.
@MainActor @Observable
public final class DiagnosisController {
public private(set) var consent: Bool?
private let defaults: UserDefaults?
private let reporter: (any DiagnosticDataReporting)?
public var needsConsent: Bool { defaults != nil && consent == nil }
public var isEnabled: Bool { consent == true }
/// An unconfigured controller is inert for previews, screenshots and test hosts.
public init(defaults: UserDefaults? = nil, reporter: (any DiagnosticDataReporting)? = nil) {
self.defaults = defaults
self.reporter = reporter
consent = defaults?.object(forKey: SettingsKey.diagnosisConsent) as? Bool
reporter?.setEnabled(consent == true)
}
public func setEnabled(_ enabled: Bool) {
guard consent != enabled else { return }
defaults?.set(enabled, forKey: SettingsKey.diagnosisConsent)
consent = enabled
reporter?.setEnabled(enabled)
}
}
@@ -2,6 +2,10 @@ import Foundation
import TermindDomain
public nonisolated enum SettingsKey {
// Device-local privacy consent; absence means the user has not chosen yet.
// Preserve the original storage key so a terminology change does not reset consent.
public static let diagnosisConsent = "diagnostics.crashReportingConsent"
// Debug visibility and logging are device-local and never included in Settings sync.
public static let debugSectionEnabled = "debug.section.enabled"
public static let loggingSavingEnabled = "debug.logging.savingEnabled"
@@ -0,0 +1,85 @@
import FirebaseCore
import FirebaseCrashlytics
import Foundation
import TermindApplication
/// A narrow SDK seam keeps consent and delayed report delivery testable without Firebase traffic.
@MainActor
public protocol DiagnosticDataClient: AnyObject {
func hasUnsentDiagnosticData() async -> Bool
func sendUnsentDiagnosticData()
func deleteUnsentDiagnosticData()
}
@MainActor
public final class FirebaseDiagnosticDataReporter: DiagnosticDataReporting {
// Preserve the discard marker across terminology changes, just like consent.
private static let discardKey = "diagnostics.discardPendingCrashReports"
private let defaults: UserDefaults
private let makeClient: () -> (any DiagnosticDataClient)?
private var client: (any DiagnosticDataClient)?
private var isEnabled = false
public init(defaults: UserDefaults, makeClient: @escaping () -> (any DiagnosticDataClient)?) {
self.defaults = defaults
self.makeClient = makeClient
}
public static func live(defaults: UserDefaults = .standard, bundle: Bundle = .main)
-> FirebaseDiagnosticDataReporter
{
FirebaseDiagnosticDataReporter(defaults: defaults) {
// No Firebase app, installation registration or crash handler before consent.
guard let path = bundle.path(forResource: "GoogleService-Info", ofType: "plist"),
let options = FirebaseOptions(contentsOfFile: path),
options.bundleID == bundle.bundleIdentifier
else { return nil }
if FirebaseApp.app() == nil {
FirebaseApp.configure(options: options)
}
FirebaseApp.app()?.isDataCollectionDefaultEnabled = false
let crashlytics = Crashlytics.crashlytics()
// Never persist `true`: automatic startup upload would bypass a later revocation.
crashlytics.setCrashlyticsCollectionEnabled(false)
return FirebaseDiagnosticDataClient(crashlytics: crashlytics)
}
}
public func setEnabled(_ enabled: Bool) {
isEnabled = enabled
guard enabled else {
// A crash after opting out must not be uploaded if consent is later granted again.
defaults.set(true, forKey: Self.discardKey)
return
}
guard client == nil, let client = makeClient() else { return }
self.client = client
Task { [weak self] in
let hasData = await client.hasUnsentDiagnosticData()
guard let self else { return }
// The user may revoke consent while the SDK checks the previous run's diagnostic data.
if !isEnabled || defaults.bool(forKey: Self.discardKey) {
client.deleteUnsentDiagnosticData()
if isEnabled { defaults.removeObject(forKey: Self.discardKey) }
} else if hasData {
client.sendUnsentDiagnosticData()
}
}
}
}
@MainActor
private final class FirebaseDiagnosticDataClient: DiagnosticDataClient {
private let crashlytics: Crashlytics
init(crashlytics: Crashlytics) {
self.crashlytics = crashlytics
}
func hasUnsentDiagnosticData() async -> Bool {
await crashlytics.checkForUnsentReports()
}
func sendUnsentDiagnosticData() { crashlytics.sendUnsentReports() }
func deleteUnsentDiagnosticData() { crashlytics.deleteUnsentReports() }
}
@@ -0,0 +1,93 @@
import SwiftUI
import TermindApplication
public struct DiagnosisConsentView: View {
private let controller: DiagnosisController
public init(controller: DiagnosisController) {
self.controller = controller
}
public var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 24) {
Image(systemName: "heart.text.clipboard")
.font(.system(size: 48, weight: .light))
.foregroundStyle(.tint)
.accessibilityHidden(true)
Text(AppStrings.Diagnosis.invitation)
.font(.largeTitle.bold())
.fixedSize(horizontal: false, vertical: true)
.accessibilityAddTraits(.isHeader)
Text(AppStrings.Diagnosis.explanation)
Label {
Text(AppStrings.Diagnosis.privacy)
} icon: {
Image(systemName: "lock.shield")
}
Text(AppStrings.Diagnosis.choice)
.foregroundStyle(.secondary)
Link(destination: Self.privacyURL) {
Text(AppStrings.Pro.privacyPolicy)
.underline()
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
HStack {
declineAction
Spacer(minLength: 24)
allowButton
}
.controlSize(.large)
}
.padding(32)
.frame(maxWidth: 560, alignment: .leading)
.frame(maxWidth: .infinity)
}
.defaultScrollAnchor(.center, for: .alignment)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private static let privacyURL = URL(string: "https://termind.akinokaede.com/privacy/")!
private var allowButton: some View {
Button(AppStrings.Common.allow.swiftUIKey) {
controller.setEnabled(true)
}
.buttonStyle(.borderedProminent)
.accessibilityIdentifier("diagnosis.allow")
}
private var declineAction: some View {
Button(AppStrings.Diagnosis.decline.swiftUIKey) {
controller.setEnabled(false)
}
.buttonStyle(.plain)
.foregroundStyle(.secondary)
.accessibilityIdentifier("diagnosis.decline")
}
}
public struct DiagnosisSettingsSection: View {
private let controller: DiagnosisController
public init(controller: DiagnosisController) {
self.controller = controller
}
public var body: some View {
Section {
Toggle(
AppStrings.Diagnosis.sendDiagnosticData.swiftUIKey,
isOn: Binding(get: { controller.isEnabled }, set: controller.setEnabled)
)
.toggleStyle(.switch)
} header: {
Text(AppStrings.Diagnosis.title)
} footer: {
Text(AppStrings.Diagnosis.settingsExplanation)
}
}
}
@@ -14,6 +14,10 @@ extension AppStrings {
"Add Hosts"
}
public static var allow: LocalizedStringResource {
"Allow"
}
public static var back: LocalizedStringResource {
"Back"
}
@@ -0,0 +1,22 @@
import Foundation
extension AppStrings {
public nonisolated enum Diagnosis {
public static var title: LocalizedStringResource { "Diagnosis" }
public static var invitation: LocalizedStringResource { "Help make Termind more reliable" }
public static var explanation: LocalizedStringResource {
"Allow Termind to send diagnostic data to Google? This includes crash details, app and operating system versions, device information, and diagnostic identifiers."
}
public static var privacy: LocalizedStringResource {
"Termind does not attach terminal output, chat content, passwords, or private keys to diagnostic data."
}
public static var choice: LocalizedStringResource {
"This is optional. Your choice applies to this device, and you can change it in Settings > App."
}
public static var decline: LocalizedStringResource { "Don't Send" }
public static var sendDiagnosticData: LocalizedStringResource { "Send Diagnostic Data" }
public static var settingsExplanation: LocalizedStringResource {
"Send diagnostic data to Google Firebase Crashlytics to help make Termind more reliable."
}
}
}
+2
View File
@@ -13,6 +13,7 @@ struct MobileAppDependencies {
var workspaceConnections: MobileWorkspaceConnectionServices
var metrics: MobileMetricsServices
var appReview: AppReviewController
var diagnosis = DiagnosisController()
var logging: any ApplicationLogManaging = DisabledApplicationLogManager.shared
var connectionCoordinator: RemoteConnectionCoordinator = RemoteConnectionCoordinator()
var container: ModelContainer? = nil
@@ -34,6 +35,7 @@ struct MobileAppDependencies {
workspaceConnections: .live(coordinator: coordinator),
metrics: .live(coordinator: coordinator),
appReview: .live(),
diagnosis: DiagnosisController(defaults: .standard, reporter: FirebaseDiagnosticDataReporter.live()),
logging: logging,
connectionCoordinator: coordinator,
makeForwardings: {
+2
View File
@@ -140,6 +140,7 @@ final class MobileAppModel {
let metrics: MobileMetricsModel
let pro: ProAccessController
let appReview: AppReviewController
let diagnosis: DiagnosisController
let logging: any ApplicationLogManaging
let liveActivities: MobileLiveActivityController
@ObservationIgnored private var workspaceActivitySessions: [WorkspaceActivityAttributes.Session] = []
@@ -198,6 +199,7 @@ final class MobileAppModel {
dependencies.workspaceStore
?? MobileWorkspaceStore(defaults: inMemory || hosts != nil ? nil : defaults)
appReview = dependencies.appReview
diagnosis = dependencies.diagnosis
logging = dependencies.logging
let memoryOnly = inMemory || hosts != nil
let storeURL = URL.applicationSupportDirectory.appending(path: "Termind.store")
+9 -3
View File
@@ -22,9 +22,15 @@ struct MobileStartupView: View {
var body: some View {
switch startup {
case .success(let app):
MobileQuickActionSceneRoot(app: app)
.modelContainer(app.container)
.preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil)
Group {
if app.diagnosis.needsConsent {
DiagnosisConsentView(controller: app.diagnosis)
} else {
MobileQuickActionSceneRoot(app: app)
}
}
.modelContainer(app.container)
.preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil)
case .failure:
ContentUnavailableView(
AppStrings.Common.unableToOpenTermind.swiftUIKey, systemImage: "exclamationmark.triangle",
@@ -6,6 +6,7 @@ struct MobileAppSettingsView: View {
let workspaceSettings: WorkspaceSettingsModel
var appReview: AppReviewController?
var reviewScene: MobileSceneModel?
var diagnosis = DiagnosisController()
@AppStorage("mobile.appearance") private var appearance = "system"
@AppStorage(SettingsKey.debugSectionEnabled) private var debugSectionEnabled = false
@@ -34,6 +35,7 @@ struct MobileAppSettingsView: View {
} footer: {
Text(AppStrings.Settings.workspaceNumberingExplanation)
}
DiagnosisSettingsSection(controller: diagnosis)
Section(AppStrings.Settings.version.swiftUIKey) {
LabeledContent(
AppStrings.Settings.app.swiftUIKey,
@@ -19,7 +19,8 @@ struct MobileSettingsView: View {
MobileAppSettingsView(
workspaceSettings: app.workspaceSettings,
appReview: app.appReview,
reviewScene: reviewScene)
reviewScene: reviewScene,
diagnosis: app.diagnosis)
} label: {
Label(AppStrings.Settings.app.swiftUIKey, systemImage: "app.badge")
}
+6
View File
@@ -2,6 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FirebaseCrashlyticsCollectionEnabled</key>
<false/>
<key>FirebaseDataCollectionDefaultEnabled</key>
<false/>
<key>FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
+3
View File
@@ -71,6 +71,7 @@ final class AppModel {
let commands = AppCommandRegistry()
let onboarding: OnboardingController
let appReview: AppReviewController
let diagnosis: DiagnosisController
let serverMetrics: ServerMetricsModel
let windowSizeStore: MainWindowSizeStore
let logging: any ApplicationLogManaging
@@ -191,6 +192,7 @@ final class AppModel {
proAccess: ProAccessController = .unrestrictedForTesting(),
onboarding: OnboardingController = OnboardingController(),
appReview: AppReviewController = AppReviewController(),
diagnosis: DiagnosisController = DiagnosisController(),
serverMetrics suppliedMetrics: ServerMetricsModel? = nil,
windowSizeStore: MainWindowSizeStore = MainWindowSizeStore()
) {
@@ -220,6 +222,7 @@ final class AppModel {
self.proAccess = proAccess
self.onboarding = onboarding
self.appReview = appReview
self.diagnosis = diagnosis
knownHosts = KnownHostsLibraryModel(library: hosts)
credentials = CredentialLibraryModel(library: hosts)
snippets = SnippetLibraryModel(library: hosts)
+19 -7
View File
@@ -56,6 +56,10 @@ struct TermindApp: App {
#else
let usesInMemoryStore = isRunningTests
#endif
let diagnosis =
usesInMemoryStore
? DiagnosisController()
: DiagnosisController(defaults: .standard, reporter: FirebaseDiagnosticDataReporter.live())
if !usesInMemoryStore { try DatabaseStartupGuard.checkRunningApplications() }
let container = try TermindPersistence.makeContainer(inMemory: usesInMemoryStore)
ContextAttachmentOpener.cleanPreviousSession()
@@ -195,6 +199,7 @@ struct TermindApp: App {
),
proAccess: proAccess,
appReview: .live(),
diagnosis: diagnosis,
windowSizeStore: MainWindowSizeStore(defaults: isRunningTests ? nil : .standard)
)
)
@@ -228,13 +233,20 @@ struct TermindApp: App {
/// destinations and dynamic Workspaces.
var body: some Scene {
WindowGroup(id: "main", for: UUID.self) { $requestID in
RootView(window: makeWindow(requestID: requestID), requestID: requestID)
.environment(app)
.environment(\.locale, launchLanguage.locale)
.modelContainer(modelContainer)
#if canImport(AppKit)
.onAppear { delegate.app = app }
#endif
Group {
if app.diagnosis.needsConsent {
DiagnosisConsentView(controller: app.diagnosis)
.frame(minWidth: 560, minHeight: 620)
} else {
RootView(window: makeWindow(requestID: requestID), requestID: requestID)
}
}
.environment(app)
.environment(\.locale, launchLanguage.locale)
.modelContainer(modelContainer)
#if canImport(AppKit)
.onAppear { delegate.app = app }
#endif
}
#if DEBUG
// XCTest launches the real app. Gating individual tests does not stop
@@ -158,6 +158,7 @@ struct SettingsPageView: View {
SettingsFormPage {
AppSettings()
StartupSettings()
DiagnosisSettingsSection(controller: app.diagnosis)
AppInfoSettings()
}
case .notifications:
+6
View File
@@ -2,6 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FirebaseCrashlyticsCollectionEnabled</key>
<false/>
<key>FirebaseDataCollectionDefaultEnabled</key>
<false/>
<key>FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
+16
View File
@@ -3,6 +3,22 @@ import Testing
@Suite
struct ArchitectureBoundaryTests {
@Test
func diagnosisSDKStaysBehindInfrastructureAndStartsWithCollectionDisabled() throws {
for module in ["TermindDomain", "TermindApplication", "TermindPresentation", "TermindPersistence"] {
try assertNoImports(
in: "../Frameworks/\(module)", forbidden: ["FirebaseCore", "FirebaseCrashlytics", "FirebaseAnalytics"])
}
for path in ["Info.plist", "../TFI/Info.plist"] {
let data = try Data(contentsOf: sourceRoot.appending(path: path))
let plist = try #require(
PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any])
#expect(plist["FirebaseCrashlyticsCollectionEnabled"] as? Bool == false)
#expect(plist["FirebaseDataCollectionDefaultEnabled"] as? Bool == false)
#expect(plist["FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED"] as? Bool == true)
}
}
@Test
func forwardingBenchmarkRemainsIndependentFromAppsAndTests() throws {
let repositoryRoot = sourceRoot.deletingLastPathComponent().standardizedFileURL
@@ -0,0 +1,41 @@
import AppKit
import SwiftUI
import TermindApplication
import TermindPresentation
import Testing
@Suite(.requiresVisibleWindow, .serialized) @MainActor
struct DiagnosisConsentRenderingTests {
@Test(arguments: ["en", "zh-Hans", "zh-Hant"], [360.0, 560.0])
func introductoryPageRendersWithoutChangingConsent(language: String, width: Double) async throws {
let domain = "DiagnosisRendering.\(UUID())"
let defaults = try #require(UserDefaults(suiteName: domain))
defer { defaults.removePersistentDomain(forName: domain) }
let controller = DiagnosisController(defaults: defaults)
let content = DiagnosisConsentView(controller: controller)
.environment(\.locale, Locale(identifier: language))
.frame(width: width, height: 680)
.background(.background)
let host = NSHostingController(rootView: content)
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: width, height: 680),
styleMask: [.titled], backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.contentViewController = host
window.makeKeyAndOrderFront(nil)
defer {
window.orderOut(nil)
window.contentViewController = nil
window.close()
}
try await Task.sleep(for: .milliseconds(200))
host.view.layoutSubtreeIfNeeded()
let bitmap = try #require(host.view.bitmapImageRepForCachingDisplay(in: host.view.bounds))
host.view.cacheDisplay(in: host.view.bounds, to: bitmap)
let png = try #require(bitmap.representation(using: .png, properties: [:]))
#expect(png.count > 5_000)
#expect(controller.needsConsent)
#expect(defaults.object(forKey: SettingsKey.diagnosisConsent) == nil)
Attachment.record(png, named: "diagnosis-\(language)-\(Int(width)).png")
}
}
+104
View File
@@ -82,6 +82,16 @@
F04700000000000000000001 /* TFMTests/SSHForwardingPumpTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04700000000000000000011 /* TFMTests/SSHForwardingPumpTests.swift */; };
F04700000000000000000002 /* TFMTests/SSHForwardingWireTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04700000000000000000012 /* TFMTests/SSHForwardingWireTests.swift */; };
F04700000000000000000003 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = LOGG00000000000000000002 /* Logging */; };
F1BA00000000000000000101 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000001 /* FirebaseCore */; };
F1BA00000000000000000102 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000002 /* FirebaseCrashlytics */; };
F1BA00000000000000000201 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000001 /* FirebaseCore */; };
F1BA00000000000000000202 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000002 /* FirebaseCrashlytics */; };
F1BA00000000000000000301 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000001 /* FirebaseCore */; };
F1BA00000000000000000302 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000002 /* FirebaseCrashlytics */; };
F1BA00000000000000000401 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000001 /* FirebaseCore */; };
F1BA00000000000000000402 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000002 /* FirebaseCrashlytics */; };
F1BA0000000000000000A001 /* Configuration/Firebase/GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F1BA00000000000000000020 /* Configuration/Firebase/GoogleService-Info.plist */; };
F1BA0000000000000000A002 /* Configuration/Firebase/GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F1BA00000000000000000020 /* Configuration/Firebase/GoogleService-Info.plist */; };
F6ACDF3012081771F6C8F659 /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; };
F996BFD771BC77E67AB91FFB /* TermindInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; };
FACB579B671967F922A33CD9 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; };
@@ -474,6 +484,7 @@
E28A00000000000000000001 /* V28.store */ = {isa = PBXFileReference; lastKnownFileType = file; name = V28.store; path = FrameworkTests/Fixtures/V28.store; sourceTree = SOURCE_ROOT; };
F04700000000000000000011 /* TFMTests/SSHForwardingPumpTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFMTests/SSHForwardingPumpTests.swift; sourceTree = SOURCE_ROOT; };
F04700000000000000000012 /* TFMTests/SSHForwardingWireTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFMTests/SSHForwardingWireTests.swift; sourceTree = SOURCE_ROOT; };
F1BA00000000000000000020 /* Configuration/Firebase/GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Configuration/Firebase/GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; };
TRMD00000000000000000001 /* Termind.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Termind.app; sourceTree = BUILT_PRODUCTS_DIR; };
TRMD00000000000000020001 /* TFMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TFMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
TRMD00000000000000030002 /* termind-mcp */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "termind-mcp"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -640,6 +651,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F1BA00000000000000000302 /* FirebaseCrashlytics in Frameworks */,
F1BA00000000000000000301 /* FirebaseCore in Frameworks */,
DEVK00000000000000000003 /* DeviceKit in Frameworks */,
LOGG00000000000000000105 /* Logging in Frameworks */,
BAAD00000000000000000011 /* GRDB in Frameworks */,
@@ -663,6 +676,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F1BA00000000000000000102 /* FirebaseCrashlytics in Frameworks */,
F1BA00000000000000000101 /* FirebaseCore in Frameworks */,
LOGG00000000000000000102 /* Logging in Frameworks */,
ABCDEF020000000000000001 /* ZIPFoundation in Frameworks */,
ABCDEF010000000000000001 /* TermindPersistence.framework in Frameworks */,
@@ -712,6 +727,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F1BA00000000000000000402 /* FirebaseCrashlytics in Frameworks */,
F1BA00000000000000000401 /* FirebaseCore in Frameworks */,
LOGG00000000000000000106 /* Logging in Frameworks */,
BAAD00000000000000000012 /* GRDB in Frameworks */,
AA1200000000000000000010 /* MarkdownView in Frameworks */,
@@ -737,6 +754,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F1BA00000000000000000202 /* FirebaseCrashlytics in Frameworks */,
F1BA00000000000000000201 /* FirebaseCore in Frameworks */,
DEVK00000000000000000004 /* DeviceKit in Frameworks */,
LOGG00000000000000000104 /* Logging in Frameworks */,
BAAD00000000000000000010 /* GRDB in Frameworks */,
@@ -826,6 +845,7 @@
TRMD00000000000000000030 = {
isa = PBXGroup;
children = (
F1BA00000000000000000020 /* Configuration/Firebase/GoogleService-Info.plist */,
BENC00000000000000000010 /* Benchmarks/PortForwarding */,
CB622EAF077B1253532F1E18 /* TFIWidgets */,
FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */,
@@ -875,6 +895,7 @@
AC89B83FA8E4154383D9AC81 /* Sources */,
7B09CFC5B32584951BD4C8F9 /* Frameworks */,
8F0C5245592A0E2AFB85E50F /* Resources */,
F1BA00000000000000003002 /* Upload Crashlytics Symbols */,
);
buildRules = (
);
@@ -894,6 +915,8 @@
);
name = TFI;
packageProductDependencies = (
F1BA00000000000000000002 /* FirebaseCrashlytics */,
F1BA00000000000000000001 /* FirebaseCore */,
DEVK00000000000000000002 /* DeviceKit */,
LOGG00000000000000000002 /* Logging */,
BAAD00000000000000000002 /* GRDB */,
@@ -933,6 +956,8 @@
);
name = FrameworkTests;
packageProductDependencies = (
F1BA00000000000000000002 /* FirebaseCrashlytics */,
F1BA00000000000000000001 /* FirebaseCore */,
LOGG00000000000000000002 /* Logging */,
BAAD00000000000000000002 /* GRDB */,
TRMD0000000000000010001B /* AgentKit */,
@@ -1015,6 +1040,8 @@
);
name = TermindInfrastructure;
packageProductDependencies = (
F1BA00000000000000000002 /* FirebaseCrashlytics */,
F1BA00000000000000000001 /* FirebaseCore */,
LOGG00000000000000000002 /* Logging */,
TRMD00000000000000100015 /* ZIPFoundation */,
);
@@ -1187,6 +1214,7 @@
TRMD00000000000000000020 /* Frameworks */,
TRMD00000000000000000080 /* Resources */,
TRMD00000000000000000085 /* Embed MCP Bridge */,
F1BA00000000000000002002 /* Upload Crashlytics Symbols */,
);
buildRules = (
);
@@ -1205,6 +1233,8 @@
);
name = TFM;
packageProductDependencies = (
F1BA00000000000000000002 /* FirebaseCrashlytics */,
F1BA00000000000000000001 /* FirebaseCore */,
DEVK00000000000000000002 /* DeviceKit */,
LOGG00000000000000000002 /* Logging */,
BAAD00000000000000000002 /* GRDB */,
@@ -1337,6 +1367,7 @@
mainGroup = TRMD00000000000000000030;
minimizedProjectReferenceProxies = 1;
packageReferences = (
F1BA00000000000000000010 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
DEVK00000000000000000001 /* XCRemoteSwiftPackageReference "DeviceKit" */,
BAAD00000000000000000001 /* XCRemoteSwiftPackageReference "GRDB.swift" */,
LOGG00000000000000000001 /* XCRemoteSwiftPackageReference "swift-log" */,
@@ -1420,6 +1451,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F1BA0000000000000000A002 /* Configuration/Firebase/GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1448,6 +1480,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F1BA0000000000000000A001 /* Configuration/Firebase/GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1460,6 +1493,55 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
F1BA00000000000000002002 /* Upload Crashlytics Symbols */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"$(BUILD_DIR)/../../SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run",
"$(BUILD_DIR)/../../SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Info.plist",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(PRODUCT_NAME)",
"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
"$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)",
);
name = "Upload Crashlytics Symbols";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"${CONFIGURATION}\" = \"Release\" ]; then\n \"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\" -gsp \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist\"\nfi\n";
};
F1BA00000000000000003002 /* Upload Crashlytics Symbols */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"$(BUILD_DIR)/../../SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run",
"$(BUILD_DIR)/../../SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Info.plist",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(PRODUCT_NAME)",
"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
"$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)",
);
name = "Upload Crashlytics Symbols";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"${CONFIGURATION}\" = \"Release\" ]; then\n \"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\" -gsp \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
01260326BA2579B3E3586B26 /* Sources */ = {
isa = PBXSourcesBuildPhase;
@@ -1923,6 +2005,7 @@
CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GENERATE_INFOPLIST_FILE = YES;
ICLOUD_CONTAINER_ENVIRONMENT = Development;
INFOPLIST_FILE = TFI/Info.plist;
@@ -2234,6 +2317,7 @@
CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GENERATE_INFOPLIST_FILE = YES;
ICLOUD_CONTAINER_ENVIRONMENT = Production;
INFOPLIST_FILE = TFI/Info.plist;
@@ -2486,6 +2570,7 @@
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 62;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -2523,6 +2608,7 @@
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 62;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -2797,6 +2883,14 @@
minimumVersion = 5.9.0;
};
};
F1BA00000000000000000010 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git";
requirement = {
kind = exactVersion;
version = 12.19.2;
};
};
LOGG00000000000000000001 /* XCRemoteSwiftPackageReference "swift-log" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-log.git";
@@ -2882,6 +2976,16 @@
package = DEVK00000000000000000001 /* XCRemoteSwiftPackageReference "DeviceKit" */;
productName = DeviceKit;
};
F1BA00000000000000000001 /* FirebaseCore */ = {
isa = XCSwiftPackageProductDependency;
package = F1BA00000000000000000010 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseCore;
};
F1BA00000000000000000002 /* FirebaseCrashlytics */ = {
isa = XCSwiftPackageProductDependency;
package = F1BA00000000000000000010 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseCrashlytics;
};
LOGG00000000000000000002 /* Logging */ = {
isa = XCSwiftPackageProductDependency;
package = LOGG00000000000000000001 /* XCRemoteSwiftPackageReference "swift-log" */;
@@ -1,6 +1,15 @@
{
"originHash" : "ea2353e26e14490b696f9bc5c68235bf92924ce177756bb5217c4cc234874a31",
"originHash" : "db5fde46a07b58b12a3db946618d16496080b8cba0501fc05694825a35f1a1b3",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "agentkit",
"kind" : "remoteSourceControl",
@@ -10,6 +19,15 @@
"version" : "0.18.1"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "97f7d74dd0e8f3d0fe5fc75cc8957aa73bdc948a",
"version" : "11.3.2"
}
},
{
"identity" : "choreographer",
"kind" : "remoteSourceControl",
@@ -28,6 +46,15 @@
"version" : "5.9.0"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk.git",
"state" : {
"revision" : "8c29ca981990a32e626c8617544fa65d22b1834f",
"version" : "12.19.2"
}
},
{
"identity" : "glyphixtextfx",
"kind" : "remoteSourceControl",
@@ -37,6 +64,42 @@
"version" : "2.3.6"
}
},
{
"identity" : "google-ads-on-device-conversion-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
"state" : {
"revision" : "0208e2681ec81f8a9c81084696f60fcce26cb7ee",
"version" : "3.7.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "b8455dd2ff55cc269fda7a4f9eac6c1e3588c53f",
"version" : "12.19.2"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "ba3358d3c3dbae8ef230b58a46b97ad65e84e974",
"version" : "10.1.1"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "cc31f5ed464b111f877ce8554cbef4455d410d91",
"version" : "8.1.4"
}
},
{
"identity" : "grdb.swift",
"kind" : "remoteSourceControl",
@@ -46,6 +109,24 @@
"version" : "7.11.1"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
"version" : "1.69.1"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "724a52eea6329b7e12d3ad8300d76ca9f3895fcc",
"version" : "5.3.1"
}
},
{
"identity" : "highlightr",
"kind" : "remoteSourceControl",
@@ -55,6 +136,24 @@
"version" : "2.3.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "libghostty-spm",
"kind" : "remoteSourceControl",
@@ -118,6 +217,15 @@
"version" : "2.2.0"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
"version" : "2.30910.1"
}
},
{
"identity" : "openssl-spm",
"kind" : "remoteSourceControl",
@@ -127,6 +235,15 @@
"version" : "3.6.2"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
"version" : "2.4.1"
}
},
{
"identity" : "respring",
"kind" : "remoteSourceControl",