fix(ios): address App Store review requirements

Use compliant permission prompts, align cloud ASR privacy disclosures, and default Flow sessions to a shorter inactivity window for safer resubmission.
This commit is contained in:
Rocky
2026-07-23 15:37:38 +08:00
parent c0c9dad149
commit e6f99d2744
18 changed files with 178 additions and 83 deletions
@@ -52,6 +52,9 @@ public struct AppGroupConfiguration: Sendable, Equatable {
public static let flowSkipAppSwitch = "config.flowSkipAppSwitch"
/// Raw `FlowInactivityDuration` value; session expires after this idle window.
public static let flowInactivityDuration = "config.flowInactivityDuration"
/// One-shot: remap previous product defaults (30m / 10m) 5m.
public static let flowInactivityMigratedToFiveMinuteDefault =
"config.flowInactivityDuration.migratedToFiveMinuteDefault"
/// Diagnostic switch: when false, local ASR skips the custom language model.
public static let localASRCustomLanguageModelEnabled = "config.localASR.customLanguageModelEnabled"
}
@@ -310,14 +313,9 @@ public struct AppGroupConfiguration: Sendable, Equatable {
preferICloudSync: config.settingsICloudSyncEnabled
)
// One-shot default migration for installs that predate an explicit
// stored value. The privacy-safe defaults ("local", 30 min TTL) are
// for NEW installs only an existing user who ran on the old
// defaults must keep their behavior, both because silently changing
// engines under someone is wrong, and because iCloud settings sync
// would stamp the flip as a fresh "edit" and propagate it to every
// other device, overriding choices made there. Persisting the
// resolved value makes the decision stable and sync-invisible.
// One-shot defaults for installs that predate explicit settings.
// Preserve the legacy engine choice, but use the current privacy-safe
// inactivity duration when the user has never selected one.
let isExistingInstall = defaults.bool(forKey: Keys.hasCompletedOnboarding)
if defaults.string(forKey: Keys.engineMode) == nil {
let resolved = isExistingInstall ? "cloud" : "local"
@@ -325,9 +323,23 @@ public struct AppGroupConfiguration: Sendable, Equatable {
defaults.set(resolved, forKey: Keys.engineMode)
}
if defaults.string(forKey: Keys.flowInactivityDuration) == nil {
let resolved: FlowInactivityDuration = isExistingInstall ? .twelveHours : .default
let resolved = FlowInactivityDuration.default
config.flowInactivityDuration = resolved
defaults.set(resolved.rawValue, forKey: Keys.flowInactivityDuration)
defaults.set(true, forKey: Keys.flowInactivityMigratedToFiveMinuteDefault)
} else if !defaults.bool(forKey: Keys.flowInactivityMigratedToFiveMinuteDefault) {
// Previous product defaults were 30m then briefly 10m. Remap those
// once so existing installs pick up the new 5-minute default; users
// who later choose 30m / 10m again keep that choice.
let previousDefaults: Set<String> = [
FlowInactivityDuration.thirtyMinutes.rawValue,
FlowInactivityDuration.tenMinutes.rawValue,
]
if previousDefaults.contains(config.flowInactivityDuration.rawValue) {
config.flowInactivityDuration = .default
defaults.set(FlowInactivityDuration.default.rawValue, forKey: Keys.flowInactivityDuration)
}
defaults.set(true, forKey: Keys.flowInactivityMigratedToFiveMinuteDefault)
}
// Cloud no longer exposes off/transcribe; migrate legacy values.
@@ -7,6 +7,8 @@
import Foundation
public enum FlowInactivityDuration: String, CaseIterable, Identifiable, Sendable, Codable {
case oneMinute = "1m"
case fiveMinutes = "5m"
case tenMinutes = "10m"
case thirtyMinutes = "30m"
case threeHours = "3h"
@@ -15,15 +17,13 @@ public enum FlowInactivityDuration: String, CaseIterable, Identifiable, Sendable
public var id: String { rawValue }
/// 30 minutes, not hours: competitors cap sessions at 560 min for a
/// reason a very long TTL keeps advertising "session active" long after
/// the host process is likely suspended or dead, amplifying every stale-
/// state bug into hours of confusing UI. Users can still opt into longer
/// windows explicitly.
public static let `default`: FlowInactivityDuration = .thirtyMinutes
/// Privacy-safe default: users can choose a longer window explicitly.
public static let `default`: FlowInactivityDuration = .fiveMinutes
public var timeInterval: TimeInterval {
switch self {
case .oneMinute: return 60
case .fiveMinutes: return 5 * 60
case .tenMinutes: return 10 * 60
case .thirtyMinutes: return 30 * 60
case .threeHours: return 3 * 60 * 60
@@ -34,6 +34,8 @@ public enum FlowInactivityDuration: String, CaseIterable, Identifiable, Sendable
public var labelKey: String {
switch self {
case .oneMinute: return "settings.flow.inactivity.1m"
case .fiveMinutes: return "settings.flow.inactivity.5m"
case .tenMinutes: return "settings.flow.inactivity.10m"
case .thirtyMinutes: return "settings.flow.inactivity.30m"
case .threeHours: return "settings.flow.inactivity.3h"