Răsfoiți Sursa

update release cpaas 5.0.55

alqindiirsyam 1 lună în urmă
părinte
comite
d36a59a6b0

+ 2 - 0
.gitignore

@@ -52,3 +52,5 @@ ExampleCode/ExampleCode.xcodeproj/xcshareddata/xcschemes/ExampleCode.xcscheme
 NexilisLite/.DS_Store
 ExampleCode/ExampleCode/Info.plist
 ExampleCode/ExampleCode.xcodeproj/project.pbxproj
+NexilisLite/NexilisLite.xcodeproj/project.pbxproj
+AppBuilder/AppBuilder.xcodeproj/project.pbxproj

+ 2 - 2
AppBuilder/AppBuilder.xcodeproj/project.pbxproj

@@ -568,7 +568,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.52;
+				MARKETING_VERSION = 5.0.55;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -604,7 +604,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.52;
+				MARKETING_VERSION = 5.0.55;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";

+ 2 - 1
AppBuilder/AppBuilder/FourthTabViewController.swift

@@ -756,7 +756,7 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
                 DispatchQueue.global().async {
                     let apiKey = Nexilis.sAPIKey
                     var id = Utils.getConnectionID()
-                    if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpApi(api: apiKey, p_pin: id), timeout: 30 * 1000) {
+                    if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpApi(api: apiKey, p_pin: id), timeout: 15 * 1000) {
                         id = response.getBody(key: CoreMessage_TMessageKey.F_PIN, default_value: "")
                         if(!id.isEmpty){
 //                            Nexilis.changeUser(f_pin: id)
@@ -778,6 +778,7 @@ public class FourthTabViewController: UIViewController, UITableViewDelegate, UIT
                                     banner.show()
                                     if Nexilis.showFB {
                                         Nexilis.floatingButton.removeFromSuperview()
+                                        FloatingButton.datePull = nil
                                         Nexilis.floatingButton = FloatingButton()
                                         Nexilis.addFB()
                                     }

+ 5 - 1
AppBuilder/AppBuilder/ViewController.swift

@@ -422,6 +422,10 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
             self.navigationController?.show(vc, sender: nil)
         } else {
             self.selectedIndex = (self.viewControllers?.firstIndex(of: fourthTab!))!
+            if indicatorImage != nil {
+                indicatorImage.removeFromSuperview()
+                addCustomViewAboveTabBarItem(at: selectedIndex, image: imageIndicator)
+            }
         }
     }
     
@@ -1229,7 +1233,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
                     }
                     return
                 }
-                if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.pullFloatingButton(), timeout: 30 * 1000) {
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.pullFloatingButton(), timeout: 20000) {
                     if response.isOk() {
                         let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
                         if !data.isEmpty {

+ 0 - 4
NexilisLite/NexilisLite.xcodeproj/project.pbxproj

@@ -247,7 +247,6 @@
 		CD95516B2A664BDB00AF6476 /* Poppins-ExtraLightItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = CD9551382A664BD400AF6476 /* Poppins-ExtraLightItalic.otf */; };
 		CD962CFD2E420B5300A7BDA5 /* TOTPGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD962CFC2E420B5300A7BDA5 /* TOTPGenerator.swift */; };
 		CD962CFE2E420B5300A7BDA5 /* MFAViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD962CFB2E420B5300A7BDA5 /* MFAViewController.swift */; };
-		CD962CFF2E420B5300A7BDA5 /* MFABiometricOnlyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD962CFA2E420B5300A7BDA5 /* MFABiometricOnlyViewController.swift */; };
 		CD9829B72A3C07CB009F6743 /* SeminarListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9829B62A3C07CB009F6743 /* SeminarListViewController.swift */; };
 		CDA461222AB99E09001CD010 /* ConfigureFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA461212AB99E09001CD010 /* ConfigureFloatingButton.swift */; };
 		CDB21B2E2BA9998400EC5280 /* pb_def_icon_mode2.gif in Resources */ = {isa = PBXBuildFile; fileRef = CDB21B2B2BA9998400EC5280 /* pb_def_icon_mode2.gif */; };
@@ -538,7 +537,6 @@
 		CD9551432A664BD400AF6476 /* Poppins-BlackItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-BlackItalic.otf"; sourceTree = "<group>"; };
 		CD9551442A664BD400AF6476 /* Poppins-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Regular.otf"; sourceTree = "<group>"; };
 		CD9551452A664BD400AF6476 /* Poppins-ExtraBold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-ExtraBold.otf"; sourceTree = "<group>"; };
-		CD962CFA2E420B5300A7BDA5 /* MFABiometricOnlyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFABiometricOnlyViewController.swift; sourceTree = "<group>"; };
 		CD962CFB2E420B5300A7BDA5 /* MFAViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFAViewController.swift; sourceTree = "<group>"; };
 		CD962CFC2E420B5300A7BDA5 /* TOTPGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPGenerator.swift; sourceTree = "<group>"; };
 		CD9829B62A3C07CB009F6743 /* SeminarListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeminarListViewController.swift; sourceTree = "<group>"; };
@@ -827,7 +825,6 @@
 				CD1E718E2A0BA86100BF871F /* HistoryBroadcastViewController.swift */,
 				CD1E719C2A0BA86100BF871F /* HistoryCCViewController.swift */,
 				CD1E71A42A0BA86100BF871F /* ImageVideoPicker.swift */,
-				CD962CFA2E420B5300A7BDA5 /* MFABiometricOnlyViewController.swift */,
 				CD962CFB2E420B5300A7BDA5 /* MFAViewController.swift */,
 				CD1E719E2A0BA86100BF871F /* NotificationSound.swift */,
 				CD1E71A62A0BA86100BF871F /* ProfileViewController.swift */,
@@ -1443,7 +1440,6 @@
 				CD1E725D2A0BA86100BF871F /* WhiteboardDelegate.swift in Sources */,
 				CD962CFD2E420B5300A7BDA5 /* TOTPGenerator.swift in Sources */,
 				CD962CFE2E420B5300A7BDA5 /* MFAViewController.swift in Sources */,
-				CD962CFF2E420B5300A7BDA5 /* MFABiometricOnlyViewController.swift in Sources */,
 				CD1E721B2A0BA86100BF871F /* Nexilis.swift in Sources */,
 				CD1E72102A0BA86100BF871F /* Extension.swift in Sources */,
 				CD1E724C2A0BA86100BF871F /* NotificationSound.swift in Sources */,

+ 392 - 99
NexilisLite/NexilisLite/Source/APIS.swift

@@ -539,133 +539,425 @@ public class APIS: NSObject {
         }
     }
     
-    private static var mfaCallback: ((String) -> Void)?
+    /*
+     0
+     Success
+     1
+     Invalid PIN/Password
+     2
+     Auth Failure
+     3
+     Device Key Signature Mismatch
+     4
+     Policy Level Not Met
+     5
+     PPKey Generated Failed
+     6
+     Challenge-Response Failed
+     7
+     TOTOP Mismatch
+     9
+     Server Error Code
+     10
+     Invalid Face ID or Face ID mismatch
+     11
+     Invalid Fingerprint
+     12
+     Invalid SIM Card does not match the one registered in our system
+     13
+     Unable to access servers. Check your internet connection and try again later
+     99
+     Back / Cancel
+     */
+    private static var mfaCallback: ((Int) -> Void)?
+    
+    private static var methodSetted = ""
 
