| 1 | import Foundation |
| 2 | import Security |
| 3 | |
| 4 | // π€ The full local reset behind SIGN OUT: wipes everything the app persists β |
| 5 | // Keychain identities, thread keys, the home session, UserDefaults, and the |
| 6 | // on-disk stores β so the next launch is a true first run. Signing out of |
| 7 | // Eleven MEANS clearing this device: the spaces live in the member's Eleven |
| 8 | // account, and signing back in restores them (the caller pushes a final sync |
| 9 | // first). Also the reason a plain uninstall isn't a reset: Keychain items |
| 10 | // survive app deletion on iOS. |
| 11 | public enum AppReset { |
| 12 | public static func wipeAll() { |
| 13 | // Whole-service deletes (no account = every item), with and without |
| 14 | // the app-family access group β matching how the stores write. |
| 15 | for service in ["com.lchat.client", "com.lchat.client.threadkeys"] { |
| 16 | for withGroup in [true, false] { |
| 17 | var query: [String: Any] = [ |
| 18 | kSecClass as String: kSecClassGenericPassword, |
| 19 | kSecAttrService as String: service, |
| 20 | kSecUseDataProtectionKeychain as String: true, |
| 21 | ] |
| 22 | if withGroup { |
| 23 | // Same group constant as Keychain/SharedThreadKeys. |
| 24 | query[kSecAttrAccessGroup as String] = |
| 25 | "J5NDF5PCD8.com.elevenmessenger.messenger.shared" |
| 26 | } |
| 27 | SecItemDelete(query as CFDictionary) |
| 28 | } |
| 29 | } |
| 30 | if let bundleID = Bundle.main.bundleIdentifier { |
| 31 | UserDefaults.standard.removePersistentDomain(forName: bundleID) |
| 32 | } |
| 33 | let fm = FileManager.default |
| 34 | #if os(macOS) |
| 35 | // NOT sandboxed on the Mac: the search paths are the user's REAL |
| 36 | // Library folders, so only paths that are provably OURS may go β the |
| 37 | // app's data dir (MessageStore/MacChatsStore both live in |
| 38 | // "Application Support/lchat"), its WebKit store (the webviews' |
| 39 | // localStorage identities), and its caches. Never the whole folder. |
| 40 | for base in fm.urls(for: .applicationSupportDirectory, in: .userDomainMask) { |
| 41 | try? fm.removeItem(at: base.appendingPathComponent("lchat")) |
| 42 | } |
| 43 | if let bundleID = Bundle.main.bundleIdentifier { |
| 44 | for base in fm.urls(for: .cachesDirectory, in: .userDomainMask) { |
| 45 | try? fm.removeItem(at: base.appendingPathComponent(bundleID)) |
| 46 | } |
| 47 | for base in fm.urls(for: .libraryDirectory, in: .userDomainMask) { |
| 48 | try? fm.removeItem(at: base.appendingPathComponent("WebKit/\(bundleID)")) |
| 49 | try? fm.removeItem(at: base.appendingPathComponent("HTTPStorages/\(bundleID)")) |
| 50 | } |
| 51 | } |
| 52 | #else |
| 53 | // Sandboxed: the search paths are the app's own container β wipe wholesale. |
| 54 | for dir: FileManager.SearchPathDirectory in [.applicationSupportDirectory, |
| 55 | .cachesDirectory, .documentDirectory] { |
| 56 | for url in fm.urls(for: dir, in: .userDomainMask) { |
| 57 | for item in (try? fm.contentsOfDirectory(at: url, |
| 58 | includingPropertiesForKeys: nil)) ?? [] { |
| 59 | try? fm.removeItem(at: item) |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | try? fm.removeItem(at: fm.temporaryDirectory) |
| 64 | #endif |
| 65 | } |
| 66 | } |
| 67 | |