SimpleX-Chat/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
Evgeny 4b42a19ccb
ios: fix XCode 16 regressions (tap not working on files, quotes, images, voice messages, etc.), open link previews on tap (#5880)
* 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
2025-05-10 14:37:45 +01:00

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()
}
}