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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ public struct PasswordImportReport: Sendable, Equatable, Codable {
public var imported: Int { store.added }
/// Everything that did not become a new saved password.
public var notImported: Int { skipped.total + store.duplicate + store.conflict + store.rejected }
/// Sign-ins cmux already has for that site and username with another
/// password: the saved one was kept. Summaries show this count on its own
/// so a differing password is never dropped silently.
public var conflicts: Int { store.conflict }
/// What did not become a new saved password, other than `conflicts`.
public var notImportedOtherThanConflicts: Int { notImported - conflicts }
}

/// Imports one Chromium source profile's saved passwords into one cmux
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ public final class ImportStepModel {
return items
}

/// Whether a Firefox profile is among them: its key is in the profile, not the Keychain.
public var passwordsIncludeFirefox: Bool { passwordProfiles.contains { $0.browser.family == .firefox } }

public var isConfirmingPasswords: Bool { phase == .confirmingPasswords }

public var canStart: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@ enum OnboardingStrings {
defaultValue: "Import asks you to confirm with Touch ID or your password. Then macOS asks whether cmux may use %@, the Keychain item the browser locks its saved passwords with. Choose Allow, and cmux unlocks them once, on this Mac.",
bundle: .module), items)
}
/// Only Firefox profiles chosen: no Keychain item to allow.
static var passwordsFirefoxOnly: String {
String(localized: "onboarding.passwords.firefoxOnly",
defaultValue: "Import asks you to confirm with Touch ID or your password. Firefox keeps its key in the profile, so macOS asks for no Keychain item. If a Firefox profile has a primary password, cmux asks for it once and does not keep it.",
bundle: .module)
}
/// Follows the Keychain paragraph when the choice mixes Chromium browsers and Firefox.
static var passwordsFirefoxPrimary: String {
String(localized: "onboarding.passwords.firefoxPrimary",
defaultValue: "If a Firefox profile has a primary password, cmux asks for it once and does not keep it.", bundle: .module)
}
/// The consent screen's paragraph: Chromium browsers name the Keychain items macOS asks about
/// (`quote` puts them in quotation marks); Firefox keeps its key in the profile.
static func passwordsConsent(keychainItems: [String], includesFirefox: Bool, quote: ([String]) -> String) -> String {
guard !keychainItems.isEmpty else { return passwordsFirefoxOnly }
let keychain = passwordsKeychain(quote(keychainItems))
return includesFirefox ? keychain + " " + passwordsFirefoxPrimary : keychain
}
/// macOS shows it as “cmux is trying to …” in the Touch ID sheet.
static var passwordsAuthReason: String {
String(localized: "onboarding.passwords.authReason", defaultValue: "import saved passwords from your other browsers", bundle: .module)
Expand All @@ -87,6 +105,22 @@ enum OnboardingStrings {
defaultValue: "They go into cmux’s own encrypted password store, where autofill finds them. Agents never see them, and nothing is read until you click Import.",
bundle: .module)
}
/// "Already saved with a different password: 2. cmux kept the saved password."
static func passwordsConflicts(_ count: String) -> String {
String(format: String(localized: "onboarding.import.passwordsConflicts",
defaultValue: "Already saved with a different password: %@. cmux kept the saved password.",
bundle: .module), count)
}
/// The finished line's password counts, counts only: skipped (other than conflicts), then
/// differing passwords that were kept. Empty when there is neither.
static func passwordsSummary(_ reports: [PasswordImportReport]) -> String {
let skipped = reports.reduce(0) { $0 + $1.notImportedOtherThanConflicts }
let conflicts = reports.reduce(0) { $0 + $1.conflicts }
var parts: [String] = []
if skipped > 0 { parts.append(passwordsSkipped(skipped.formatted(.number))) }
if conflicts > 0 { parts.append(passwordsConflicts(conflicts.formatted(.number))) }
return parts.joined(separator: " ")
}
/// "Passwords skipped: 9" (already saved, or not a web sign-in).
static func passwordsSkipped(_ count: String) -> String {
String(format: String(localized: "onboarding.import.passwordsSkipped", defaultValue: "Passwords skipped: %@", bundle: .module), count)
Expand Down
Loading
Loading