-    static func getMFACallback() -> ((String) -> Void)? {
+    public static func setTxnAuthActivity(activity: String) {
+        methodSetted = activity
+    }
+    static func getMFACallback() -> ((Int) -> Void)? {
         return mfaCallback
     }
-
-    public static func setMFACallback(_ callback: @escaping (String) -> Void) {
-        mfaCallback = callback
+    
+    public static func signUp(userId: String, mfaCallback: @escaping (Int) -> Void) {
+        self.mfaCallback = mfaCallback
+        let method = "Sign Up"
+        let policyLevel = Utils.getSignUpLevel()
+//        print("signUp: \(policyLevel)")
+        signUpSignInMFA(method: method, userId: userId, policyLevel: policyLevel)
+    }
+    
+    public static func signIn(userId: String, mfaCallback: @escaping (Int) -> Void) {
+        self.mfaCallback = mfaCallback
+        let method = "Sign In"
+        let policyLevel = Utils.getSignInLevel()
+//        print("signIn: \(policyLevel)")
+        signUpSignInMFA(method: method, userId: userId, policyLevel: policyLevel)
+    }
+    
+    public static func txnAuth(userId: String, txnId: String, amount: Double, mfaCallback: @escaping (Int) -> Void) {
+        self.mfaCallback = mfaCallback
+        var method = "Transaction"
+        if !methodSetted.isEmpty {
+            method = methodSetted
+        }
+        let dataTxn = Utils.getTxnLevel()
+        var policyLevel = "1,2"
+//        print("txnAuth: \(dataTxn)")
+        if !dataTxn.isEmpty {
+            if let data = dataTxn.data(using: .utf8) {
+                do {
+                    // Parse to generic JSON array
+                    if let jsonArray = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] {
+                        for json in jsonArray {
+                            let min = json["min"] as? Double ?? 0
+                            let max = json["max"] as? Double ?? 0
+                            let policy = json["policy"] as? String ?? ""
+                            if max == -1 {
+                                if amount >= min {
+                                    policyLevel = policy
+                                    break
+                                }
+                            } else {
+                                if amount >= min && amount <= max {
+                                    policyLevel = policy
+                                    break
+                                }
+                            }
+                        }
+                        openMFA(method: method, flag: policyLevel)
+                    }
+                } catch {
+                    print("Error converting string to JSONArray:", error)
+                }
+            }
+        }
+    }
+    
+    private static func signUpSignInMFA(method: String, userId: String, policyLevel: String) {
+        Nexilis.showLoader()
+        DispatchQueue.global().async {
+            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpSignInAPI(p_name: userId, p_password: ""), timeout: 15 * 1000) {
+                if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "20" {
+                    DispatchQueue.main.async {
+                        Nexilis.hideLoader {
+                            let errMessage = "Invalid user / Username and password does not match".localized()
+                            UIApplication.shared.visibleViewController?.view.makeToast(errMessage, duration: 3)
+                            APIS.getMFACallback()?(2)
+                        }
+                    }
+                } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "11" {
+                    DispatchQueue.main.async {
+                        Nexilis.hideLoader {
+                            let errMessage = "Failed, unknown user".localized()
+                            UIApplication.shared.visibleViewController?.view.makeToast(errMessage, duration: 3)
+                            APIS.getMFACallback()?(2)
+                        }
+                    }
+                } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "4u" {
+                    DispatchQueue.main.async {
+                        Nexilis.hideLoader {
+                            let errMessage = "Failed, blocked user".localized()
+                            UIApplication.shared.visibleViewController?.view.makeToast(errMessage, duration: 3)
+                            APIS.getMFACallback()?(2)
+                        }
+                    }
+                } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "13" {
+                    DispatchQueue.main.async {
+                        Nexilis.hideLoader {
+                            let errMessage = "Failed, This user is not registered on this device".localized()
+                            UIApplication.shared.visibleViewController?.view.makeToast(errMessage, duration: 3)
+                            APIS.getMFACallback()?(2)
+                        }
+                    }
+                } else if !response.isOk() {
+                    DispatchQueue.main.async {
+                        Nexilis.hideLoader {
+                            let errMessage = "Failed".localized()
+                            UIApplication.shared.visibleViewController?.view.makeToast(errMessage, duration: 3)
+                            APIS.getMFACallback()?(2)
+                        }
+                    }
+                } else {
+                    if Database.shared.openDatabase() == 0 {
+                        APIS.showRestartApp()
+                        KeyManagerNexilis.deleteKey()
+                        KeyManagerNexilis.deleteMarker()
+                        return
+                    }
+                    let sign = response.getBody(key: CoreMessage_TMessageKey.SIGN, default_value: "")
+                    if sign == "1" {
+                        let id = response.getBody(key: CoreMessage_TMessageKey.F_PIN, default_value: "")
+                        let f_pin = response.getBody(key: CoreMessage_TMessageKey.F_PIN_REAL, default_value: "")
+                        let device_id = response.getBody(key: CoreMessage_TMessageKey.IMEI, default_value: id)
+                        let last_sign = response.getBody(key: CoreMessage_TMessageKey.LAST_SIGN, default_value: "0")
+                        if last_sign != "0" {
+                            Utils.setLoginMultipleFPin(value: f_pin)
+                            DispatchQueue.main.async {
+                                let errMessage = "Multiple Login Detected...".localized()
+                                UIApplication.shared.visibleViewController?.view.makeToast(errMessage, duration: 3)
+                                if Nexilis.showFB {
+                                    Nexilis.floatingButton.removeFromSuperview()
+                                    FloatingButton.datePull = nil
+                                    Nexilis.floatingButton = FloatingButton()
+                                    Nexilis.addFB()
+                                }
+                                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
+                                    let dialog = DialogUnableAccess()
+                                    dialog.modalTransitionStyle = .crossDissolve
+                                    dialog.modalPresentationStyle = .overCurrentContext
+                                    UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                                })
+                            }
+                            return
+                        }
+                        DispatchQueue.main.async {
+                            UIApplication.shared.visibleViewController?.deleteAllRecordDatabase()
+                        }
+                        if(!id.isEmpty) {
+                            SecureUserDefaults.shared.set(device_id, forKey: "device_id")
+                            Utils.setProfile(value: true)
+                            // pos registration
+                            _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: id))
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
+                                Nexilis.hideLoader(completion: {
+                                    if Nexilis.showFB {
+                                        Nexilis.floatingButton.removeFromSuperview()
+                                        FloatingButton.datePull = nil
+                                        Nexilis.floatingButton = FloatingButton()
+                                        Nexilis.addFB()
+                                    }
+                                    Nexilis.getFeatureAccess()
+                                    openMFA(method: method, flag: policyLevel)
+                                })
+                            })
+                        }
+                    } else {
+                        let idMe = User.getMyPin()!
+                        _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: idMe))
+                        Utils.setProfile(value: true)
+                        DispatchQueue.main.async {
+                            Nexilis.hideLoader(completion: {
+                                openMFA(method: method, flag: policyLevel)
+                            })
+                        }
+                    }
+                }
+            } else {
+                DispatchQueue.main.async {
+                    Nexilis.hideLoader {
+                        let errMessage = "Unable to access servers. Check your internet connection and try again later".localized()
+                        UIApplication.shared.visibleViewController?.view.makeToast(errMessage, duration: 3)
+                        APIS.getMFACallback()?(13)
+                    }
+                }
+            }
+        }
     }
     
