From a66f5e20fa025540218efffca98a7740ac75fa97 Mon Sep 17 00:00:00 2001
From: Bruno Muniz Azevedo Filho <bruno@elixxir.io>
Date: Fri, 2 Sep 2022 01:21:46 -0300
Subject: [PATCH] Fixed delete account

---
 Package.swift                                 |  7 +-
 .../Controllers/SingleChatController.swift    |  8 +-
 .../ViewModels/SingleChatViewModel.swift      | 97 +++++++++++--------
 Sources/ChatFeature/Views/ChatView.swift      |  8 +-
 .../ViewModel/ChatListViewModel.swift         |  1 +
 Sources/Defaults/KeyObject.swift              |  1 +
 Sources/LaunchFeature/LaunchViewModel.swift   | 34 ++++++-
 Sources/NetworkMonitor/NetworkMonitor.swift   |  1 +
 .../ViewModels/ScanDisplayViewModel.swift     |  6 +-
 .../ViewModels/SearchContainerViewModel.swift |  2 +
 .../ViewModels/SearchLeftViewModel.swift      | 15 +++
 .../Controllers/AccountDeleteController.swift |  2 +-
 .../ViewModels/AccountDeleteViewModel.swift   | 96 +++++++++++++-----
 .../ViewModels/SettingsViewModel.swift        | 16 +--
 14 files changed, 205 insertions(+), 89 deletions(-)

