Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Convos/Config/QALaunchHooks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import ConvosCore
import Foundation
import Security

/// Launch-time hooks for QA automation. Every hook is double-gated: it
/// only runs in non-production environments and only when its environment
/// variable is set (pass to a simulator app by exporting the
/// SIMCTL_CHILD_-prefixed name before `xcrun simctl launch`).
enum QALaunchHooks {
static func run(environment: AppEnvironment) {
guard !environment.isProduction else { return }
wipePrimaryIdentityIfRequested(environment: environment)
}

/// CONVOS_QA_WIPE_PRIMARY_IDENTITY=1 deletes the device-local identity
/// slot while leaving the iCloud-synced backup slot intact. iCloud
/// Keychain doesn't sync between simulators, so two-simulator QA seeds
/// the "brand-new device on the same iCloud account" state by cloning
/// the onboarded simulator (clones copy the whole keychain) and wiping
/// the cloned primary slot with this hook on first launch. The app then
/// registers a fresh placeholder identity and finds the original
/// device's synced backup, which drives the first-install
/// "Pair <device>?" prompt. See qa/tests/44-icloud-pairing-prompt.md.
private static func wipePrimaryIdentityIfRequested(environment: AppEnvironment) {
guard ProcessInfo.processInfo.environment["CONVOS_QA_WIPE_PRIMARY_IDENTITY"] == "1" else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: KeychainIdentityStore.defaultService,
kSecAttrAccount as String: KeychainIdentityStore.identityAccount,
kSecAttrAccessGroup as String: environment.keychainAccessGroup,
kSecAttrSynchronizable as String: false
]
let status = SecItemDelete(query as CFDictionary)
Log.info("QALaunchHooks: wiped primary identity slot (status=\(status))")
QAEvent.emit(.app, "qa_wiped_primary_identity", ["status": "\(status)"])
}
}
40 changes: 35 additions & 5 deletions Convos/Conversations List/ConversationsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,26 @@ private struct ConversationsSheetModifier: ViewModifier {
)
}

/// Routes every dismissal of the joiner pairing sheet through
/// `dismissJoinerPairing()` so the ephemeral pairing client is torn
/// down before the reference drops. Interactive (swipe) dismissal
/// nils a plain item binding before `onDismiss` runs, which would
/// orphan the joiner's streaming task and temp database - the same
/// gap `MainTabSheetsModifier.incomingPairingBinding` closes for the
/// initiator sheet.
private var joinerPairingBinding: Binding<JoinerPairingSheetViewModel?> {
Binding(
get: { viewModel.pendingJoinerPairing },
set: { newValue in
if newValue == nil {
viewModel.dismissJoinerPairing()
} else {
viewModel.pendingJoinerPairing = newValue
}
}
)
}

func body(content: Content) -> some View {
content
// The `NewConversationView` and `AgentBuilderView` sheets
Expand All @@ -325,16 +345,26 @@ private struct ConversationsSheetModifier: ViewModifier {
.presentationDetents([.medium])
}
.selfSizingSheet(
item: $viewModel.pendingJoinerPairing,
onDismiss: {
viewModel.pendingPairDevice = nil
viewModel.pendingJoinerPairing = nil
},
item: joinerPairingBinding,
content: { pairingVM in
JoinerPairingSheetView(viewModel: pairingVM)
.padding(.top, DesignConstants.Spacing.step5x)
}
)
.selfSizingSheet(
item: $viewModel.foundDevicePairingPrompt,
onDismiss: {
viewModel.onFoundDevicePromptDismissed()
},
content: { prompt in
PairFoundDeviceInfoSheet(
deviceName: prompt.deviceName,
onPair: { viewModel.pairWithFoundDevice() },
onSkip: { viewModel.skipFoundDevicePairing() }
)
.padding(.top, DesignConstants.Spacing.step5x)
}
)
.selfSizingSheet(isPresented: $viewModel.presentingExplodeInfo) {
ExplodeInfoView()
}
Expand Down
Loading
Loading