mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
* ios: fix XCode 16 regressions (tap not working on files, quotes, images, voice messages, etc.), open link previews on tap * fix voice recording * fix video, accepting calls from chat, preference toggles in chat * WIP message and meta * handle links in attributed strings * custom attribute for links to prevent race conditions with default tap handler
778 lines
32 KiB
Swift
778 lines
32 KiB
Swift
//
|
|
// PrivacySettings.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by Evgeny on 29/05/2022.
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SimpleXChat
|
|
|
|
struct PrivacySettings: View {
|
|
@EnvironmentObject var m: ChatModel
|
|
@EnvironmentObject var theme: AppTheme
|
|
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
|
|
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
|
|
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
|
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
|
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
|
|
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
|
|
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
|
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
|
@AppStorage(DEFAULT_PRIVACY_SHORT_LINKS) private var shortSimplexLinks = false
|
|
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
|
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
|
@State private var currentLAMode = privacyLocalAuthModeDefault.get()
|
|
@AppStorage(DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) private var privacyMediaBlurRadius: Int = 0
|
|
@State private var contactReceipts = false
|
|
@State private var contactReceiptsReset = false
|
|
@State private var contactReceiptsOverrides = 0
|
|
@State private var contactReceiptsDialogue = false
|
|
@State private var groupReceipts = false
|
|
@State private var groupReceiptsReset = false
|
|
@State private var groupReceiptsOverrides = 0
|
|
@State private var groupReceiptsDialogue = false
|
|
@State private var alert: PrivacySettingsViewAlert?
|
|
|
|
enum PrivacySettingsViewAlert: Identifiable {
|
|
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
|
|
|
var id: String {
|
|
switch self {
|
|
case let .error(title, _): return "error \(title)"
|
|
}
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
VStack {
|
|
List {
|
|
Section(header: Text("Device").foregroundColor(theme.colors.secondary)) {
|
|
NavigationLink {
|
|
SimplexLockView(prefPerformLA: $prefPerformLA, currentLAMode: $currentLAMode)
|
|
.navigationTitle("SimpleX Lock")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
} label: {
|
|
if prefPerformLA {
|
|
settingsRow("lock.fill", color: .green) {
|
|
simplexLockRow(currentLAMode.text)
|
|
}
|
|
} else {
|
|
settingsRow("lock", color: theme.colors.secondary) {
|
|
simplexLockRow("Off")
|
|
}
|
|
}
|
|
}
|
|
settingsRow("eye.slash", color: theme.colors.secondary) {
|
|
Toggle("Protect app screen", isOn: $protectScreen)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
settingsRow("network", color: theme.colors.secondary) {
|
|
Toggle("Send link previews", isOn: $useLinkPreviews)
|
|
.onChange(of: useLinkPreviews) { linkPreviews in
|
|
privacyLinkPreviewsGroupDefault.set(linkPreviews)
|
|
}
|
|
}
|
|
settingsRow("message", color: theme.colors.secondary) {
|
|
Toggle("Show last messages", isOn: $showChatPreviews)
|
|
}
|
|
settingsRow("rectangle.and.pencil.and.ellipsis", color: theme.colors.secondary) {
|
|
Toggle("Message draft", isOn: $saveLastDraft)
|
|
}
|
|
.onChange(of: saveLastDraft) { saveDraft in
|
|
if !saveDraft {
|
|
m.draft = nil
|
|
m.draftChatId = nil
|
|
}
|
|
}
|
|
settingsRow("link", color: theme.colors.secondary) {
|
|
Picker("SimpleX links", selection: $simplexLinkMode) {
|
|
ForEach(
|
|
SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode])
|
|
) { mode in
|
|
Text(mode.text)
|
|
}
|
|
}
|
|
}
|
|
.frame(height: 36)
|
|
.onChange(of: simplexLinkMode) { mode in
|
|
privacySimplexLinkModeDefault.set(mode)
|
|
}
|
|
if developerTools {
|
|
settingsRow("link.badge.plus", color: theme.colors.secondary) {
|
|
Toggle("Use short links (BETA)", isOn: $shortSimplexLinks)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Chats")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
|
|
Section {
|
|
settingsRow("lock.doc", color: theme.colors.secondary) {
|
|
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
|
|
.onChange(of: encryptLocalFiles) {
|
|
setEncryptLocalFiles($0)
|
|
}
|
|
}
|
|
settingsRow("photo", color: theme.colors.secondary) {
|
|
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
|
.onChange(of: autoAcceptImages) {
|
|
privacyAcceptImagesGroupDefault.set($0)
|
|
}
|
|
}
|
|
settingsRow("circle.filled.pattern.diagonalline.rectangle", color: theme.colors.secondary) {
|
|
Picker("Blur media", selection: $privacyMediaBlurRadius) {
|
|
let values = [0, 12, 24, 48] + ([0, 12, 24, 48].contains(privacyMediaBlurRadius) ? [] : [privacyMediaBlurRadius])
|
|
ForEach(values, id: \.self) { radius in
|
|
let text: String = switch radius {
|
|
case 0: NSLocalizedString("Off", comment: "blur media")
|
|
case 12: NSLocalizedString("Soft", comment: "blur media")
|
|
case 24: NSLocalizedString("Medium", comment: "blur media")
|
|
case 48: NSLocalizedString("Strong", comment: "blur media")
|
|
default: "\(radius)"
|
|
}
|
|
Text(text)
|
|
}
|
|
}
|
|
}
|
|
.frame(height: 36)
|
|
settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) {
|
|
Toggle("Protect IP address", isOn: $askToApproveRelays)
|
|
}
|
|
} header: {
|
|
Text("Files")
|
|
.foregroundColor(theme.colors.secondary)
|
|
} footer: {
|
|
if askToApproveRelays {
|
|
Text("The app will ask to confirm downloads from unknown file servers (except .onion).")
|
|
.foregroundColor(theme.colors.secondary)
|
|
} else {
|
|
Text("Without Tor or VPN, your IP address will be visible to file servers.")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
settingsRow("person", color: theme.colors.secondary) {
|
|
Toggle("Contacts", isOn: $contactReceipts)
|
|
}
|
|
settingsRow("person.2", color: theme.colors.secondary) {
|
|
Toggle("Small groups (max 20)", isOn: $groupReceipts)
|
|
}
|
|
} header: {
|
|
Text("Send delivery receipts to")
|
|
.foregroundColor(theme.colors.secondary)
|
|
} footer: {
|
|
VStack(alignment: .leading) {
|
|
Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.")
|
|
Text("They can be overridden in contact and group settings.")
|
|
}
|
|
.foregroundColor(theme.colors.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.confirmationDialog(contactReceiptsDialogTitle, isPresented: $contactReceiptsDialogue, titleVisibility: .visible) {
|
|
Button(contactReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") {
|
|
setSendReceiptsContacts(contactReceipts, clearOverrides: false)
|
|
}
|
|
Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) {
|
|
setSendReceiptsContacts(contactReceipts, clearOverrides: true)
|
|
}
|
|
Button("Cancel", role: .cancel) {
|
|
contactReceiptsReset = true
|
|
contactReceipts.toggle()
|
|
}
|
|
}
|
|
.confirmationDialog(groupReceiptsDialogTitle, isPresented: $groupReceiptsDialogue, titleVisibility: .visible) {
|
|
Button(groupReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") {
|
|
setSendReceiptsGroups(groupReceipts, clearOverrides: false)
|
|
}
|
|
Button(groupReceipts ? "Enable for all" : "Disable for all", role: .destructive) {
|
|
setSendReceiptsGroups(groupReceipts, clearOverrides: true)
|
|
}
|
|
Button("Cancel", role: .cancel) {
|
|
groupReceiptsReset = true
|
|
groupReceipts.toggle()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: contactReceipts) { _ in
|
|
if contactReceiptsReset {
|
|
contactReceiptsReset = false
|
|
} else {
|
|
setOrAskSendReceiptsContacts(contactReceipts)
|
|
}
|
|
}
|
|
.onChange(of: groupReceipts) { _ in
|
|
if groupReceiptsReset {
|
|
groupReceiptsReset = false
|
|
} else {
|
|
setOrAskSendReceiptsGroups(groupReceipts)
|
|
}
|
|
}
|
|
.onAppear {
|
|
if let u = m.currentUser {
|
|
if contactReceipts != u.sendRcptsContacts {
|
|
contactReceiptsReset = true
|
|
contactReceipts = u.sendRcptsContacts
|
|
}
|
|
if groupReceipts != u.sendRcptsSmallGroups {
|
|
groupReceiptsReset = true
|
|
groupReceipts = u.sendRcptsSmallGroups
|
|
}
|
|
}
|
|
}
|
|
.alert(item: $alert) { alert in
|
|
switch alert {
|
|
case let .error(title, error):
|
|
return Alert(title: Text(title), message: Text(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setEncryptLocalFiles(_ enable: Bool) {
|
|
do {
|
|
try apiSetEncryptLocalFiles(enable)
|
|
} catch let error {
|
|
let err = responseError(error)
|
|
logger.error("apiSetEncryptLocalFiles \(err)")
|
|
alert = .error(title: "Error", error: "\(err)")
|
|
}
|
|
}
|
|
|
|
private func setOrAskSendReceiptsContacts(_ enable: Bool) {
|
|
contactReceiptsOverrides = m.chats.reduce(0) { count, chat in
|
|
let sendRcpts = chat.chatInfo.contact?.chatSettings.sendRcpts
|
|
return count + (sendRcpts == nil || sendRcpts == enable ? 0 : 1)
|
|
}
|
|
if contactReceiptsOverrides == 0 {
|
|
setSendReceiptsContacts(enable, clearOverrides: false)
|
|
} else {
|
|
contactReceiptsDialogue = true
|
|
}
|
|
}
|
|
|
|
private var contactReceiptsDialogTitle: LocalizedStringKey {
|
|
contactReceipts
|
|
? "Sending receipts is disabled for \(contactReceiptsOverrides) contacts"
|
|
: "Sending receipts is enabled for \(contactReceiptsOverrides) contacts"
|
|
}
|
|
|
|
private func setSendReceiptsContacts(_ enable: Bool, clearOverrides: Bool) {
|
|
Task {
|
|
do {
|
|
if let currentUser = m.currentUser {
|
|
let userMsgReceiptSettings = UserMsgReceiptSettings(enable: enable, clearOverrides: clearOverrides)
|
|
try await apiSetUserContactReceipts(currentUser.userId, userMsgReceiptSettings: userMsgReceiptSettings)
|
|
privacyDeliveryReceiptsSet.set(true)
|
|
await MainActor.run {
|
|
var updatedUser = currentUser
|
|
updatedUser.sendRcptsContacts = enable
|
|
m.updateUser(updatedUser)
|
|
if clearOverrides {
|
|
m.chats.forEach { chat in
|
|
if var contact = chat.chatInfo.contact {
|
|
let sendRcpts = contact.chatSettings.sendRcpts
|
|
if sendRcpts != nil && sendRcpts != enable {
|
|
contact.chatSettings.sendRcpts = nil
|
|
m.updateContact(contact)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch let error {
|
|
alert = .error(title: "Error setting delivery receipts!", error: "Error: \(responseError(error))")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setOrAskSendReceiptsGroups(_ enable: Bool) {
|
|
groupReceiptsOverrides = m.chats.reduce(0) { count, chat in
|
|
let sendRcpts = chat.chatInfo.groupInfo?.chatSettings.sendRcpts
|
|
return count + (sendRcpts == nil || sendRcpts == enable ? 0 : 1)
|
|
}
|
|
if groupReceiptsOverrides == 0 {
|
|
setSendReceiptsGroups(enable, clearOverrides: false)
|
|
} else {
|
|
groupReceiptsDialogue = true
|
|
}
|
|
}
|
|
|
|
private var groupReceiptsDialogTitle: LocalizedStringKey {
|
|
groupReceipts
|
|
? "Sending receipts is disabled for \(groupReceiptsOverrides) groups"
|
|
: "Sending receipts is enabled for \(groupReceiptsOverrides) groups"
|
|
}
|
|
|
|
private func setSendReceiptsGroups(_ enable: Bool, clearOverrides: Bool) {
|
|
Task {
|
|
do {
|
|
if let currentUser = m.currentUser {
|
|
let userMsgReceiptSettings = UserMsgReceiptSettings(enable: enable, clearOverrides: clearOverrides)
|
|
try await apiSetUserGroupReceipts(currentUser.userId, userMsgReceiptSettings: userMsgReceiptSettings)
|
|
privacyDeliveryReceiptsSet.set(true)
|
|
await MainActor.run {
|
|
var updatedUser = currentUser
|
|
updatedUser.sendRcptsSmallGroups = enable
|
|
m.updateUser(updatedUser)
|
|
if clearOverrides {
|
|
m.chats.forEach { chat in
|
|
if var groupInfo = chat.chatInfo.groupInfo {
|
|
let sendRcpts = groupInfo.chatSettings.sendRcpts
|
|
if sendRcpts != nil && sendRcpts != enable {
|
|
groupInfo.chatSettings.sendRcpts = nil
|
|
m.updateGroup(groupInfo)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch let error {
|
|
alert = .error(title: "Error setting delivery receipts!", error: "Error: \(responseError(error))")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func simplexLockRow(_ value: LocalizedStringKey) -> some View {
|
|
HStack {
|
|
Text("SimpleX Lock")
|
|
Spacer()
|
|
Text(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum LAMode: String, Identifiable, CaseIterable {
|
|
case system
|
|
case passcode
|
|
|
|
public var id: Self { self }
|
|
|
|
var text: LocalizedStringKey {
|
|
switch self {
|
|
case .system: return "System"
|
|
case .passcode: return "Passcode"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SimplexLockView: View {
|
|
@Binding var prefPerformLA: Bool
|
|
@Binding var currentLAMode: LAMode
|
|
@EnvironmentObject var m: ChatModel
|
|
@EnvironmentObject var theme: AppTheme
|
|
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
|
@State private var laMode: LAMode = privacyLocalAuthModeDefault.get()
|
|
@AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30
|
|
@State private var performLA: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
|
@State private var selfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT)
|
|
@State private var currentSelfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT)
|
|
@AppStorage(DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) private var selfDestructDisplayName = ""
|
|
@AppStorage(GROUP_DEFAULT_ALLOW_SHARE_EXTENSION, store: groupDefaults) private var allowShareExtension = false
|
|
@State private var performLAToggleReset = false
|
|
@State private var performLAModeReset = false
|
|
@State private var performLASelfDestructReset = false
|
|
@State private var showPasswordAction: PasswordAction? = nil
|
|
@State private var showChangePassword = false
|
|
@State var laAlert: LASettingViewAlert? = nil
|
|
|
|
enum LASettingViewAlert: Identifiable {
|
|
case laTurnedOnAlert
|
|
case laFailedAlert
|
|
case laUnavailableInstructionAlert
|
|
case laUnavailableTurningOffAlert
|
|
case laPasscodeSetAlert
|
|
case laPasscodeChangedAlert
|
|
case laSelfDestructPasscodeSetAlert
|
|
case laSelfDestructPasscodeChangedAlert
|
|
case laPasscodeNotChangedAlert
|
|
|
|
var id: Self { self }
|
|
}
|
|
|
|
enum PasswordAction: Identifiable {
|
|
case enableAuth
|
|
case toggleMode
|
|
case changePasscode
|
|
case enableSelfDestruct
|
|
case changeSelfDestructPasscode
|
|
case selfDestructInfo
|
|
|
|
var id: Self { self }
|
|
}
|
|
|
|
let laDelays: [Int] = [10, 30, 60, 180, 600, 0]
|
|
|
|
func laDelayText(_ t: Int) -> LocalizedStringKey {
|
|
let m = t / 60
|
|
let s = t % 60
|
|
return t == 0
|
|
? "Immediately"
|
|
: m == 0 || s != 0
|
|
? "\(s) seconds" // there are no options where both minutes and seconds are needed
|
|
: "\(m) minutes"
|
|
}
|
|
|
|
var body: some View {
|
|
VStack {
|
|
List {
|
|
Section("") {
|
|
Toggle("Enable lock", isOn: $performLA)
|
|
Picker("Lock mode", selection: $laMode) {
|
|
ForEach(LAMode.allCases) { mode in
|
|
Text(mode.text)
|
|
}
|
|
}
|
|
.frame(height: 36)
|
|
if performLA {
|
|
Picker("Lock after", selection: $laLockDelay) {
|
|
let delays = laDelays.contains(laLockDelay) ? laDelays : [laLockDelay] + laDelays
|
|
ForEach(delays, id: \.self) { t in
|
|
Text(laDelayText(t))
|
|
}
|
|
}
|
|
.frame(height: 36)
|
|
if showChangePassword && laMode == .passcode {
|
|
Button("Change passcode") {
|
|
changeLAPassword()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if performLA {
|
|
Section("Share to SimpleX") {
|
|
Toggle("Allow sharing", isOn: $allowShareExtension)
|
|
}
|
|
}
|
|
|
|
if performLA && laMode == .passcode {
|
|
Section(header: Text("Self-destruct passcode").foregroundColor(theme.colors.secondary)) {
|
|
Toggle(isOn: $selfDestruct) {
|
|
HStack(spacing: 6) {
|
|
Text("Enable self-destruct")
|
|
Image(systemName: "info.circle")
|
|
.foregroundColor(theme.colors.primary)
|
|
.font(.system(size: 14))
|
|
}
|
|
.onTapGesture {
|
|
showPasswordAction = .selfDestructInfo
|
|
}
|
|
}
|
|
if selfDestruct {
|
|
TextField("New display name", text: $selfDestructDisplayName)
|
|
Button("Change self-destruct passcode") {
|
|
changeSelfDestructPassword()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: performLA) { performLAToggle in
|
|
appLocalAuthEnabledGroupDefault.set(performLAToggle)
|
|
prefLANoticeShown = true
|
|
if performLAToggleReset {
|
|
performLAToggleReset = false
|
|
} else if performLAToggle {
|
|
switch currentLAMode {
|
|
case .system:
|
|
enableLA()
|
|
case .passcode:
|
|
resetLA()
|
|
showPasswordAction = .enableAuth
|
|
}
|
|
} else {
|
|
disableLA()
|
|
}
|
|
}
|
|
.onChange(of: laMode) { _ in
|
|
if performLAModeReset {
|
|
performLAModeReset = false
|
|
} else if prefPerformLA {
|
|
toggleLAMode()
|
|
} else {
|
|
updateLAMode()
|
|
}
|
|
}
|
|
.onChange(of: selfDestruct) { _ in
|
|
if performLASelfDestructReset {
|
|
performLASelfDestructReset = false
|
|
} else if prefPerformLA {
|
|
toggleSelfDestruct()
|
|
}
|
|
}
|
|
.alert(item: $laAlert) { alertItem in
|
|
switch alertItem {
|
|
case .laTurnedOnAlert: return laTurnedOnAlert()
|
|
case .laFailedAlert: return laFailedAlert()
|
|
case .laUnavailableInstructionAlert: return laUnavailableInstructionAlert()
|
|
case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert()
|
|
case .laPasscodeSetAlert: return passcodeAlert("Passcode set!")
|
|
case .laPasscodeChangedAlert: return passcodeAlert("Passcode changed!")
|
|
case .laSelfDestructPasscodeSetAlert: return selfDestructPasscodeAlert("Self-destruct passcode enabled!")
|
|
case .laSelfDestructPasscodeChangedAlert: return selfDestructPasscodeAlert("Self-destruct passcode changed!")
|
|
case .laPasscodeNotChangedAlert: return mkAlert(title: "Passcode not changed!")
|
|
}
|
|
}
|
|
.sheet(item: $showPasswordAction) { a in
|
|
switch a {
|
|
case .enableAuth:
|
|
SetAppPasscodeView {
|
|
m.contentViewAccessAuthenticated = true
|
|
laLockDelay = 30
|
|
prefPerformLA = true
|
|
showChangePassword = true
|
|
showLAAlert(.laPasscodeSetAlert)
|
|
} cancel: {
|
|
resetLAEnabled(false)
|
|
}
|
|
case .toggleMode:
|
|
SetAppPasscodeView {
|
|
laLockDelay = 30
|
|
updateLAMode()
|
|
showChangePassword = true
|
|
showLAAlert(.laPasscodeSetAlert)
|
|
} cancel: {
|
|
revertLAMode()
|
|
}
|
|
case .changePasscode:
|
|
SetAppPasscodeView {
|
|
showLAAlert(.laPasscodeChangedAlert)
|
|
} cancel: {
|
|
showLAAlert(.laPasscodeNotChangedAlert)
|
|
}
|
|
case .enableSelfDestruct:
|
|
SetAppPasscodeView(
|
|
passcodeKeychain: kcSelfDestructPassword,
|
|
prohibitedPasscodeKeychain: kcAppPassword,
|
|
title: "Set passcode",
|
|
reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")
|
|
) {
|
|
updateSelfDestruct()
|
|
showLAAlert(.laSelfDestructPasscodeSetAlert)
|
|
} cancel: {
|
|
revertSelfDestruct()
|
|
}
|
|
case .changeSelfDestructPasscode:
|
|
SetAppPasscodeView(
|
|
passcodeKeychain: kcSelfDestructPassword,
|
|
prohibitedPasscodeKeychain: kcAppPassword,
|
|
reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")
|
|
) {
|
|
showLAAlert(.laSelfDestructPasscodeChangedAlert)
|
|
} cancel: {
|
|
showLAAlert(.laPasscodeNotChangedAlert)
|
|
}
|
|
case .selfDestructInfo:
|
|
selfDestructInfoView()
|
|
}
|
|
}
|
|
.onAppear {
|
|
showChangePassword = prefPerformLA && currentLAMode == .passcode
|
|
}
|
|
.onDisappear() {
|
|
m.laRequest = nil
|
|
}
|
|
}
|
|
|
|
private func selfDestructInfoView() -> some View {
|
|
VStack(alignment: .leading) {
|
|
Text("Self-destruct")
|
|
.font(.largeTitle)
|
|
.bold()
|
|
.padding(.vertical)
|
|
ScrollView {
|
|
VStack(alignment: .leading) {
|
|
Group {
|
|
Text("If you enter your self-destruct passcode while opening the app:")
|
|
VStack(spacing: 8) {
|
|
textListItem("1.", "All app data is deleted.")
|
|
textListItem("2.", "App passcode is replaced with self-destruct passcode.")
|
|
textListItem("3.", "An empty chat profile with the provided name is created, and the app opens as usual.")
|
|
}
|
|
}
|
|
.padding(.bottom)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
}
|
|
|
|
private func showLAAlert(_ a: LASettingViewAlert) {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
laAlert = a
|
|
}
|
|
}
|
|
|
|
private func toggleLAMode() {
|
|
authenticate(reason: NSLocalizedString("Change lock mode", comment: "authentication reason")) { laResult in
|
|
switch laResult {
|
|
case .failed:
|
|
revertLAMode()
|
|
laAlert = .laFailedAlert
|
|
case .success:
|
|
switch laMode {
|
|
case .system:
|
|
updateLAMode()
|
|
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
|
|
switch laResult {
|
|
case .success:
|
|
_ = kcAppPassword.remove()
|
|
resetSelfDestruct()
|
|
laAlert = .laTurnedOnAlert
|
|
case .failed, .unavailable:
|
|
currentLAMode = .passcode
|
|
privacyLocalAuthModeDefault.set(.passcode)
|
|
revertLAMode()
|
|
laAlert = .laFailedAlert
|
|
}
|
|
}
|
|
case .passcode:
|
|
showPasswordAction = .toggleMode
|
|
}
|
|
case .unavailable:
|
|
disableUnavailableLA()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func toggleSelfDestruct() {
|
|
authenticate(reason: NSLocalizedString("Change self-destruct mode", comment: "authentication reason")) { laResult in
|
|
switch laResult {
|
|
case .failed:
|
|
revertSelfDestruct()
|
|
laAlert = .laFailedAlert
|
|
case .success:
|
|
if selfDestruct {
|
|
showPasswordAction = .enableSelfDestruct
|
|
} else {
|
|
resetSelfDestruct()
|
|
}
|
|
case .unavailable:
|
|
disableUnavailableLA()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func changeLAPassword() {
|
|
authenticate(title: "Current Passcode", reason: NSLocalizedString("Change passcode", comment: "authentication reason")) { laResult in
|
|
switch laResult {
|
|
case .failed: laAlert = .laFailedAlert
|
|
case .success: showPasswordAction = .changePasscode
|
|
case .unavailable: disableUnavailableLA()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func changeSelfDestructPassword() {
|
|
authenticate(reason: NSLocalizedString("Change self-destruct passcode", comment: "authentication reason")) { laResult in
|
|
switch laResult {
|
|
case .failed: laAlert = .laFailedAlert
|
|
case .success: showPasswordAction = .changeSelfDestructPasscode
|
|
case .unavailable: disableUnavailableLA()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func enableLA() {
|
|
resetLA()
|
|
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
|
|
switch laResult {
|
|
case .success:
|
|
m.contentViewAccessAuthenticated = true
|
|
prefPerformLA = true
|
|
laAlert = .laTurnedOnAlert
|
|
case .failed:
|
|
resetLAEnabled(false)
|
|
laAlert = .laFailedAlert
|
|
case .unavailable:
|
|
disableUnavailableLA()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func disableUnavailableLA() {
|
|
resetLAEnabled(false)
|
|
laMode = .system
|
|
updateLAMode()
|
|
laAlert = .laUnavailableInstructionAlert
|
|
}
|
|
|
|
private func disableLA() {
|
|
authenticate(reason: NSLocalizedString("Disable SimpleX Lock", comment: "authentication reason")) { laResult in
|
|
switch (laResult) {
|
|
case .success:
|
|
prefPerformLA = false
|
|
resetLA()
|
|
case .failed:
|
|
resetLAEnabled(true)
|
|
laAlert = .laFailedAlert
|
|
case .unavailable:
|
|
prefPerformLA = false
|
|
laAlert = .laUnavailableTurningOffAlert
|
|
}
|
|
}
|
|
}
|
|
|
|
private func resetLA() {
|
|
_ = kcAppPassword.remove()
|
|
laLockDelay = 30
|
|
showChangePassword = false
|
|
resetSelfDestruct()
|
|
}
|
|
|
|
private func resetLAEnabled(_ onOff: Bool) {
|
|
prefPerformLA = onOff
|
|
performLAToggleReset = true
|
|
withAnimation { performLA = onOff }
|
|
}
|
|
|
|
private func revertLAMode() {
|
|
performLAModeReset = true
|
|
withAnimation { laMode = currentLAMode }
|
|
}
|
|
|
|
private func updateLAMode() {
|
|
currentLAMode = laMode
|
|
privacyLocalAuthModeDefault.set(laMode)
|
|
}
|
|
|
|
private func resetSelfDestruct() {
|
|
_ = kcSelfDestructPassword.remove()
|
|
selfDestruct = false
|
|
updateSelfDestruct()
|
|
}
|
|
|
|
private func revertSelfDestruct() {
|
|
performLASelfDestructReset = true
|
|
withAnimation { selfDestruct = currentSelfDestruct }
|
|
}
|
|
|
|
private func updateSelfDestruct() {
|
|
UserDefaults.standard.set(selfDestruct, forKey: DEFAULT_LA_SELF_DESTRUCT)
|
|
currentSelfDestruct = selfDestruct
|
|
}
|
|
|
|
private func passcodeAlert(_ title: LocalizedStringKey) -> Alert {
|
|
mkAlert(title: title, message: "Please remember or store it securely - there is no way to recover a lost passcode!")
|
|
}
|
|
|
|
private func selfDestructPasscodeAlert(_ title: LocalizedStringKey) -> Alert {
|
|
mkAlert(title: title, message: "If you enter this passcode when opening the app, all app data will be irreversibly removed!")
|
|
}
|
|
}
|
|
|
|
struct PrivacySettings_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
PrivacySettings()
|
|
}
|
|
}
|