diff --git a/Package.swift b/Package.swift
index c9b7a2e0..f654545c 100644
--- a/Package.swift
+++ b/Package.swift
@@ -190,7 +190,10 @@ let package = Package(
             name: "CrashReporting"
         ),
         .target(
-            name: "NetworkMonitor"
+            name: "NetworkMonitor",
+            dependencies: [
+                .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
+            ]
         ),
         .target(
             name: "VersionChecking"
@@ -452,6 +455,7 @@ let package = Package(
                 .product(name: "ChatLayout", package: "ChatLayout"),
                 .product(name: "DifferenceKit", package: "DifferenceKit"),
                 .product(name: "ScrollViewController", package: "ScrollViewController"),
+                .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
             ]
         ),
         .testTarget(
@@ -631,6 +635,7 @@ let package = Package(
                 .target(name: "DrawerFeature"),
                 .target(name: "ReportingFeature"),
                 .target(name: "DependencyInjection"),
+                .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
             ]
         ),
         .target(
diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift
index 2028fc2d..5bded7c9 100644
--- a/Sources/ChatFeature/Controllers/SingleChatController.swift
+++ b/Sources/ChatFeature/Controllers/SingleChatController.swift
@@ -135,6 +135,7 @@ public final class SingleChatController: UIViewController {
         setupBindings()
 
         KeyboardListener.shared.add(delegate: self)
+        screenView.bringSubviewToFront(screenView.snackBar)
     }
 
     // MARK: Private
@@ -532,13 +533,6 @@ extension SingleChatController: KeyboardListenerDelegate {
     func keyboardWillChangeFrame(info: KeyboardInfo) {
         let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
 
-//        let keyWindow: UIWindow? = UIApplication.shared.connectedScenes
-//            .filter { $0.activationState == .foregroundActive }
-//            .compactMap { $0 as? UIWindowScene }
-//            .first?
-//            .windows
-//            .first(where: \.isKeyWindow)
-
         guard let keyWindow = keyWindow else {
             fatalError("[keyboardWillChangeFrame]: Couldn't get key window")
         }
diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift
index 157b03b1..958634b6 100644
--- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift
+++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift
@@ -14,9 +14,11 @@ import DifferenceKit
 import ReportingFeature
 import DependencyInjection
 import XXMessengerClient
+import XXClient
 
 import struct XXModels.Message
 import struct XXModels.FileTransfer
+import NetworkMonitor
 
 enum SingleChatNavigationRoutes: Equatable {
     case none
@@ -36,6 +38,7 @@ final class SingleChatViewModel: NSObject {
     @Dependency var messenger: Messenger
     @Dependency var permissions: PermissionHandling
     @Dependency var toastController: ToastController
+    @Dependency var networkMonitor: NetworkMonitoring
     @Dependency var transferManager: XXClient.FileTransfer
 
     @KeyObject(.username, defaultValue: nil) var username: String?
@@ -49,14 +52,19 @@ final class SingleChatViewModel: NSObject {
     private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([])
     private let reportPopupSubject = PassthroughSubject<Void, Never>()
 
+    private var healthCancellable: XXClient.Cancellable?
+
     var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() }
     private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none)
 
     var isOnline: AnyPublisher<Bool, Never> {
-        // TO REFACTOR:
-        Just(.init(true)).eraseToAnyPublisher()
+        networkMonitor
+            .statusPublisher
+            .map { $0 == .available }
+            .eraseToAnyPublisher()
     }
 
+
     var myId: Data {
         try! messenger.e2e.get()!.getContact().getId()
     }
@@ -116,6 +124,11 @@ final class SingleChatViewModel: NSObject {
             }.receive(on: DispatchQueue.main)
             .sink { [unowned self] in sectionsRelay.send($0) }
             .store(in: &cancellables)
+
+        healthCancellable = messenger.cMix.get()!.addHealthCallback(.init(handle: { [weak self] in
+            guard let self = self else { return }
+            self.networkMonitor.update($0)
+        }))
     }
 
     // MARK: Public
@@ -316,50 +329,54 @@ final class SingleChatViewModel: NSObject {
             replyMessageId: stagedReply?.messageId
         )
 
-        do {
-            message = try database.saveMessage(message)
-
-            let report = try messenger.e2e.get()!.send(
-                messageType: 2,
-                recipientId: contact.id,
-                payload: Payload(text: message.text, reply: stagedReply).asData(),
-                e2eParams: GetE2EParams.liveDefault()
-            )
-
-            try messenger.cMix.get()!.waitForRoundResult(
-                roundList: try report.encode(),
-                timeoutMS: 5_000,
-                callback: .init(handle: {
-                    switch $0 {
-                    case .delivered:
-                        message.status = .sent
-                        _ = try? self.database.saveMessage(message)
-
-                    case .notDelivered(timedOut: let timedOut):
-                        if timedOut {
-                            message.status = .sendingTimedOut
-                        } else {
-                            message.status = .sendingFailed
+        DispatchQueue.global().async { [weak self] in
+            guard let self = self else { return }
+
+            do {
+                message = try self.database.saveMessage(message)
+
+                let report = try self.messenger.e2e.get()!.send(
+                    messageType: 2,
+                    recipientId: self.contact.id,
+                    payload: Payload(text: message.text, reply: self.stagedReply).asData(),
+                    e2eParams: GetE2EParams.liveDefault()
+                )
+
+                try self.messenger.cMix.get()!.waitForRoundResult(
+                    roundList: try report.encode(),
+                    timeoutMS: 5_000,
+                    callback: .init(handle: {
+                        switch $0 {
+                        case .delivered:
+                            message.status = .sent
+                            _ = try? self.database.saveMessage(message)
+
+                        case .notDelivered(timedOut: let timedOut):
+                            if timedOut {
+                                message.status = .sendingTimedOut
+                            } else {
+                                message.status = .sendingFailed
+                            }
+
+                            _ = try? self.database.saveMessage(message)
                         }
+                    })
+                )
 
-                        _ = try? self.database.saveMessage(message)
-                    }
-                })
-            )
+                message.networkId = report.messageId
+                if let timestamp = report.timestamp {
+                    message.date = Date.fromTimestamp(Int(timestamp))
+                }
 
-            message.networkId = report.messageId
-            if let timestamp = report.timestamp {
-                message.date = Date.fromTimestamp(Int(timestamp))
+                message = try self.database.saveMessage(message)
+            } catch {
+                print(error.localizedDescription)
+                message.status = .sendingFailed
+                _ = try? self.database.saveMessage(message)
             }
 
-            message = try database.saveMessage(message)
-        } catch {
-            print(error.localizedDescription)
-            message.status = .sendingFailed
-            _ = try? database.saveMessage(message)
+            self.stagedReply = nil
         }
-
-        stagedReply = nil
     }
 
     func didRequestReply(_ message: Message) {
diff --git a/Sources/ChatFeature/Views/ChatView.swift b/Sources/ChatFeature/Views/ChatView.swift
index ee3c7d98..0d352970 100644
--- a/Sources/ChatFeature/Views/ChatView.swift
+++ b/Sources/ChatFeature/Views/ChatView.swift
@@ -28,10 +28,10 @@ final class ChatView: UIView {
         networkIssueInvisibleConstraint?.isActive = true
         snackBar.translatesAutoresizingMaskIntoConstraints = false
 
-        titleLabel.snp.makeConstraints { make in
-            make.top.equalTo(safeAreaLayoutGuide).offset(45)
-            make.left.equalToSuperview().offset(48)
-            make.right.equalToSuperview().offset(-61)
+        titleLabel.snp.makeConstraints {
+            $0.top.equalTo(safeAreaLayoutGuide).offset(45)
+            $0.left.equalToSuperview().offset(48)
+            $0.right.equalToSuperview().offset(-61)
         }
     }
 
diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift
index dbe20f1b..c01169dd 100644
--- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift
+++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift
@@ -50,6 +50,7 @@ final class ChatListViewModel {
 
     var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> {
         let query = Contact.Query(
+            authStatus: [.friend],
             isRecent: true,
             isBlocked: reportingStatus.isEnabled() ? false : nil,
             isBanned: reportingStatus.isEnabled() ? false : nil
diff --git a/Sources/Defaults/KeyObject.swift b/Sources/Defaults/KeyObject.swift
index 1effae9c..0ade4e83 100644
--- a/Sources/Defaults/KeyObject.swift
+++ b/Sources/Defaults/KeyObject.swift
@@ -39,6 +39,7 @@ public enum Key: String {
     case crashReporting
     case icognitoKeyboard
 
+    case dummyTrafficOn
     case askedDummyTrafficOnce
 }
 
diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift
index 6c27c1de..12b722f4 100644
--- a/Sources/LaunchFeature/LaunchViewModel.swift
+++ b/Sources/LaunchFeature/LaunchViewModel.swift
@@ -21,6 +21,7 @@ import class XXClient.Cancellable
 import XXDatabase
 import XXLegacyDatabaseMigrator
 import XXMessengerClient
+import NetworkMonitor
 
 struct Update {
     let content: String
@@ -44,11 +45,13 @@ final class LaunchViewModel {
     @Dependency var reportingStatus: ReportingStatus
     @Dependency var toastController: ToastController
     @Dependency var keychainHandler: KeychainHandling
+    @Dependency var networkMonitor: NetworkMonitoring
     @Dependency var processBannedList: ProcessBannedList
     @Dependency var permissionHandler: PermissionHandling
 
     @KeyObject(.username, defaultValue: nil) var username: String?
     @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool
+    @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn: Bool
 
     var hudPublisher: AnyPublisher<HUDStatus, Never> {
         hudSubject.eraseToAnyPublisher()
@@ -153,7 +156,25 @@ final class LaunchViewModel {
                 senderId: nil,
                 messageType: 2,
                 callback: .init(handle: {
-                    print(">>> \(String(data: $0.payload, encoding: .utf8))")
+                    // let roundId = $0.roundId
+
+                    guard let payload = try? Payload(with: $0.payload) else {
+                        fatalError("Couldn't decode payload: \(String(data: $0.payload, encoding: .utf8) ?? "nil")")
+                    }
+
+                    try! self.database.saveMessage(.init(
+                        networkId: $0.id,
+                        senderId: $0.sender,
+                        recipientId: messenger.e2e.get()!.getContact().getId(),
+                        groupId: nil,
+                        date: Date.fromTimestamp($0.timestamp),
+                        status: .received,
+                        isUnread: true,
+                        text: payload.text,
+                        replyMessageId: payload.reply?.messageId,
+                        roundURL: "https://www.google.com.br",
+                        fileTransferId: nil
+                    ))
                 })
             )
 
@@ -161,6 +182,8 @@ final class LaunchViewModel {
             try generateTrafficManager(messenger: messenger)
             try generateTransferManager(messenger: messenger)
 
+            networkMonitor.start()
+
             if messenger.isLoggedIn() == false {
                 if try messenger.isRegistered() == false {
                     hudSubject.send(.none)
@@ -452,6 +475,7 @@ extension LaunchViewModel {
         )
 
         DependencyInjection.Container.shared.register(manager)
+        try! manager.setStatus(dummyTrafficOn)
     }
 }
 
@@ -551,12 +575,14 @@ extension LaunchViewModel {
             return
         }
 
-        let leaderId = try! group.getMembership() // This is all users on the group, the 1st is the leader/creator.
+        guard let members = try? group.getMembership(), let leader = members.first else {
+            fatalError("Failed to get group membership/leader")
+        }
 
         try! database.saveGroup(.init(
             id: group.getId(),
             name: String(data: group.getName(), encoding: .utf8)!,
-            leaderId: leaderId,
+            leaderId: leader.id,
             createdAt: Date.fromTimestamp(Int(group.getCreatedMS())),
             authStatus: .pending,
             serialized: group.serialize()
@@ -564,7 +590,7 @@ extension LaunchViewModel {
 
         if let initialMessage = String(data: group.getInitMessage(), encoding: .utf8) {
             try! database.saveMessage(.init(
-                senderId: leaderId,
+                senderId: leader.id,
                 recipientId: nil,
                 groupId: group.getId(),
                 date: Date.fromTimestamp(Int(group.getCreatedMS())),
diff --git a/Sources/NetworkMonitor/NetworkMonitor.swift b/Sources/NetworkMonitor/NetworkMonitor.swift
index ab805ca5..84899d7e 100644
--- a/Sources/NetworkMonitor/NetworkMonitor.swift
+++ b/Sources/NetworkMonitor/NetworkMonitor.swift
@@ -2,6 +2,7 @@
 
 import Network
 import Combine
+import XXClient
 import Foundation
 
 public enum NetworkStatus: Equatable {
diff --git a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift
index d28de8f7..ad307df1 100644
--- a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift
+++ b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift
@@ -72,10 +72,10 @@ final class ScanDisplayViewModel {
         }
 
         let e2e = messenger.e2e.get()!
-        let contactData = e2e.getContact().data
-        let wrappedContact = try! SetFactsOnContact.live(contactData: contactData, facts: facts)
+        var contact = e2e.getContact()
+        try! contact.setFacts(facts)
 
-        filter.setValue(wrappedContact, forKey: "inputMessage")
+        filter.setValue(contact.data, forKey: "inputMessage")
         let transform = CGAffineTransform(scaleX: 5, y: 5)
 
         if let output = filter.outputImage?.transformed(by: transform) {
diff --git a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift
index 9ca3794c..c3f0f59e 100644
--- a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift
+++ b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift
@@ -9,6 +9,7 @@ final class SearchContainerViewModel {
     @Dependency var pushHandler: PushHandling
     @Dependency var dummyTrafficManager: DummyTraffic
 
+    @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn
     @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications
     @KeyObject(.askedDummyTrafficOnce, defaultValue: false) var offeredCoverTraffic
 
@@ -25,6 +26,7 @@ final class SearchContainerViewModel {
 
     func didEnableCoverTraffic() {
         try! dummyTrafficManager.setStatus(true)
+        dummyTrafficOn = dummyTrafficManager.getStatus()
     }
 
     private func verifyCoverTraffic() {
diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift
index c5ec4513..a8f0135d 100644
--- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift
+++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift
@@ -9,6 +9,7 @@ import XXClient
 import Defaults
 import Countries
 import CustomDump
+import ToastFeature
 import NetworkMonitor
 import ReportingFeature
 import CombineSchedulers
@@ -28,6 +29,7 @@ final class SearchLeftViewModel {
     @Dependency var database: Database
     @Dependency var messenger: Messenger
     @Dependency var reportingStatus: ReportingStatus
+    @Dependency var toastController: ToastController
     @Dependency var networkMonitor: NetworkMonitoring
 
     @KeyObject(.username, defaultValue: nil) var username: String?
@@ -208,6 +210,7 @@ final class SearchLeftViewModel {
                 contact = try self.database.saveContact(contact)
 
                 self.hudSubject.send(.none)
+                self.presentSuccessToast(for: contact, resent: true)
             } catch {
                 contact.authStatus = .requestFailed
                 _ = try? self.database.saveContact(contact)
@@ -242,6 +245,7 @@ final class SearchLeftViewModel {
 
                 self.hudSubject.send(.none)
                 self.successSubject.send(contact)
+                self.presentSuccessToast(for: contact, resent: false)
             } catch {
                 contact.authStatus = .requestFailed
                 _ = try? self.database.saveContact(contact)
@@ -299,4 +303,15 @@ final class SearchLeftViewModel {
     private func removeMyself(from collection: [XXModels.Contact]) -> [XXModels.Contact]? {
         collection.filter { $0.id != myId }
     }
+
+    private func presentSuccessToast(for contact: XXModels.Contact, resent: Bool) {
+        let name = contact.nickname ?? contact.username
+        let sentTitle = Localized.Requests.Sent.Toast.sent(name ?? "")
+        let resentTitle = Localized.Requests.Sent.Toast.resent(name ?? "")
+
+        toastController.enqueueToast(model: .init(
+            title: resent ? resentTitle : sentTitle,
+            leftImage: Asset.sharedSuccess.image
+        ))
+    }
 }
diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift
index ee5be65a..0f583b42 100644
--- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift
+++ b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift
@@ -51,7 +51,7 @@ public final class AccountDeleteController: UIViewController {
     }
 
     private func setupBindings() {
-        viewModel.hud
+        viewModel.hudPublisher
             .receive(on: DispatchQueue.main)
             .sink { [hud] in hud.update(with: $0) }
             .store(in: &cancellables)
diff --git a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift
index 96586dea..94869e07 100644
--- a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift
+++ b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift
@@ -1,46 +1,98 @@
 import HUD
+import Models
 import Combine
 import Defaults
-import Foundation
+import Keychain
 import XXClient
+import Foundation
 import XXMessengerClient
 import DependencyInjection
-import Models
+import Retry
 
 final class AccountDeleteViewModel {
     @Dependency var messenger: Messenger
+    @Dependency var keychain: KeychainHandling
 
     @KeyObject(.username, defaultValue: nil) var username: String?
 
-    var deleting = false
+    var hudPublisher: AnyPublisher<HUDStatus, Never> {
+        hudSubject.eraseToAnyPublisher()
+    }
 
-    var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() }
-    private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none)
+    private var isCurrentlyDeleting = false
+    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
 
     func didTapDelete() {
-        guard deleting == false else { return }
-        deleting = true
+        guard isCurrentlyDeleting == false else { return }
+        isCurrentlyDeleting = true
 
         DispatchQueue.main.async { [weak self] in
-            self?.hudRelay.send(.on)
+            guard let self = self else { return }
+            self.hudSubject.send(.on)
         }
 
-        do {
-            let fact = Fact(fact: username!, type: FactType.username.rawValue)
-            try messenger.ud.get()!.permanentDeleteAccount(username: fact)
-            try messenger.destroy()
-
-            DispatchQueue.main.async { [weak self] in
-                self?.hudRelay.send(.error(.init(
-                    content: "Now kill the app and re-open",
-                    title: "Account deleted",
-                    dismissable: false
-                )))
+        DispatchQueue.global().async { [weak self] in
+            guard let self = self else { return }
+
+            do {
+                try self.cleanUD()
+                try self.stopNetwork()
+                try self.messenger.destroy()
+                try self.keychain.clear()
+                try self.deleteDatabase()
+
+                UserDefaults.resetStandardUserDefaults()
+                UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
+                UserDefaults.standard.synchronize()
+
+                DispatchQueue.main.async { [weak self] in
+                    guard let self = self else { return }
+                    self.hudSubject.send(.error(.init(
+                        content: "Now kill the app and re-open",
+                        title: "Account deleted",
+                        dismissable: false
+                    )))
+                }
+            } catch {
+                DispatchQueue.main.async { [weak self] in
+                    guard let self = self else { return }
+                    self.hudSubject.send(.error(.init(with: error)))
+                }
             }
-        } catch {
-            DispatchQueue.main.async { [weak self] in
-                self?.hudRelay.send(.error(.init(with: error)))
+        }
+    }
+
+    private func cleanUD() throws {
+        let fact = Fact(fact: username!, type: FactType.username.rawValue)
+
+        print(">>> Deleting my username (\(fact.fact)) from ud")
+        try messenger.ud.get()!.permanentDeleteAccount(username: fact)
+    }
+
+    private func stopNetwork() throws {
+        let cMix = messenger.cMix.get()!
+
+        print(">>> Stopping network follower...")
+        try cMix.stopNetworkFollower()
+
+        retry(max: 10, retryStrategy: .delay(seconds: 2)) {
+            if cMix.networkFollowerStatus() != .stopped {
+                print(">>> Network still hasn't stopped. Its \(cMix.networkFollowerStatus())")
+                throw NSError.create("Gave up on stopping the network.")
             }
+
+            print(">>> Network has stopped")
         }
     }
+
+    private func deleteDatabase() throws {
+        print(">>> Deleting database...")
+
+        let dbPath = FileManager.default
+            .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
+            .appendingPathComponent("xxm_database")
+            .appendingPathExtension("sqlite").path
+
+        try FileManager.default.removeItem(atPath: dbPath)
+    }
 }
diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift
index b44c37d8..bee9de29 100644
--- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift
+++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift
@@ -21,15 +21,16 @@ struct SettingsViewState: Equatable {
 }
 
 final class SettingsViewModel {
+    @Dependency var pushHandler: PushHandling
+    @Dependency var permissions: PermissionHandling
     @Dependency var dummyTrafficManager: DummyTraffic
-    @Dependency private var pushHandler: PushHandling
-    @Dependency private var permissions: PermissionHandling
 
-    @KeyObject(.biometrics, defaultValue: false) private var biometrics
-    @KeyObject(.hideAppList, defaultValue: false) private var hideAppList
-    @KeyObject(.icognitoKeyboard, defaultValue: false) private var icognitoKeyboard
-    @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications
-    @KeyObject(.inappnotifications, defaultValue: true) private var inAppNotifications
+    @KeyObject(.biometrics, defaultValue: false) var biometrics
+    @KeyObject(.hideAppList, defaultValue: false) var hideAppList
+    @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn
+    @KeyObject(.icognitoKeyboard, defaultValue: false) var icognitoKeyboard
+    @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications
+    @KeyObject(.inappnotifications, defaultValue: true) var inAppNotifications
 
     var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler()
 
@@ -66,6 +67,7 @@ final class SettingsViewModel {
         let currently = dummyTrafficManager.getStatus()
         try! dummyTrafficManager.setStatus(!currently)
         stateRelay.value.isDummyTrafficOn = !currently
+        dummyTrafficOn = stateRelay.value.isDummyTrafficOn
     }
 
     func didToggleHideActiveApps() {
-- 
GitLab