-    public static func openMFA(method: String, flag: Int){
+    private static func openMFA(method: String, flag: String) {
         let isChangeProfile = Utils.getSetProfile()
         if !isChangeProfile {
             APIS.showChangeProfile()
             return
         }
-        if flag == MFAViewController.STEP_NEEDED_FIDO {
-            DispatchQueue.global().async {
-                if let me = User.getMyPin() {
-                    do {
-                        let message = CoreMessage_TMessageBank.getMFAValidation(data: me)
-                        var hasKey = false
-                        if !KeyManagerNexilis.hasGeneratedKey() {
-                            KeyManagerNexilis.generateKey()
-                            KeyManagerNexilis.saveMarker()
-                        } else {
-                            hasKey = true
+        if flag == MFAViewController.STEP_FIDO || flag == MFAViewController.STEP_FIDO_BIOFACE || flag == MFAViewController.STEP_FIDO_BIOFINGER {
+            checkFidoWithOrBIO(method: method, flag: flag)
+        } else {
+            let controller = MFAViewController()
+            controller.METHOD = method
+            controller.STEP_NEEDED = flag
+            let navigationController = CustomNavigationController(rootViewController: controller)
+            navigationController.defaultStyle()
+            
+            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+            } else {
+                UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+            }
+        }
+    }
+    
+    private static func checkFidoWithOrBIO(method: String, flag: String) {
+        DispatchQueue.global().async {
+            if let me = User.getMyPin() {
+                do {
+                    let message = CoreMessage_TMessageBank.getMFAValidation(data: me)
+                    var hasKey = false
+                    if !KeyManagerNexilis.hasGeneratedKey() {
+                        KeyManagerNexilis.generateKey()
+                        KeyManagerNexilis.saveMarker()
+                    } else {
+                        hasKey = true
+                    }
+                    guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: false) else {
+                        KeyManagerNexilis.deleteKey()
+                        KeyManagerNexilis.deleteMarker()
+                        DispatchQueue.main.async {
+                            let errorMessage = "PPKey Generated Failed".localized()
+                            let dialog = DialogErrorMFA()
+                            dialog.modalTransitionStyle = .crossDissolve
+                            dialog.modalPresentationStyle = .overCurrentContext
+                            dialog.errorDesc = errorMessage
+                            dialog.method = method
+                            UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                            APIS.getMFACallback()?(5)
                         }
-                        guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: false) else {
-                            KeyManagerNexilis.deleteKey()
-                            KeyManagerNexilis.deleteMarker()
-                            DispatchQueue.main.async {
-                                let errorMessage = "Failed to get Private Key"
-                                let dialog = DialogErrorMFA()
-                                dialog.modalTransitionStyle = .crossDissolve
-                                dialog.modalPresentationStyle = .overCurrentContext
-                                dialog.errorDesc = errorMessage
-                                dialog.method = method
-                                UIApplication.shared.visibleViewController?.present(dialog, animated: true)
-                                APIS.getMFACallback()?("Failed: \(errorMessage)")
+                        return
+                    }
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
+                        if response.isOk() {
+                            let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
+                            if data.isEmpty {
+                                KeyManagerNexilis.deleteKey()
+                                KeyManagerNexilis.deleteMarker()
+                                DispatchQueue.main.async {
+                                    let errorMessage = "Auth Failure".localized()
+                                    let dialog = DialogErrorMFA()
+                                    dialog.modalTransitionStyle = .crossDissolve
+                                    dialog.modalPresentationStyle = .overCurrentContext
+                                    dialog.errorDesc = errorMessage
+                                    dialog.method = method
+                                    UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                                    APIS.getMFACallback()?(2)
+                                }
+                                return
                             }
-                            return
-                        }
-                        if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
-                            if response.isOk() {
-                                let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
-                                if data.isEmpty {
+                            let df = HMACDeviceFingerprintNexilis.generate()
+                            message.mBodies[CoreMessage_TMessageKey.FINGERPRINT] = df
+                            if hasKey {
+                                var sign = ""
+                                if let dataSign = "\(data)!\(df)".data(using: .utf8) {
+                                    if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
+                                        sign = signature.base64EncodedString()
+                                    }
+                                }
+                                message.mBodies[CoreMessage_TMessageKey.SIGNATURE] = sign
+                            } else {
+                                if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
+                                    message.mBodies[CoreMessage_TMessageKey.PUBLIC_KEY] = publicKey
+                                }
+                            }
+                            let secret = "JBSWY3DPEHPK3PXP" // Google Authenticator example
+                            let otp = try TOTPGenerator.generateTOTP(base32Secret: secret, digits: 6, timeStepSeconds: 300)
+                            message.mBodies[CoreMessage_TMessageKey.TOTP] = otp
+                            if let response = Nexilis.writeAndWait(message: message) {
+                                if response.isOk() {
+                                    if flag == MFAViewController.STEP_FIDO_BIOFACE || flag == MFAViewController.STEP_FIDO_BIOFINGER {
+                                        let semaphore = DispatchSemaphore(value: 0)
+                                        var result = true
+                                        var stateErr = 0
+                                        let manager = BiometricStateManager()
+                                        if method == "Sign Up" {
+                                            manager.authenticateAndSaveState { res in
+                                                result = res
+                                                semaphore.signal()
+                                            }
+                                        } else {
+                                            manager.hasBiometricStateChanged { (res, state) in
+                                                result = res
+                                                stateErr = state
+                                                semaphore.signal()
+                                            }
+                                        }
+                                        
+                                        semaphore.wait()
+
+                                        if result {
+                                            DispatchQueue.main.async {
+                                                UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
+                                            }
+                                            APIS.getMFACallback()?(0)
+                                        } else {
+                                            KeyManagerNexilis.deleteKey()
+                                            KeyManagerNexilis.deleteMarker()
+                                            DispatchQueue.main.async {
+                                                var errorMessage = "Gagal mendeteksi Biometric (Touch/Face ID)"
+                                                var errCode = 10
+                                                if stateErr == 1 {
+                                                    errorMessage = "Terjadi Perubahan Biometric (Touch/Face ID)"
+                                                    errCode = 14
+                                                }
+                                                let dialog = DialogErrorMFA()
+                                                dialog.modalTransitionStyle = .crossDissolve
+                                                dialog.modalPresentationStyle = .overCurrentContext
+                                                dialog.errorDesc = errorMessage
+                                                dialog.method = method
+                                                dialog.hideTryAgain = (stateErr == 1)
+                                                dialog.isDismiss = { res in
+                                                    if res == 0 {
+                                                        APIS.logOut()
+                                                        APIS.getMFACallback()?(errCode)
+                                                    }
+                                                }
+                                                UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                                            }
+                                        }
+                                    } else {
+                                        DispatchQueue.main.async {
+                                            UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
+                                        }
+                                        APIS.getMFACallback()?(0)
+                                    }
+                                }
+                                else {
+                                    KeyManagerNexilis.deleteKey()
+                                    KeyManagerNexilis.deleteMarker()
+                                    let errorMessage = response.getBody(key: CoreMessage_TMessageKey.MESSAGE_TEXT, default_value: "Auth Failure".localized())
+                                    let errCode = response.getBodyAsInteger(key: CoreMessage_TMessageKey.ERRAPICOD, default_value: 2)
                                     DispatchQueue.main.async {
-                                        let errorMessage = "Failed to get Auth Data"
                                         let dialog = DialogErrorMFA()
                                         dialog.modalTransitionStyle = .crossDissolve
                                         dialog.modalPresentationStyle = .overCurrentContext
                                         dialog.errorDesc = errorMessage
                                         dialog.method = method
                                         UIApplication.shared.visibleViewController?.present(dialog, animated: true)
-                                        APIS.getMFACallback()?("Failed: \(errorMessage)")
-                                    }
-                                    return
-                                }
-                                let df = HMACDeviceFingerprintNexilis.generate()
-                                message.mBodies[CoreMessage_TMessageKey.FINGERPRINT] = df
-                                if hasKey {
-                                    var sign = ""
-                                    if let dataSign = "\(data)!\(df)".data(using: .utf8) {
-                                        if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
-                                            sign = signature.base64EncodedString()
-                                        }
-                                    }
-                                    message.mBodies[CoreMessage_TMessageKey.SIGNATURE] = sign
-                                } else {
-                                    if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
-                                        message.mBodies[CoreMessage_TMessageKey.PUBLIC_KEY] = publicKey
+                                        APIS.getMFACallback()?(errCode)
                                     }
                                 }
-                                let secret = "JBSWY3DPEHPK3PXP" // Google Authenticator example
-                                let otp = try TOTPGenerator.generateTOTP(base32Secret: secret, digits: 6, timeStepSeconds: 30)
-                                message.mBodies[CoreMessage_TMessageKey.TOTP] = otp
-                                if let response = Nexilis.writeAndWait(message: message) {
-                                    if response.isOk() {
-                                        DispatchQueue.main.async {
-                                            UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
-                                        }
-                                        APIS.getMFACallback()?("Success")
-                                    }
-                                    else {
-                                        let errorMessage = response.getBody(key: CoreMessage_TMessageKey.MESSAGE_TEXT)
-                                        DispatchQueue.main.async {
-                                            let errorMessage = "Failed to get Auth Data"
-                                            let dialog = DialogErrorMFA()
-                                            dialog.modalTransitionStyle = .crossDissolve
-                                            dialog.modalPresentationStyle = .overCurrentContext
-                                            dialog.errorDesc = errorMessage
-                                            dialog.method = method
-                                            UIApplication.shared.visibleViewController?.present(dialog, animated: true)
-                                            APIS.getMFACallback()?("Failed: \(errorMessage)")
-                                        }
-                                    }
+                            } else {
+                                KeyManagerNexilis.deleteKey()
+                                KeyManagerNexilis.deleteMarker()
+                                DispatchQueue.main.async {
+                                    let errorMessage = "Unable to access servers. Check your internet connection and try again later".localized()
+                                    let dialog = DialogErrorMFA()
+                                    dialog.modalTransitionStyle = .crossDissolve
+                                    dialog.modalPresentationStyle = .overCurrentContext
+                                    dialog.errorDesc = errorMessage
+                                    dialog.method = method
+                                    UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                                    APIS.getMFACallback()?(13)
                                 }
                             }
-                        } else {
-                            DispatchQueue.main.async {
-                                let errorMessage = "Failed to get Auth Data"
-                                let dialog = DialogErrorMFA()
-                                dialog.modalTransitionStyle = .crossDissolve
-                                dialog.modalPresentationStyle = .overCurrentContext
-                                dialog.errorDesc = errorMessage
-                                dialog.method = method
-                                UIApplication.shared.visibleViewController?.present(dialog, animated: true)
-                                APIS.getMFACallback()?("Failed: \(errorMessage)")
-                            }
                         }
-                    } catch {
+                    } else {
+                        KeyManagerNexilis.deleteKey()
+                        KeyManagerNexilis.deleteMarker()
+                        DispatchQueue.main.async {
+                            let errorMessage = "Unable to access servers. Check your internet connection and try again later".localized()
+                            let dialog = DialogErrorMFA()
+                            dialog.modalTransitionStyle = .crossDissolve
+                            dialog.modalPresentationStyle = .overCurrentContext
+                            dialog.errorDesc = errorMessage
+                            dialog.method = method
+                            UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                            APIS.getMFACallback()?(13)
+                        }
                     }
+                } catch {
                 }
             }
         }
-        else {
-            let controller = MFAViewController()
-            controller.METHOD = method
-            controller.STEP_NEEDED = flag
-            let navigationController = CustomNavigationController(rootViewController: controller)
-            navigationController.defaultStyle()
-            
-            if UIApplication.shared.visibleViewController?.navigationController != nil {
-                UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
-            } else {
-                UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+    }
+    
+    public static func setFloatingButton(isShow: Bool) {
+        DispatchQueue.main.async {
+            Nexilis.floatingButton.removeFromSuperview()
+            FloatingButton.datePull = nil
+            if isShow {
+                Nexilis.floatingButton = FloatingButton()
+                Nexilis.addFB()
             }
         }
     }
@@ -1123,6 +1415,7 @@ public class APIS: NSObject {
         }
         let idx = Nexilis.IDX_SOCIAL_COMMERCE
         let url = getURLFB(idx: idx)
+        print("HUHU: \(idx) <><> \(url)")
         Nexilis.buttonClicked(index: idx, id: url)
     }
     

+ 109 - 43
NexilisLite/NexilisLite/Source/Callback.swift

@@ -137,59 +137,35 @@ class NetworkMonitor {
     
     private let monitor = NWPathMonitor()
     private var isMonitoring = false
+    
     var isConnected: Bool = false
     var fromDisconnect = false
     var timerReloadData: Timer?
+    private var disconnectWorkItem: DispatchWorkItem?
     
     private init() {}
     
     func startMonitoring() {
         guard !isMonitoring else { return }
         
-        monitor.pathUpdateHandler = { path in
-            self.canAccessGoogle(completion: { [self] connected in
-                self.isConnected = connected
-                InquiryThread.default.set(wait: !connected)
-                OutgoingThread.default.set(wait: !connected)
-                if !connected {
-                    fromDisconnect = true
-                    DispatchQueue.main.async { [self] in
-                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
-                        imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
-                        banner.show()
-                        if Utils.getSecureFolderEncrypt().isEmpty && Database.shared.database != nil {
-                            timerReloadData?.invalidate()
-                            timerReloadData = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) {_ in
-                                Database.shared.database = nil
-                                FileEncryption.shared.aesKey = nil
-                                FileEncryption.shared.aesIV = nil
-                                Database.recreateInstance()
-                                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "disconnected_nexilis"), object: nil, userInfo: nil)
-                                if let navigationC = UIApplication.shared.visibleViewController as? UINavigationController {
-                                    if navigationC.viewControllers[navigationC.viewControllers.count - 1] is EditorPersonal || navigationC.viewControllers[navigationC.viewControllers.count - 1] is EditorGroup {
-                                        navigationC.popViewController(animated: true)
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-                if connected && fromDisconnect {
-                    fromDisconnect = false
-                    DispatchQueue.main.async {
-                        self.timerReloadData?.invalidate()
-                        self.timerReloadData = nil
-                        let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
-                        imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "You're Connected".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
-                        banner.show()
-                    }
-                    if Database.shared.database == nil {
-                        Nexilis.getFeatureAccess()
+        monitor.pathUpdateHandler = { [weak self] path in
+            guard let self = self else { return }
+            
+            self.canAccessGoogle { connected in
+                if connected {
+                    // Cancel any pending "disconnected" work if connection is back
+                    self.disconnectWorkItem?.cancel()
+                    self.disconnectWorkItem = nil
+                    
+                    if !self.isConnected {
+                        self.isConnected = true
+                        self.handleConnected()
                     }
+                } else {
+                    // Debounce disconnection: wait 3s before declaring "offline"
+                    self.scheduleDisconnectionCheck()
                 }
-            })
+            }
         }
         
         let queue = DispatchQueue.global(qos: .background)
@@ -203,6 +179,96 @@ class NetworkMonitor {
         isMonitoring = false
     }
     
+    private func scheduleDisconnectionCheck() {
+        // Cancel any previous disconnection task
+        disconnectWorkItem?.cancel()
+        
+        let workItem = DispatchWorkItem { [weak self] in
+            guard let self = self else { return }
+            
+            self.isConnected = false
+            self.handleDisconnected()
+        }
+        
+        disconnectWorkItem = workItem
+        // Wait 3 seconds before declaring disconnected
+        DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: workItem)
+    }
+    
+    private func handleDisconnected() {
+        fromDisconnect = true
+        DispatchQueue.main.async {
+            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+            imageView.tintColor = .white
+            let banner = FloatingNotificationBanner(
+                title: "Check your connection".localized(),
+                subtitle: nil,
+                titleFont: UIFont.systemFont(ofSize: 16),
+                titleColor: nil,
+                titleTextAlign: .left,
+                subtitleFont: nil,
+                subtitleColor: nil,
+                subtitleTextAlign: nil,
+                leftView: imageView,
+                rightView: nil,
+                style: .danger,
+                colors: nil,
+                iconPosition: .center
+            )
+            banner.show()
+            
+            if Utils.getSecureFolderEncrypt().isEmpty && Database.shared.database != nil {
+                self.timerReloadData?.invalidate()
+                self.timerReloadData = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
+                    Database.shared.database = nil
+                    FileEncryption.shared.aesKey = nil
+                    FileEncryption.shared.aesIV = nil
+                    Database.recreateInstance()
+                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "disconnected_nexilis"), object: nil, userInfo: nil)
+                    
+                    if let navigationC = UIApplication.shared.visibleViewController as? UINavigationController {
+                        if navigationC.viewControllers.last is EditorPersonal ||
+                           navigationC.viewControllers.last is EditorGroup {
+                            navigationC.popViewController(animated: true)
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    private func handleConnected() {
+        if fromDisconnect {
+            fromDisconnect = false
+            DispatchQueue.main.async {
+                self.timerReloadData?.invalidate()
+                self.timerReloadData = nil
+                
+                let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
+                imageView.tintColor = .white
+                let banner = FloatingNotificationBanner(
+                    title: "You're Connected".localized(),
+                    subtitle: nil,
+                    titleFont: UIFont.systemFont(ofSize: 16),
+                    titleColor: nil,
+                    titleTextAlign: .left,
+                    subtitleFont: nil,
+                    subtitleColor: nil,
+                    subtitleTextAlign: nil,
+                    leftView: imageView,
+                    rightView: nil,
+                    style: .success,
+                    colors: nil,
+                    iconPosition: .center
+                )
+                banner.show()
+            }
+            if Database.shared.database == nil {
+                Nexilis.getFeatureAccess()
+            }
+        }
+    }
+    
     func canAccessGoogle(completion: @escaping (Bool) -> Void) {
         guard let url = URL(string: "https://www.google.com") else {
             completion(false)
@@ -212,7 +278,7 @@ class NetworkMonitor {
         var request = URLRequest(url: url)
         request.timeoutInterval = 5
         
-        let task = URLSession.shared.dataTask(with: request) { _, response, error in
+        let task = URLSession.shared.dataTask(with: request) { _, response, _ in
             if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
                 completion(true)
             } else {

+ 1 - 0
NexilisLite/NexilisLite/Source/CoreMessage_TMessageKey.swift

@@ -194,6 +194,7 @@ public class CoreMessage_TMessageKey {
     public static let ANDROID_PACKAGE_NAME = "A95"
     public static let ANDROID_CLASS_NAME     = "A96"
     public static let ERRCOD                 = "A97"
+    public static let ERRAPICOD = "A97A";
     public static let REQUEST_ID             = "A98"
     
     public static let FAMILY_ID            = "A99"

+ 4 - 4
NexilisLite/NexilisLite/Source/Extension.swift

@@ -1906,7 +1906,7 @@ class DataCaptured: NSObject {
             type = "2"
             value = textAction
         }
-        print("sendLogMonitorAction: \(type) <><> \(textAction) <><> \(value)")
+//        print("sendLogMonitorAction: \(type) <><> \(textAction) <><> \(value)")
 //        _ = Nexilis.writeSync(message: CoreMessage_TMessageBank.getLogMonitor(type: type, label: textAction, value: value))
     }
     
@@ -1915,7 +1915,7 @@ class DataCaptured: NSObject {
         if !actVC.isEmpty {
             act = actVC
         }
-        print("sendLogMonitorActivity: \(act)")
+//        print("sendLogMonitorActivity: \(act)")
 //        _ = Nexilis.write(message: CoreMessage_TMessageBank.getLogActivity(pActivityClassName: act))
     }
     
@@ -1925,8 +1925,8 @@ class DataCaptured: NSObject {
         data["filename"] = fileName
         if let jsonData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted),
            let jsonString = String(data: jsonData, encoding: .utf8) {
-            print("sendErrorDLP: \(jsonString)")
-//            _ = Nexilis.write(message: CoreMessage_TMessageBank.getCaptureDLP(data: jsonString))
+//            print("sendErrorDLP: \(jsonString)")
+            _ = Nexilis.write(message: CoreMessage_TMessageBank.getCaptureDLP(data: jsonString))
         }
     }
     

+ 2 - 2
NexilisLite/NexilisLite/Source/FloatingButton/FloatingButton.swift

@@ -33,7 +33,7 @@ public class FloatingButton: UIView, UIGestureRecognizerDelegate {
     let labelCounterFB = UILabel()
     let indicatorCounterFBBig = UIImageView()
     
-    static var datePull: Date?
+    public static var datePull: Date?
     var animationTimer = Timer()
     var configAnim: Int = Int(Utils.getFloatingAnim().components(separatedBy: "~")[0]) ?? 1
     var isLoopingAnim = (Int(Utils.getFloatingAnim().components(separatedBy: "~")[1]) ?? 1) == 1 ? true : false
@@ -389,7 +389,7 @@ public class FloatingButton: UIView, UIGestureRecognizerDelegate {
                 while API.nGetCLXConnState() == 0 || !API.bInetConnAvailable() {
                     Thread.sleep(forTimeInterval: 1)
                 }
-                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.pullFloatingButton(), timeout: 5000) {
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.pullFloatingButton(), timeout: 10000) {
                     if response.isOk() {
                         Utils.setHistoryPullFB(value: response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: ""))
                         setFBFromPull()

+ 50 - 5
NexilisLite/NexilisLite/Source/IncomingThread.swift

@@ -262,11 +262,55 @@ class IncomingThread {
         if let packetId = message.mBodies[CoreMessage_TMessageKey.PACKET_ID] {
             _ = Nexilis.responseString(packetId: packetId, message: "00", timeout: 3000)
         }
-        DispatchQueue.main.async {
-            UIApplication.shared.visibleViewController?.deleteAllRecordDatabase()
-            Nexilis.floatingButton.removeFromSuperview()
-            SecureUserDefaults.shared.removeValue(forKey: "me")
-            Utils.setProfile(value: false)
+        DispatchQueue.global().async {
+            let apiKey = Nexilis.sAPIKey
+            var id = Utils.getConnectionID()
+            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpApi(api: apiKey, p_pin: id), timeout: 15 * 1000) {
+                id = response.getBody(key: CoreMessage_TMessageKey.F_PIN, default_value: "")
+                if(!id.isEmpty){
+//                            Nexilis.changeUser(f_pin: id)
+                    SecureUserDefaults.shared.set(id, forKey: "me")
+                    APIS.sendPushToken(Utils.getTokenAPN(), isResend: true)
+                    Nexilis.sendVersionToBE()
+                    Utils.setProfile(value: false)
+                    if Utils.getForceAnonymous() {
+                        DispatchQueue.main.async {
+                            APIS.setFloatingButton(isShow: false)
+                            UIApplication.shared.visibleViewController?.deleteAllRecordDatabase()
+                        }
+                        SecureUserDefaults.shared.removeValue(forKey: "device_id")
+                        Nexilis.destroyAll()
+                        _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: id))
+                    }
+                    DispatchQueue.main.async {
+                        var dataImage: [AnyHashable : Any] = [:]
+                        dataImage["name"] = ""
+                        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil, userInfo: dataImage)
+                        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
+                        if !Utils.getForceAnonymous() {
+                            Nexilis.showForceSignIn()
+                        }
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        Nexilis.hideLoader(completion: {
+                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                            banner.show()
+                        })
+                    }
+                }
+            } else {
+                DispatchQueue.main.async {
+                    Nexilis.hideLoader(completion: {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                    })
+                }
+            }
         }
         ack(message: message)
     }
@@ -1289,6 +1333,7 @@ class IncomingThread {
     }
     
     private func receiveMessage(message: TMessage) -> Void {
+        print("recive message \(message.toLogString())")
         if Utils.getSecureFolderOffline() == "0" {
             if API.nGetCLXConnState() == 0 {
                 do {

+ 41 - 0
NexilisLite/NexilisLite/Source/MasterKeyUtil.swift

@@ -8,6 +8,7 @@
 import CryptoKit
 import LocalAuthentication
 import UIKit
+import LocalAuthentication
 
 
 public class MasterKeyUtil {
@@ -468,3 +469,43 @@ class KeyManagerNexilis {
         return signature
     }
 }
+
+class BiometricStateManager {
+    func authenticateAndSaveState(completion: @escaping (Bool) -> Void) {
+        let context = LAContext()
+        var error: NSError?
+
+        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
+            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
+                                   localizedReason: "Daftarkan biometric anda!") { success, _ in
+                if success, let domainState = context.evaluatedPolicyDomainState {
+                    Utils.setBiometricState(value: domainState)
+                }
+                completion(success)
+            }
+        } else {
+            completion(false)
+        }
+    }
+    
+    func hasBiometricStateChanged(completion: @escaping (Bool, Int) -> Void) {
+        let context = LAContext()
+        var error: NSError?
+
+        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
+            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
+                                   localizedReason: "Validasi biometric anda!") { success, _ in
+                if success, let currentState = context.evaluatedPolicyDomainState,
+                   let savedState = Utils.getBiometricState() {
+                    completion(savedState == currentState, 1)
+                } else {
+                    completion(false, 0)
+                }
+            }
+        } else {
+            completion(false, 0)
+        }
+    }
+}
+
+

+ 29 - 6
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.52"
+    public static var cpaasVersion = "5.0.55"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""
@@ -257,9 +257,9 @@ public class Nexilis: NSObject {
                                     if !enable_signup && !userId.isEmpty {
                                         enable_signup = true
                                         Utils.setProfile(value: true)
-                                        KeyManagerNexilis.deleteKey()
-                                        KeyManagerNexilis.deleteMarker()
                                     }
+                                    KeyManagerNexilis.deleteKey()
+                                    KeyManagerNexilis.deleteMarker()
                                     Utils.setForceAnonymous(value: enable_signup)
                                     if(!id.isEmpty) {
                                         SecureUserDefaults.shared.set(id, forKey: "me")
@@ -342,7 +342,7 @@ public class Nexilis: NSObject {
                             showForceSignIn()
                         }
                     }
-                    getServiceBank()
+//                    getServiceBank()
                     getPullWorkingArea()
                     getPullGroupNoMember()
                     getWhitelistFileExt()
@@ -657,10 +657,33 @@ public class Nexilis: NSObject {
                                 }
                                 if jsonData["chatbot_greetings"]! != nil {
                                     if let greeting = jsonData["chatbot_greetings"] as? String {
-                                        print("Chatbot greeting: \(greeting)")
+//                                        print("Chatbot greeting: \(greeting)")
                                         Utils.setChatbotGreetings(value: greeting)
                                     }
                                 }
+                                if jsonData["mfa_signup"]! != nil {
+                                    if let data = jsonData["mfa_signup"] as? String {
+//                                        print("mfa_signup: \(data)")
+                                        Utils.setSignUpLevel(value: data)
+                                    }
+                                }
+                                if jsonData["mfa_signin"]! != nil {
+                                    if let data = jsonData["mfa_signin"] as? String {
+//                                        print("mfa_signin: \(data)")
+                                        Utils.setSignInLevel(value: data)
+                                    }
+                                }
+                                if jsonData["mfa_trx"]! != nil {
+                                    if let data = jsonData["mfa_trx"] as? String {
+//                                        print("mfa_trx: \(data)")
+                                        let encoder = JSONEncoder()
+                                        encoder.outputFormatting = .prettyPrinted
+                                        let jsonData = try encoder.encode(data)
+                                        if let jsonString = String(data: jsonData, encoding: .utf8) {
+                                            Utils.setTxnLevel(value: jsonString)
+                                        }
+                                    }
+                                }
                             }
                             keyTemp = ""
                             keyIvTemp = ""
@@ -3420,7 +3443,7 @@ extension Nexilis: MessageDelegate {
                     data["l_pin"] = message.getBody(key: CoreMessage_TMessageKey.L_PIN)
                     data["f_display_name"] = message.getBody(key: CoreMessage_TMessageKey.F_DISPLAY_NAME)
                     Nexilis.onGoingPushCC = data
-                } else if Nexilis.onGoingPushCC["f_display_name"] == message.getBody(key: CoreMessage_TMessageKey.F_DISPLAY_NAME) {
+                } else if Nexilis.onGoingPushCC["l_pin"] == message.getBody(key: CoreMessage_TMessageKey.L_PIN) {
                     return
                 }
                 let alert = LibAlertController(title: "", message: "\n\n\n\n\n\n\n\n\n\n".localized(), preferredStyle: .alert)

+ 71 - 15
NexilisLite/NexilisLite/Source/Utils.swift

@@ -1526,6 +1526,47 @@ public final class Utils {
         return value
     }
     
+    public static func setBiometricState(value: Data?) {
+        SecureUserDefaults.shared.set(value, forKey: "pb_biometric_state")
+    }
+    public static func getBiometricState() -> Data? {
+        let value: Data? = SecureUserDefaults.shared.value(forKey: "pb_biometric_state")
+        return value
+    }
+    
+    static func setSignUpLevel(value: String) {
+        SecureUserDefaults.shared.set(value, forKey: "pb_signup_level")
+    }
+
+    static func getSignUpLevel() -> String {
+        if let value: String = SecureUserDefaults.shared.value(forKey: "pb_signup_level") {
+            return value
+        }
+        return "1,2"
+    }
+    
+    static func setSignInLevel(value: String) {
+        SecureUserDefaults.shared.set(value, forKey: "pb_signin_level")
+    }
+
+    static func getSignInLevel() -> String {
+        if let value: String = SecureUserDefaults.shared.value(forKey: "pb_signin_level") {
+            return value
+        }
+        return "1,2"
+    }
+    
+    static func setTxnLevel(value: String) {
+        SecureUserDefaults.shared.set(value, forKey: "pb_txn_level")
+    }
+
+    static func getTxnLevel() -> String {
+        if let value: String = SecureUserDefaults.shared.value(forKey: "pb_txn_level") {
+            return value
+        }
+        return ""
+    }
+    
     static func getPasswordDB() -> String? {
         do {
             let p = getPassEncDB()
@@ -2202,6 +2243,7 @@ public class DialogVerifyYou: UIViewController {
                                 banner.show()
                                 if Nexilis.showFB {
                                     Nexilis.floatingButton.removeFromSuperview()
+                                    FloatingButton.datePull = nil
                                     Nexilis.floatingButton = FloatingButton()
                                     Nexilis.addFB()
                                 }
@@ -2833,6 +2875,7 @@ public class DialogErrorMFA: UIViewController {
     
     public var errorDesc = ""
     public var method = ""
+    public var hideTryAgain = false
     var isDismiss: ((Int) -> ())?
     
     public override func viewDidLoad() {
@@ -2853,9 +2896,9 @@ public class DialogErrorMFA: UIViewController {
         title.textAlignment = .center
         title.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
         container.addSubview(title)
-        title.anchor(top: container.topAnchor, paddingTop: 15, centerX: container.centerXAnchor, maxWidth: 270)
+        title.anchor(top: container.topAnchor, paddingTop: 15, centerX: container.centerXAnchor, maxWidth: UIScreen.main.bounds.width / 2)
         
-        let imageWarning = UIImageView(image: UIImage(named: "pb_security_warning", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
+        let imageWarning = UIImageView(image: UIImage(named: "pb_security_warning_green", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
         container.addSubview(imageWarning)
         imageWarning.anchor(top: container.topAnchor, right: title.leftAnchor, paddingTop: 10, paddingRight: -5, width: 30, height: 30)
         
@@ -2867,7 +2910,10 @@ public class DialogErrorMFA: UIViewController {
         container.addSubview(imageChat)
         imageChat.anchor(top: container.topAnchor, right: container.rightAnchor, paddingTop: 10, paddingRight: 10, width: 30, height: 30)
         
-        let contentDesc = "Silakan coba lagi atau hubungi Contact Center BJB untuk bantuan lebih lanjut"
+        var contentDesc = "Silakan coba lagi atau hubungi Contact Center BJB untuk bantuan lebih lanjut"
+        if hideTryAgain {
+            contentDesc = "Silakan hubungi Contact Center BJB untuk bantuan lebih lanjut atau Silahkan Sign Up/Sign In Ulang"
+        }
         let contentS = UILabel()
         contentS.tintColor = .label
         contentS.attributedText = contentDesc.richText()
@@ -2884,18 +2930,24 @@ public class DialogErrorMFA: UIViewController {
         buttonCC.clipsToBounds = true
         buttonCC.addTarget(self, action: #selector(ccTapped), for: .touchUpInside)
         container.addSubview(buttonCC)
-        buttonCC.anchor(top: contentS.bottomAnchor, paddingTop: 20, centerX: container.centerXAnchor, width: UIScreen.main.bounds.width / 3 - 30, height: 35)
+        if !hideTryAgain {
+            buttonCC.anchor(top: contentS.bottomAnchor, paddingTop: 20, centerX: container.centerXAnchor, width: UIScreen.main.bounds.width / 3 - 30, height: 35)
+        } else {
+            buttonCC.anchor(top: contentS.bottomAnchor, left: container.leftAnchor, paddingTop: 20, paddingLeft: 5, width: UIScreen.main.bounds.width / 2 - 30, height: 35)
+        }
         
-        let buttonTryAgain = UIButton(type: .custom)
-        buttonTryAgain.setTitle("Coba Lagi", for: .normal)
-        buttonTryAgain.backgroundColor = .mainColor
-        buttonTryAgain.titleLabel?.textColor = .white
-        buttonTryAgain.titleLabel?.font = .boldSystemFont(ofSize: 14)
-        buttonTryAgain.layer.cornerRadius = 17.5
-        buttonTryAgain.clipsToBounds = true
-        buttonTryAgain.addTarget(self, action: #selector(tryAgainTapped), for: .touchUpInside)
-        container.addSubview(buttonTryAgain)
-        buttonTryAgain.anchor(top: contentS.bottomAnchor, right: buttonCC.leftAnchor, paddingTop: 20, paddingRight: 5, width: UIScreen.main.bounds.width / 3 - 30, height: 35)
+        if !hideTryAgain {
+            let buttonTryAgain = UIButton(type: .custom)
+            buttonTryAgain.setTitle("Coba Lagi", for: .normal)
+            buttonTryAgain.backgroundColor = .mainColor
+            buttonTryAgain.titleLabel?.textColor = .white
+            buttonTryAgain.titleLabel?.font = .boldSystemFont(ofSize: 14)
+            buttonTryAgain.layer.cornerRadius = 17.5
+            buttonTryAgain.clipsToBounds = true
+            buttonTryAgain.addTarget(self, action: #selector(tryAgainTapped), for: .touchUpInside)
+            container.addSubview(buttonTryAgain)
+            buttonTryAgain.anchor(top: contentS.bottomAnchor, right: buttonCC.leftAnchor, paddingTop: 20, paddingRight: 5, width: UIScreen.main.bounds.width / 3 - 30, height: 35)
+        }
         
         let buttonReject = UIButton(type: .custom)
         buttonReject.setTitle("Tutup", for: .normal)
@@ -2906,7 +2958,11 @@ public class DialogErrorMFA: UIViewController {
         buttonReject.clipsToBounds = true
         buttonReject.addTarget(self, action: #selector(rejectTapped), for: .touchUpInside)
         container.addSubview(buttonReject)
-        buttonReject.anchor(top: contentS.bottomAnchor, left: buttonCC.rightAnchor, paddingTop: 20, paddingLeft: 5, width: UIScreen.main.bounds.width / 3 - 30, height: 35)
+        if !hideTryAgain {
+            buttonReject.anchor(top: contentS.bottomAnchor, left: buttonCC.rightAnchor, paddingTop: 20, paddingLeft: 5, width: UIScreen.main.bounds.width / 3 - 30, height: 35)
+        } else {
+            buttonReject.anchor(top: contentS.bottomAnchor, right: container.rightAnchor, paddingTop: 20, paddingRight: 5, width: UIScreen.main.bounds.width / 2 - 30, height: 35)
+        }
         
         let footer = UILabel()
         footer.text = "We value your security".localized()

+ 17 - 0
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -3254,6 +3254,23 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
         }
         DispatchQueue.global().async {
             let message = CoreMessage_TMessageBank.getRequestCallCenter(p_channel: channel, category_id: service_id)
+            message.mBodies["wlc_device"] = "\(UIDevice.current.model)(\(UIDevice.current.name))"
+            message.mBodies["wlc_time"] = "\(Date().currentTimeMillis())"
+            message.mBodies["wlc_longitude"] = "\(self.longitude)"
+            message.mBodies["wlc_latitude"] = "\(self.latitude)"
+            if !self.contextCC.isEmpty {
+                let dataSplit = self.contextCC.components(separatedBy: "~")
+                var activity = ""
+                var titleErr = ""
+                if dataSplit.count > 1 {
+                    activity = dataSplit[1]
+                }
+                if dataSplit.count > 2 {
+                    titleErr = dataSplit[2]
+                }
+                message.mBodies["wlc_activity"] = "\(activity)"
+                message.mBodies["wlc_error_description"] = "\(titleErr)"
+            }
             if let response = Nexilis.writeSync(message: message) {
                 if !self.isDirectCC {
                     DispatchQueue.main.async {

+ 2 - 0
NexilisLite/NexilisLite/Source/View/Control/ChangeDeviceViewController.swift

@@ -445,6 +445,7 @@ public class ChangeDeviceViewController: UIViewController {
                 Nexilis.hideLoader(completion: {
                     if Nexilis.showFB {
                         Nexilis.floatingButton.removeFromSuperview()
+                        FloatingButton.datePull = nil
                         Nexilis.floatingButton = FloatingButton()
                         Nexilis.addFB()
                     }
@@ -484,6 +485,7 @@ public class ChangeDeviceViewController: UIViewController {
                     banner.show()
                     if Nexilis.showFB {
                         Nexilis.floatingButton.removeFromSuperview()
+                        FloatingButton.datePull = nil
                         Nexilis.floatingButton = FloatingButton()
                         Nexilis.addFB()
                     }

+ 1 - 0
NexilisLite/NexilisLite/Source/View/Control/ConfigureFloatingButton.swift

@@ -117,6 +117,7 @@ public class ConfigureFloatingButton: UIViewController {
         }
         DispatchQueue.main.async {
             Nexilis.floatingButton.removeFromSuperview()
+            FloatingButton.datePull = nil
             Nexilis.floatingButton = FloatingButton()
             Nexilis.addFB()
             if Nexilis.fromMAB {

+ 45 - 30
NexilisLite/NexilisLite/Source/View/Control/MFAViewController.swift

@@ -13,11 +13,15 @@ import nuSDKService
 
 // MARK: - MFA Class
 class MFAViewController: UIViewController {
-    static let STEP_NEEDED_FIDO = 1
-    static let STEP_NEEDED_FIDO_PWD = 2
-    static let STEP_NEEDED_FIDO_PWD_BIOMETRIC = 3
     
-    var STEP_NEEDED = STEP_NEEDED_FIDO_PWD
+    static let STEP_FIDO = "1";
+    static let STEP_FIDO_PWD = "1,2";
+    static let STEP_FIDO_PWD_BIOFINGER = "1,2,3";
+    static let STEP_FIDO_PWD_BIOFACE = "1,2,4";
+    static let STEP_FIDO_BIOFINGER = "1,3";
+    static let STEP_FIDO_BIOFACE = "1,4";
+    
+    var STEP_NEEDED = STEP_FIDO_PWD
     var METHOD = ""
 
     private let imageViewBackground = UIImageView()
@@ -88,6 +92,7 @@ class MFAViewController: UIViewController {
     }
     
     @objc func cancel(sender: Any) {
+        APIS.getMFACallback()?(99)
         navigationController?.dismiss(animated: true, completion: nil)
     }
     
@@ -285,7 +290,7 @@ class MFAViewController: UIViewController {
                     KeyManagerNexilis.deleteMarker()
                     DispatchQueue.main.async {
                         Nexilis.hideLoader {
-                            let errorMessage = "Failed to get Private Key"
+                            let errorMessage = "PPKey Generated Failed".localized()
                             let dialog = DialogErrorMFA()
                             dialog.modalTransitionStyle = .crossDissolve
                             dialog.modalPresentationStyle = .overCurrentContext
@@ -294,7 +299,7 @@ class MFAViewController: UIViewController {
                             dialog.isDismiss = { res in
                                 if res == 0 {
                                     self.navigationController?.dismiss(animated: true, completion: {
-                                        APIS.getMFACallback()?("Failed: \(errorMessage)")
+                                        APIS.getMFACallback()?(5)
                                     })
                                 }
                             }
@@ -311,7 +316,7 @@ class MFAViewController: UIViewController {
                                 KeyManagerNexilis.deleteKey()
                                 KeyManagerNexilis.deleteMarker()
                                 Nexilis.hideLoader {
-                                    let errorMessage = "Failed to get Auth Data"
+                                    let errorMessage = "Auth Failure".localized()
                                     let dialog = DialogErrorMFA()
                                     dialog.modalTransitionStyle = .crossDissolve
                                     dialog.modalPresentationStyle = .overCurrentContext
@@ -320,7 +325,7 @@ class MFAViewController: UIViewController {
                                     dialog.isDismiss = { res in
                                         if res == 0 {
                                             self.navigationController?.dismiss(animated: true, completion: {
-                                                APIS.getMFACallback()?("Failed: \(errorMessage)")
+                                                APIS.getMFACallback()?(2)
                                             })
                                         }
                                     }
@@ -345,11 +350,11 @@ class MFAViewController: UIViewController {
                             }
                         }
                         let secret = "JBSWY3DPEHPK3PXP" // Google Authenticator example
-                        let otp = try TOTPGenerator.generateTOTP(base32Secret: secret, digits: 6, timeStepSeconds: 30)
+                        let otp = try TOTPGenerator.generateTOTP(base32Secret: secret, digits: 6, timeStepSeconds: 300)
                         tMessage.mBodies[CoreMessage_TMessageKey.TOTP] = otp
                         if let response = Nexilis.writeAndWait(message: tMessage) {
                             if response.isOk() {
-                                if self.STEP_NEEDED == MFAViewController.STEP_NEEDED_FIDO_PWD_BIOMETRIC {
+                                if self.STEP_NEEDED == MFAViewController.STEP_FIDO_PWD_BIOFINGER || self.STEP_NEEDED == MFAViewController.STEP_FIDO_PWD_BIOFACE {
                                     self.biometricAuth()
                                 } else {
                                     DispatchQueue.main.async {
@@ -357,7 +362,7 @@ class MFAViewController: UIViewController {
                                             self.navigationController?.dismiss(animated: true, completion: {
                                                 UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
                                                 self.dismissKeyboard()
-                                                APIS.getMFACallback()?("Success")
+                                                APIS.getMFACallback()?(0)
                                             })
                                         }
                                     }
@@ -368,7 +373,8 @@ class MFAViewController: UIViewController {
                                     KeyManagerNexilis.deleteKey()
                                     KeyManagerNexilis.deleteMarker()
                                     Nexilis.hideLoader {
-                                        let errorMessage = response.getBody(key: CoreMessage_TMessageKey.MESSAGE_TEXT)
+                                        let errorMessage = response.getBody(key: CoreMessage_TMessageKey.MESSAGE_TEXT, default_value: "Auth Failure".localized())
+                                        let errCode = response.getBodyAsInteger(key: CoreMessage_TMessageKey.ERRAPICOD, default_value: 2)
                                         let dialog = DialogErrorMFA()
                                         dialog.modalTransitionStyle = .crossDissolve
                                         dialog.modalPresentationStyle = .overCurrentContext
@@ -377,7 +383,7 @@ class MFAViewController: UIViewController {
                                         dialog.isDismiss = { res in
                                             if res == 0 {
                                                 self.navigationController?.dismiss(animated: true, completion: {
-                                                    APIS.getMFACallback()?("Failed: \(errorMessage)")
+                                                    APIS.getMFACallback()?(errCode)
                                                 })
                                             }
                                         }
@@ -392,7 +398,7 @@ class MFAViewController: UIViewController {
                         KeyManagerNexilis.deleteKey()
                         KeyManagerNexilis.deleteMarker()
                         Nexilis.hideLoader {
-                            let errorMessage = "Failed to get Auth Data"
+                            let errorMessage = "Unable to access servers. Check your internet connection and try again later".localized()
                             let dialog = DialogErrorMFA()
                             dialog.modalTransitionStyle = .crossDissolve
                             dialog.modalPresentationStyle = .overCurrentContext
@@ -401,7 +407,7 @@ class MFAViewController: UIViewController {
                             dialog.isDismiss = { res in
                                 if res == 0 {
                                     self.navigationController?.dismiss(animated: true, completion: {
-                                        APIS.getMFACallback()?("Failed: \(errorMessage)")
+                                        APIS.getMFACallback()?(13)
                                     })
                                 }
                             }
@@ -417,18 +423,22 @@ class MFAViewController: UIViewController {
 
     private func biometricAuth() {
         let semaphore = DispatchSemaphore(value: 0)
-        var result = false
-
-        Utils.authenticateWithBiometrics { success, errorMessage in
-            if success {
-                print("Access granted!")
-                result = true
-            } else {
-                print("Access denied: \(errorMessage ?? "Unknown error")")
+        var result = true
+        var stateErr = 0
+        let manager = BiometricStateManager()
+        if self.METHOD == "Sign Up" {
+            manager.authenticateAndSaveState { res in
+                result = res
+                semaphore.signal()
+            }
+        } else {
+            manager.hasBiometricStateChanged { (res, state) in
+                result = res
+                stateErr = state
+                semaphore.signal()
             }
-            semaphore.signal()
         }
-
+        
         semaphore.wait()
 
         if result {
@@ -437,7 +447,7 @@ class MFAViewController: UIViewController {
                     self.navigationController?.dismiss(animated: true, completion: {
                         UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
                         self.dismissKeyboard()
-                        APIS.getMFACallback()?("Success")
+                        APIS.getMFACallback()?(0)
                     })
                 }
             }
@@ -446,17 +456,22 @@ class MFAViewController: UIViewController {
             KeyManagerNexilis.deleteMarker()
             DispatchQueue.main.async {
                 Nexilis.hideLoader {
-                    let errorMessage = "Gagal mendeteksi Biometric (Fingerprint/Face ID)"
+                    var errorMessage = "Gagal mendeteksi Biometric (Touch/Face ID)"
+                    var errCode = 10
+                    if stateErr == 1 {
+                        errorMessage = "Terjadi Perubahan Biometric (Touch/Face ID)"
+                        errCode = 14
+                    }
                     let dialog = DialogErrorMFA()
                     dialog.modalTransitionStyle = .crossDissolve
                     dialog.modalPresentationStyle = .overCurrentContext
                     dialog.errorDesc = errorMessage
                     dialog.method = self.METHOD
+                    dialog.hideTryAgain = (stateErr == 1)
                     dialog.isDismiss = { res in
                         if res == 0 {
-                            self.navigationController?.dismiss(animated: true, completion: {
-                                APIS.getMFACallback()?("Failed: \(errorMessage)")
-                            })
+                            APIS.logOut()
+                            APIS.getMFACallback()?(errCode)
                         }
                     }
                     UIApplication.shared.visibleViewController?.present(dialog, animated: true)

+ 2 - 1
NexilisLite/NexilisLite/Source/View/Control/SettingTableViewController.swift

@@ -635,7 +635,7 @@ public class SettingTableViewController: UITableViewController, UIGestureRecogni
                 DispatchQueue.global().async {
                     let apiKey = Nexilis.sAPIKey
                     var id = Utils.getConnectionID()
-                    if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpApi(api: apiKey, p_pin: id), timeout: 30 * 1000) {
+                    if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpApi(api: apiKey, p_pin: id), timeout: 15 * 1000) {
                         id = response.getBody(key: CoreMessage_TMessageKey.F_PIN, default_value: "")
                         if(!id.isEmpty){
 //                            Nexilis.changeUser(f_pin: id)
@@ -658,6 +658,7 @@ public class SettingTableViewController: UITableViewController, UIGestureRecogni
                                     banner.show()
                                     if Nexilis.showFB {
                                         Nexilis.floatingButton.removeFromSuperview()
+                                        FloatingButton.datePull = nil
                                         Nexilis.floatingButton = FloatingButton()
                                         Nexilis.addFB()
                                     }

+ 2 - 0
NexilisLite/NexilisLite/Source/View/Control/SignUpSignIn.swift

@@ -507,6 +507,7 @@ public class SignUpSignIn: UIViewController {
                     Nexilis.hideLoader(completion: {
                         if Nexilis.showFB {
                             Nexilis.floatingButton.removeFromSuperview()
+                            FloatingButton.datePull = nil
                             Nexilis.floatingButton = FloatingButton()
                             Nexilis.addFB()
                         }
@@ -546,6 +547,7 @@ public class SignUpSignIn: UIViewController {
                         banner.show()
                         if Nexilis.showFB {
                             Nexilis.floatingButton.removeFromSuperview()
+                            FloatingButton.datePull = nil
                             Nexilis.floatingButton = FloatingButton()
                             Nexilis.addFB()
                         }

+ 95 - 7
StreamShield/StreamShield/Source/SecurityShield.swift

@@ -17,6 +17,7 @@ import CommonCrypto
 import SystemConfiguration.CaptiveNetwork
 import CoreLocation
 import Network
+import CoreMotion
 
 public class SecurityShield: NSObject {
     
@@ -1165,7 +1166,7 @@ private class Process: NSObject, CLLocationManagerDelegate {
         let semaphore = DispatchSemaphore(value: 0)
         
         DispatchQueue.main.async {
-            LocationFetcher.shared.getCurrentLocation { coordinate in
+            LocationFetcher.shared.getCurrentLocation { coordinate, score in
                 var long = "0"
                 var lat = "0"
                 if let coord = coordinate {
@@ -1381,9 +1382,21 @@ private class Process: NSObject, CLLocationManagerDelegate {
 private class LocationFetcher: NSObject, CLLocationManagerDelegate {
     static var shared = LocationFetcher()
     private var manager: CLLocationManager?
-    private var completion: ((CLLocationCoordinate2D?) -> Void)?
+    private var completion: ((CLLocationCoordinate2D?, Int) -> Void)?
+    let motionMgr = CMMotionActivityManager()
     
-    func getCurrentLocation(completion: @escaping (CLLocationCoordinate2D?) -> Void) {
+    func motionSnapshot(_ done: @escaping (CMMotionActivity?) -> Void) {
+        guard CMMotionActivityManager.isActivityAvailable() else {
+            done(nil)
+            return
+        }
+        let now = Date()
+        motionMgr.queryActivityStarting(from: now.addingTimeInterval(-120), to: now, to: .main) { acts, _ in
+            done(acts?.last)
+        }
+    }
+    
+    func getCurrentLocation(completion: @escaping (CLLocationCoordinate2D?, Int) -> Void) {
         self.completion = completion
         self.manager = CLLocationManager()
         self.manager?.delegate = self
@@ -1393,19 +1406,22 @@ private class LocationFetcher: NSObject, CLLocationManagerDelegate {
         if CLLocationManager.locationServicesEnabled() {
             self.manager?.requestLocation()
         } else {
-            completion(nil)
+            completion(nil, 0)
         }
     }
     
     // MARK: - CLLocationManagerDelegate
     func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
-        completion?(locations.last?.coordinate)
-        cleanup()
+        motionSnapshot { snap in
+            let (gpsScore, gpsReasons) = FakeGps.movementAndAccuracy(prev: locations.first, curr: locations.last!, motion: snap)
+            self.completion?(locations.last?.coordinate, gpsScore)
+        }
+//        cleanup()
     }
 
     func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
         print("Error: \(error.localizedDescription)")
-        completion?(nil)
+        completion?(nil, 0)
         cleanup()
     }
     
@@ -1415,6 +1431,78 @@ private class LocationFetcher: NSObject, CLLocationManagerDelegate {
         manager = nil
         completion = nil
     }
+    
+    enum FakeGps {
+        static func movementAndAccuracy(prev: CLLocation?, curr: CLLocation, motion: CMMotionActivity?) -> (Int, [String]) {
+            var score = 0
+            var reasons: [String] = []
+            
+            // Accuracy check
+            if curr.horizontalAccuracy > 200 {
+                score += 10
+                reasons.append("Low accuracy (>200m).")
+            }
+            
+            // Movement checks
+            if let p = prev {
+                let dt = curr.timestamp.timeIntervalSince(p.timestamp)
+                if dt >= 3 {
+                    let d = curr.distance(from: p)
+                    let v = d / dt
+                    let vRep = curr.speed > 0 ? Double(curr.speed) : v
+                    
+                    if v > 150 && vRep < 10 {
+                        score += 40
+                        reasons.append("Unrealistic jump vs reported speed.")
+                    }
+                    
+                    if v > 350 {
+                        score += 60
+                        reasons.append("Physically implausible speed (>350 m/s).")
+                    }
+                    
+                    if curr.horizontalAccuracy <= 8 && d > 1000 {
+                        score += 20
+                        reasons.append("High accuracy but >1 km jump.")
+                    }
+                    
+                    if p.courseAccuracy >= 0 && curr.courseAccuracy >= 0 {
+                        let delta = abs(curr.course - p.course)
+                        if delta < 1 && d > 3000 {
+                            score += 10
+                            reasons.append("Near-zero course jitter over long distance.")
+                        }
+                    }
+                    
+                    // NEW: unnatural smoothness
+                    let speedDiff = abs(curr.speed - Double(v))
+                    if speedDiff < 0.5 && d > 100 {
+                        score += 10
+                        reasons.append("Unnaturally smooth trajectory.")
+                    }
+                }
+            }
+            
+            // Motion vs GPS mismatch
+            if let m = motion {
+                let moving = (m.walking || m.running || m.cycling || m.automotive)
+                if !moving && curr.speed > 8 {
+                    score += 20
+                    reasons.append("High speed while motion reports stationary.")
+                }
+            }
+            
+            // Timezone mismatch check
+            let deviceTZ = TimeZone.current
+            let gpsTZ = TimeZone(secondsFromGMT: Int(curr.timestamp.timeIntervalSince1970)) // heuristic only
+            if let gpsTZ = gpsTZ, gpsTZ.secondsFromGMT() != deviceTZ.secondsFromGMT() {
+                score += 5
+                reasons.append("Timezone mismatch with GPS region (heuristic).")
+            }
+            
+            return (min(100, score), reasons)
+        }
+    }
 }
 
 private class Service {