Browse Source

update for demo BJB and release for 5.0.53

alqindiirsyam 5 ngày trước cách đây
mục cha
commit
328425265a

BIN
.DS_Store


BIN
NexilisLite/.DS_Store


+ 21 - 0
NexilisLite/NexilisLite/Resource/Assets.xcassets/bjb-blue-flat.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "bjb-blue-flat.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
NexilisLite/NexilisLite/Resource/Assets.xcassets/bjb-blue-flat.imageset/bjb-blue-flat.png


+ 136 - 18
NexilisLite/NexilisLite/Source/APIS.swift

@@ -160,6 +160,34 @@ public class APIS: NSObject {
         }
     }
     
+    public static func openContactCenterWithContext(context: String) {
+        let isChangeProfile = Utils.getSetProfile()
+        if !isChangeProfile {
+            APIS.showChangeProfile()
+            return
+        }
+        if !Nexilis.checkingAccess(key: "call_center") {
+            if Nexilis.checkingAccessAlert(key: "call_center") != "|" && !Nexilis.checkingAccessAlert(key: "call_center").isEmpty {
+                let title = Nexilis.checkingAccessAlert(key: "call_center").components(separatedBy: "|")[0]
+                let message = Nexilis.checkingAccessAlert(key: "call_center").components(separatedBy: "|")[1]
+                APIS.nexilisShowAlertWithHTMLMessage(on: UIApplication.shared.visibleViewController ?? UIViewController(), title: title, message: message)
+            } else {
+                UIApplication.shared.visibleViewController?.view.makeToast("Feature disabled".localized(), duration: 5)
+            }
+            return
+        }
+        let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+        controller.isContactCenter = true
+        controller.contextCC = context
+        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 openUrl(url: String) {
         let isChangeProfile = Utils.getSetProfile()
         if !isChangeProfile {
@@ -511,6 +539,16 @@ public class APIS: NSObject {
         }
     }
     
+    private static var mfaCallback: ((String) -> Void)?
+
+    static func getMFACallback() -> ((String) -> Void)? {
+        return mfaCallback
+    }
+
+    public static func setMFACallback(_ callback: @escaping (String) -> Void) {
+        mfaCallback = callback
+    }
+    
     public static func openMFA(method: String, flag: Int){
         let isChangeProfile = Utils.getSetProfile()
         if !isChangeProfile {
@@ -518,32 +556,112 @@ public class APIS: NSObject {
             return
         }
         if flag == MFAViewController.STEP_NEEDED_FIDO {
-            if let me = User.getMyPin() {
-                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getMFAValidation(data: me)) {
-                    if response.isOk() {
-                        UIApplication.shared.visibleViewController?.view.makeToast("Action Successful".localized(), duration: 3)
-                    }
-                    else {
-                        UIApplication.shared.visibleViewController?.view.makeToast(response.mBodies[CoreMessage_TMessageKey.MESSAGE_TEXT], duration: 3)
+            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 = "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 {
+                                    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
+                                    }
+                                }
+                                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 {
+                            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 if flag == MFAViewController.STEP_NEEDED_FINGER {
-            let controller = MFAOnlyBiometricViewController()
-            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)
-            }
         }
         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 {

+ 30 - 16
NexilisLite/NexilisLite/Source/Extension.swift

@@ -991,16 +991,17 @@ extension String {
         let textUTF8 = self
         let finalText = NSMutableAttributedString(string: textUTF8, attributes: [.font: font])
         
-        let formattingRules: [(String, [NSAttributedString.Key: Any])] = [
-            ("_", [.font: italicFont]), // Italic
-            ("*", [.font: boldFont]), // Bold
-            ("~", [.strikethroughStyle: NSUnderlineStyle.single.rawValue]),
-            ("^", [.underlineStyle: NSUnderlineStyle.single.rawValue]),
-            ("$", [.font: italicFont, .foregroundColor: UIColor.darkGray]) // Italic + Gray for $
+        let rules: [(String, [NSAttributedString.Key: Any])] = [
+            ("_", [NSAttributedString.Key.font: italicFont]),
+            ("*", [NSAttributedString.Key.font: boldFont]),
+            ("~", [NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.single.rawValue]),
+            ("^", [NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue]),
+            ("$", [NSAttributedString.Key.font: italicFont,
+                   NSAttributedString.Key.foregroundColor: UIColor.darkGray])
         ]
 
-        for (sign, attributes) in formattingRules {
-            applyTextFormatting(to: finalText, sign: sign, attributes: attributes, isEditing: isEditing)
+        for (sign, attributes) in rules {
+            applyTextFormatting(to: finalText, sign: sign, attributes: attributes, isEditing: isEditing, boldItalicFont: boldItalicFont)
         }
         
         processMentions(in: finalText, groupID: group_id, isEditing: isEditing, listMentionInTextField: listMentionInTextField)
@@ -1018,29 +1019,42 @@ extension String {
         to text: NSMutableAttributedString,
         sign: String,
         attributes: [NSAttributedString.Key: Any],
-        isEditing: Bool
+        isEditing: Bool,
+        boldItalicFont: UIFont? = nil
     ) {
         let escapedSign = NSRegularExpression.escapedPattern(for: sign)
-        let pattern = "\(escapedSign)(.+?)\(escapedSign)" // Ensure sign is correctly escaped in regex
+        let pattern = "\(escapedSign)(.+?)\(escapedSign)"
         guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return }
 
         let matches = regex.matches(in: text.string, options: [], range: NSRange(location: 0, length: text.length))
 
-        for match in matches.reversed() { // Iterate in reverse to prevent index shifting
+        for match in matches.reversed() {
             let fullRange = match.range
             let textRange = match.range(at: 1)
 
-            // Apply the desired formatting
-            for (key, value) in attributes {
-                text.addAttribute(key, value: value, range: textRange)
+            // Special case: if applying bold or italic, check if the other style is already present
+            if let font = attributes[.font] as? UIFont, let boldItalicFont {
+                let currentFont = text.attribute(.font, at: textRange.location, effectiveRange: nil) as? UIFont
+                let isItalic = currentFont?.fontDescriptor.symbolicTraits.contains(.traitItalic) ?? false
+                let isBold = currentFont?.fontDescriptor.symbolicTraits.contains(.traitBold) ?? false
+
+                if (font.fontDescriptor.symbolicTraits.contains(.traitBold) && isItalic) ||
+                   (font.fontDescriptor.symbolicTraits.contains(.traitItalic) && isBold) {
+                    text.addAttribute(.font, value: boldItalicFont, range: textRange)
+                } else {
+                    text.addAttribute(.font, value: font, range: textRange)
+                }
+            } else {
+                // Apply normally
+                for (key, value) in attributes {
+                    text.addAttribute(key, value: value, range: textRange)
+                }
             }
 
             if !isEditing {
-                // Remove formatting characters (signs)
                 text.replaceCharacters(in: NSRange(location: fullRange.upperBound - sign.count, length: sign.count), with: "")
                 text.replaceCharacters(in: NSRange(location: fullRange.lowerBound, length: sign.count), with: "")
             } else {
-                // Change color of formatting characters (grayed out)
                 text.addAttribute(.foregroundColor, value: UIColor.gray, range: NSRange(location: fullRange.lowerBound, length: sign.count))
                 text.addAttribute(.foregroundColor, value: UIColor.gray, range: NSRange(location: fullRange.upperBound - sign.count, length: sign.count))
             }

+ 6 - 4
NexilisLite/NexilisLite/Source/IncomingThread.swift

@@ -262,10 +262,12 @@ class IncomingThread {
         if let packetId = message.mBodies[CoreMessage_TMessageKey.PACKET_ID] {
             _ = Nexilis.responseString(packetId: packetId, message: "00", timeout: 3000)
         }
-        UIApplication.shared.visibleViewController?.deleteAllRecordDatabase()
-        Nexilis.floatingButton.removeFromSuperview()
-        SecureUserDefaults.shared.removeValue(forKey: "me")
-        Utils.setProfile(value: false)
+        DispatchQueue.main.async {
+            UIApplication.shared.visibleViewController?.deleteAllRecordDatabase()
+            Nexilis.floatingButton.removeFromSuperview()
+            SecureUserDefaults.shared.removeValue(forKey: "me")
+            Utils.setProfile(value: false)
+        }
         ack(message: message)
     }
     

+ 22 - 11
NexilisLite/NexilisLite/Source/MasterKeyUtil.swift

@@ -281,15 +281,12 @@ class KeyManagerNexilis {
     static let keyMarkerTag = "io.nexilis.fido2.key.\(Bundle.main.infoDictionary?["CFBundleName"] as! String).marker".data(using: .utf8)!
     static let markerAccount = "nexilis.key.\(Bundle.main.infoDictionary?["CFBundleName"] as! String).marker"
     static func generateKey() {
-        let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.userPresence], nil)!
-
         let attributes: [String: Any] = [
             kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
             kSecAttrKeySizeInBits as String: 2048,
             kSecPrivateKeyAttrs as String: [
                 kSecAttrIsPermanent as String: true,
-                kSecAttrApplicationTag as String: tag,
-                kSecAttrAccessControl as String: accessControl
+                kSecAttrApplicationTag as String: tag
             ]
         ]
 
@@ -381,15 +378,29 @@ class KeyManagerNexilis {
         return [0x80 | UInt8(bytes.count)] + bytes
     }
     
-    static func getPrivateKey() -> SecKey? {
-        let context = LAContext()
-        context.localizedReason = "Verify your identity to continue with login.".localized()
+    static func getPrivateKey(useBiometric: Bool = true) -> SecKey? {
+        if useBiometric {
+            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")")
+                }
+                semaphore.signal()
+            }
 
-        var error: NSError?
-        guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
-            print("Biometric auth not available: \(String(describing: error))")
-            return nil
+            semaphore.wait()
+
+            if !result {
+                return nil
+            }
         }
+        let context = LAContext()
+        context.localizedReason = "Verify your identity to continue with login.".localized()
 
         let query: [String: Any] = [
             kSecClass as String: kSecClassKey,

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

@@ -242,13 +242,13 @@ public class Nexilis: NSObject {
                                     let message = "94:Unregistered user"
                                     delegate.onFailed(error: message)
                                     sendStateToServer(s: message)
-                                    print("checkSignInSignUp sleep 30s before retrying send to server..")
+                                    print("checkSignInSignUp sleep 30s before retrying send to server..\(response.toLogString())")
                                     Thread.sleep(forTimeInterval: 30)
                                 } else if !response.isOk() {
                                     let message = "99:Something went wrong. Invalid response, please check your connection.."
                                     delegate.onFailed(error: message)
                                     sendStateToServer(s: message)
-                                    print("checkSignInSignUp sleep 30s before retrying send to server..")
+                                    print("checkSignInSignUp sleep 30s before retrying send to server..\(response.toLogString())")
                                     Thread.sleep(forTimeInterval: 30)
                                 } else {
                                     SecureUserDefaults.shared.set(id, forKey: "device_id")
@@ -257,6 +257,8 @@ public class Nexilis: NSObject {
                                     if !enable_signup && !userId.isEmpty {
                                         enable_signup = true
                                         Utils.setProfile(value: true)
+                                        KeyManagerNexilis.deleteKey()
+                                        KeyManagerNexilis.deleteMarker()
                                     }
                                     Utils.setForceAnonymous(value: enable_signup)
                                     if(!id.isEmpty) {
@@ -2607,10 +2609,6 @@ public class Nexilis: NSObject {
     
     weak open var connectionDelegate: ConnectionDelegate?
     
-    weak open var mfaDelegate: MFADelegate?
-    
-    weak open var authDelegate: AuthenticationDelegate?
-    
     var floating: FloatingNotificationBanner!
     
     var stateUnfriend = ""

+ 138 - 6
NexilisLite/NexilisLite/Source/Utils.swift

@@ -446,6 +446,23 @@ public final class Utils {
         return dateFormatter
     }()
     
+    static func getGreetingsTimeDefaultWelcome() -> String {
+        let calendar = Calendar.current
+        let hour = calendar.component(.hour, from: Date())
+        let minute = calendar.component(.minute, from: Date())
+        var time: String
+
+        if hour < 10 || (hour == 10 && minute <= 0) {
+            time = "1"
+        } else if hour < 15 || (hour == 15 && minute <= 0) {
+            time = "2"
+        } else {
+            time = "3"
+        }
+        
+        return time
+    }
+    
     public static func previewMessageText(chat: Chat) -> Any {
         if chat.credential == "1" && chat.lock == "2" {
             return ("🚫 _"+"Message has expired".localized()+"_").richText(group_id: chat.pin)
@@ -2342,8 +2359,9 @@ public class DialogSignIn: UIViewController {
     
     @objc func ccTapped() {
         //print("ccTapped")
-        APIS.openContactCenter()
-        self.dismiss(animated: true)
+        self.dismiss(animated: true, completion: {
+            APIS.openContactCenter()
+        })
     }
     
     @objc func verifyTapped() {
@@ -2562,8 +2580,9 @@ public class DialogSecurityShield: UIViewController {
     
     @objc func ccTapped() {
         //print("ccTapped")
-        APIS.openContactCenter()
-        self.dismiss(animated: true)
+        self.dismiss(animated: true, completion: {
+            APIS.openContactCenter()
+        })
     }
     
     @objc func activateTapped() {
@@ -2675,8 +2694,9 @@ public class DialogTransactionApproval: UIViewController {
     
     @objc func ccTapped() {
         //print("ccTapped")
-        APIS.openContactCenter()
-        self.dismiss(animated: true)
+        self.dismiss(animated: true, completion: {
+            APIS.openContactCenter()
+        })
     }
     
     @objc func approveTapped() {
@@ -2809,6 +2829,118 @@ public class ValidationTransactionLimit: UIViewController, UITextFieldDelegate {
     }
 }
 
+public class DialogErrorMFA: UIViewController {
+    
+    public var errorDesc = ""
+    public var method = ""
+    var isDismiss: ((Int) -> ())?
+    
+    public override func viewDidLoad() {
+        super.viewDidLoad()
+        self.view.backgroundColor = .black.withAlphaComponent(0.5)
+        
+        let container = UIView()
+        self.view.addSubview(container)
+        container.anchor(top: self.view.topAnchor, left: self.view.leftAnchor, right: self.view.rightAnchor, paddingTop: 30, paddingLeft: 20, paddingRight: 20)
+        container.layer.cornerRadius = 20.0
+        container.clipsToBounds = true
+        container.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .white
+        
+        let title = UILabel()
+        title.text = errorDesc
+        title.font = .boldSystemFont(ofSize: 14)
+        title.numberOfLines = 0
+        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)
+        
+        let imageWarning = UIImageView(image: UIImage(named: "pb_security_warning", 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)
+        
+        let imageLogo = UIImageView(image: UIImage(named: "bjb-blue-flat", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
+        container.addSubview(imageLogo)
+        imageLogo.anchor(top: container.topAnchor, left: container.leftAnchor, paddingTop: 10, paddingLeft: 10, width: 40, height: 40)
+        
+        let imageChat = UIImageView(image: UIImage(named: "pb_startup_iconsuffix", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
+        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"
+        let contentS = UILabel()
+        contentS.tintColor = .label
+        contentS.attributedText = contentDesc.richText()
+        contentS.numberOfLines = 0
+        container.addSubview(contentS)
+        contentS.anchor(top: title.bottomAnchor, left: container.leftAnchor, right: container.rightAnchor, paddingTop: 20, paddingLeft: 15, paddingRight: 10)
+        
+        let buttonCC = UIButton(type: .custom)
+        buttonCC.setTitle("Call Center", for: .normal)
+        buttonCC.backgroundColor = .gray
+        buttonCC.titleLabel?.textColor = .white
+        buttonCC.titleLabel?.font = .boldSystemFont(ofSize: 14)
+        buttonCC.layer.cornerRadius = 17.5
+        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)
+        
+        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)
+        buttonReject.backgroundColor = .red
+        buttonReject.titleLabel?.textColor = .white
+        buttonReject.titleLabel?.font = .boldSystemFont(ofSize: 14)
+        buttonReject.layer.cornerRadius = 17.5
+        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)
+        
+        let footer = UILabel()
+        footer.text = "We value your security".localized()
+        footer.font = .systemFont(ofSize: 12)
+        footer.textColor = .gray
+        footer.numberOfLines = 0
+        container.addSubview(footer)
+        footer.anchor(top: buttonReject.bottomAnchor, bottom: container.bottomAnchor, right: container.rightAnchor, paddingBottom: 5, paddingRight: 10)
+        
+    }
+    
+    private func getContentDesc() -> String {
+        return "Saya mengalami hambatan pada waktu *\(method)*, dikarenakan *\(errorDesc)*"
+    }
+    
+    @objc func ccTapped() {
+        let contentDesc = getContentDesc()
+        self.dismiss(animated: true, completion: { [self] in
+            APIS.openContactCenterWithContext(context: "\(contentDesc)~\(method)~\(errorDesc)")
+        })
+    }
+    
+    @objc func tryAgainTapped() {
+        self.dismiss(animated: true, completion: {
+            self.isDismiss?(1)
+        })
+    }
+    
+    @objc func rejectTapped() {
+        self.dismiss(animated: true)
+        self.isDismiss?(0)
+    }
+}
+
 class LocationManager: NSObject, CLLocationManagerDelegate {
     private var locationManager = CLLocationManager()
 

+ 32 - 3
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -142,6 +142,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     var buttonSpec = UIButton(type: .custom)
     var tableViewConfigFile: UITableView!
     var specFileString = ""
+    var contextCC = ""
     
     func offset() -> CGFloat{
         guard let fontSize = Int(SecureUserDefaults.shared.value(forKey: "font_size") ?? "0") else { return 0 }
@@ -6119,7 +6120,33 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 messageText.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 5).isActive = true
                 messageText.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
                 messageText.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
-                if category_cc[0].id.contains("level0_") || dataMessages[indexPath.row]["attachment_flag"] != nil && dataMessages[indexPath.row]["attachment_flag"]  as? String ?? "" == "503" {
+                if !contextCC.isEmpty {
+                    let dataSplit = contextCC.components(separatedBy: "~")
+                    let contentErr = dataSplit[0]
+                    var activity = ""
+                    var titleErr = ""
+                    if dataSplit.count > 1 {
+                        activity = dataSplit[1]
+                    }
+                    if dataSplit.count > 2 {
+                        titleErr = dataSplit[2]
+                    }
+                    var welcome = ""
+                    let time = Utils.getGreetingsTimeDefaultWelcome()
+                    if time == "1" {
+                        welcome = "Selamat pagi"
+                    } else if time == "2" {
+                        welcome = "Selamat siang"
+                    } else {
+                        welcome = "Selamat malam"
+                    }
+                    var myName = ""
+                    let myData = User.getDataCanNil(pin: User.getMyPin())
+                    if myData != nil {
+                        myName = myData!.fullName
+                    }
+                    messageText.attributedText = "_\(welcome) Pak/Bu *\(myName)*. Kami mendeteksi anda mengalami hambatan pada waktu *\(activity)*, dikarenakan *\(titleErr)*. Kami siap membantu anda untuk mensolusikan masalah yang dihadapi. Silahkan memilih cara interaksi yang diinginkan melalui tombol di bawah ini._".richText(fontSize: 14)
+                } else if category_cc[0].id.contains("level0_") || dataMessages[indexPath.row]["attachment_flag"] != nil && dataMessages[indexPath.row]["attachment_flag"]  as? String ?? "" == "503" {
                     messageText.text = "Welcome to".localized() + " " + dataPerson["name"]!! + " " + "Contact Center".localized()
                      + "\n" + "Please choose your desired communication method...".localized()
                 } else if category_cc[0].id.contains("level1_") {
@@ -6131,8 +6158,10 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                 } else {
                     messageText.text = "Sorry, currently all our representatives are busy helping other customers. Do you want us to get back to you as soon as one of them is available?".localized()
                 }
-                messageText.font = UIFont.systemFont(ofSize: 14 + offset(), weight: .medium)
-                messageText.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
+                if contextCC.isEmpty {
+                    messageText.font = UIFont.systemFont(ofSize: 14 + offset(), weight: .medium)
+                    messageText.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
+                }
                 
 //                let date = Date()
 //                let formatter = DateFormatter()

+ 0 - 63
NexilisLite/NexilisLite/Source/View/Control/MFABiometricOnlyViewController.swift

@@ -1,63 +0,0 @@
-//
-//  MFABiometricOnlyViewController.swift
-//  Pods
-//
-//  Created by Maronakins on 01/08/25.
-//
-
-import UIKit
-import LocalAuthentication
-
-class MFAOnlyBiometricViewController: UIViewController {
-
-    override func viewDidLoad() {
-        super.viewDidLoad()
-        self.view.backgroundColor = .clear // Equivalent to setBackgroundColor(Color.TRANSPARENT)
-        dummyNextStep()
-    }
-
-    private func dummyNextStep() {
-        // This is where you would call the biometric authentication
-        biometricAuth(onSuccess: {
-            Nexilis.shared.authDelegate?.onAuthenticationSucceeded()
-            self.dismiss(animated: true, completion: nil) // Equivalent to finish()
-        }, onFailed: { error in
-            Nexilis.shared.authDelegate?.onAuthenticationFailed(error: error)
-            // Handle the error here, e.g., show a dialog
-        })
-    }
-    
-    private func biometricAuth(onSuccess: @escaping () -> Void, onFailed: @escaping (Error?) -> Void) {
-        let context = LAContext()
-        var error: NSError?
-
-        // 1. Check if the device can evaluate the biometric policy.
-        // This is equivalent to BiometricManager.canAuthenticate in Java.
-        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
-            let reason = "Confirm your identity" // This is the title of the prompt
-
-            // 2. Present the biometric authentication prompt.
-            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
-                DispatchQueue.main.async {
-                    if success {
-                        // Biometric authentication succeeded.
-                        onSuccess()
-                    } else {
-                        // Biometric authentication failed or was cancelled.
-                        onFailed(authenticationError)
-                    }
-                }
-            }
-        } else {
-            // Biometrics are not available or an error occurred.
-            // This handles cases like a user not having Touch ID/Face ID enabled.
-            onFailed(error)
-        }
-    }
-}
-
-// Protocol to handle the authentication result
-public protocol AuthenticationDelegate: AnyObject {
-    func onAuthenticationSucceeded()
-    func onAuthenticationFailed(error: Error?)
-}

+ 251 - 245
NexilisLite/NexilisLite/Source/View/Control/MFAViewController.swift

@@ -9,27 +9,17 @@ import UIKit
 import Foundation
 import os
 import LocalAuthentication
+import nuSDKService
 
 // MARK: - MFA Class
 class MFAViewController: UIViewController {
-
-    // MARK: - Constants & Variables
-    private let TAG = "MFA"
-
-    // Step definitions mirroring the Java code
     static let STEP_NEEDED_FIDO = 1
     static let STEP_NEEDED_FIDO_PWD = 2
-    static let STEP_NEEDED_FIDO_PWD_FINGER = 3
-    static let STEP_NEEDED_FINGER = 4
+    static let STEP_NEEDED_FIDO_PWD_BIOMETRIC = 3
     
-    // Properties to be set on initialization
     var STEP_NEEDED = STEP_NEEDED_FIDO_PWD
     var METHOD = ""
-    
-    private var mfaState = "open"
-    private var challengeString = ""
 
-    // MARK: - UI Components
     private let imageViewBackground = UIImageView()
     private let scrollView = UIScrollView()
     private let mainStackView = UIStackView()
@@ -39,29 +29,70 @@ class MFAViewController: UIViewController {
     private let subtitleLabel = UILabel()
     private let passwordTextField = UITextField()
     private let passwordVisibilityButton = UIButton(type: .system)
-    private let submitButton = UIButton(type: .system)
     private let poweredStackView = UIStackView()
     private let poweredLabel = UILabel()
     private let poweredImageView = UIImageView()
 
     private var isPasswordVisible = false
 
-    // MARK: - Lifecycle Methods
     override func viewDidLoad() {
         super.viewDidLoad()
+        navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(cancel(sender:)))
+        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Submit".localized(), style: .plain, target: self, action: #selector(submitAction))
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
+        tapGesture.cancelsTouchesInView = false
+        self.view.addGestureRecognizer(tapGesture)
         setupUI()
         setupLayout()
         loadData()
         updateUIBasedOnMethod()
-        updateUIBasedOnMode()
     }
-
+    
+    override func viewWillAppear(_ animated: Bool) {
+        let attributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 16.0), NSAttributedString.Key.foregroundColor: UIColor.white]
+        let navBarAppearance = UINavigationBarAppearance()
+        navBarAppearance.configureWithOpaqueBackground()
+        navBarAppearance.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : UIColor.mainColor
+        navBarAppearance.titleTextAttributes = attributes
+        navigationController?.navigationBar.standardAppearance = navBarAppearance
+        navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow),
+                                                   name: UIResponder.keyboardWillShowNotification, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide),
+                                                   name: UIResponder.keyboardWillHideNotification, object: nil)
+    }
+    
     override func viewWillDisappear(_ animated: Bool) {
-        super.viewWillDisappear(animated)
-        // Notify delegate if the view is closed prematurely
-        if mfaState == "open" {
-            Nexilis.shared.mfaDelegate?.onFailed(error: "back")
-        }
+        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
+        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
+    }
+    
+    @objc private func keyboardWillShow(notification: Notification) {
+        guard let userInfo = notification.userInfo,
+              let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
+        
+        let keyboardHeight = keyboardFrame.height
+        let bottomInset = keyboardHeight - view.safeAreaInsets.bottom
+        scrollView.contentInset.bottom = bottomInset - 20
+        scrollView.verticalScrollIndicatorInsets.bottom = bottomInset - 20
+        
+        // ✅ Scroll password field above keyboard
+        let passwordFrameInScroll = scrollView.convert(passwordTextField.frame, from: passwordTextField.superview)
+        scrollView.scrollRectToVisible(passwordFrameInScroll, animated: true)
+    }
+
+    @objc private func keyboardWillHide(notification: Notification) {
+        scrollView.contentInset = .zero
+        scrollView.verticalScrollIndicatorInsets = .zero
+    }
+    
+    @objc func cancel(sender: Any) {
+        navigationController?.dismiss(animated: true, completion: nil)
+    }
+    
+    @objc func dismissKeyboard() {
+        passwordTextField.resignFirstResponder()
     }
 
     // MARK: - UI Setup
@@ -87,24 +118,24 @@ class MFAViewController: UIViewController {
 
         // Header Images
         headerImageView1.contentMode = .scaleAspectFit
-        headerImageView1.image = UIImage(named: "pb_mfa_bjb")
+        headerImageView1.image = UIImage(named: "pb_mfa_bjb", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
         headerImageView1.heightAnchor.constraint(equalToConstant: 100).isActive = true
         mainStackView.addArrangedSubview(headerImageView1)
 
         headerImageView2.contentMode = .scaleAspectFit
-        headerImageView2.image = UIImage(named: "pb_mfa_splash")
+        headerImageView2.image = UIImage(named: "pb_mfa_splash", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
         headerImageView2.heightAnchor.constraint(equalToConstant: 200).isActive = true
         mainStackView.addArrangedSubview(headerImageView2)
 
         // Header Title Label
-        headerTitleLabel.font = UIFont(name: "Poppins-Bold", size: 17)
+        headerTitleLabel.font = .boldSystemFont(ofSize: 17)
         headerTitleLabel.textAlignment = .center
         headerTitleLabel.numberOfLines = 0
         mainStackView.addArrangedSubview(headerTitleLabel)
 
         // Subtitle Label
         subtitleLabel.text = "Please input your password to continue"
-        subtitleLabel.font = UIFont(name: "Poppins-Regular", size: 12)
+        subtitleLabel.font = .systemFont(ofSize: 12)
         subtitleLabel.textAlignment = .center
         subtitleLabel.numberOfLines = 0
         mainStackView.addArrangedSubview(subtitleLabel)
@@ -119,7 +150,7 @@ class MFAViewController: UIViewController {
         // Password Text Field
         passwordTextField.placeholder = "Type your password..."
         passwordTextField.isSecureTextEntry = true
-        passwordTextField.font = UIFont(name: "Poppins-Regular", size: 15)
+        passwordTextField.font = .systemFont(ofSize: 15)
         passwordTextField.borderStyle = .roundedRect
         passwordTextField.keyboardType = .default
         passwordTextField.autocapitalizationType = .none
@@ -131,19 +162,9 @@ class MFAViewController: UIViewController {
         passwordVisibilityButton.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
         passwordVisibilityButton.addTarget(self, action: #selector(togglePasswordVisibility), for: .touchUpInside)
         passwordVisibilityButton.translatesAutoresizingMaskIntoConstraints = false
+        passwordVisibilityButton.tintColor = .black
         passwordContainerView.addSubview(passwordVisibilityButton)
         
-        // Submit Button
-        submitButton.setTitle("SUBMIT", for: .normal)
-        submitButton.backgroundColor = UIColor(hex: "#14507f")
-        submitButton.setTitleColor(.white, for: .normal)
-        submitButton.layer.cornerRadius = 24
-        submitButton.titleLabel?.font = UIFont(name: "Poppins-Regular", size: 16)
-        submitButton.addTarget(self, action: #selector(submitAction), for: .touchUpInside)
-        submitButton.heightAnchor.constraint(equalToConstant: 48).isActive = true
-        submitButton.widthAnchor.constraint(equalToConstant: 300).isActive = true
-        mainStackView.addArrangedSubview(submitButton)
-        
         mainStackView.setCustomSpacing(12, after: subtitleLabel)
         mainStackView.setCustomSpacing(24, after: passwordContainerView)
 
@@ -168,7 +189,7 @@ class MFAViewController: UIViewController {
     private func setupLayout() {
         NSLayoutConstraint.activate([
             // Background
-            imageViewBackground.topAnchor.constraint(equalTo: view.topAnchor),
+            imageViewBackground.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
             imageViewBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor),
             imageViewBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor),
             imageViewBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor),
@@ -206,21 +227,10 @@ class MFAViewController: UIViewController {
     
     // MARK: - Data & Logic
     private func loadData() {
-        // Fetch and apply "Powered By" text from shared configuration
         let poweredText = "Nexilis"
         if !poweredText.isEmpty {
             poweredLabel.text = "Powered by \(poweredText)"
         }
-        
-        setBackground()
-    }
-    
-    private func randomizeBackground(from list: String) {
-        
-    }
-
-    private func setBackground() {
-
     }
     
     // MARK: - Actions
@@ -232,235 +242,231 @@ class MFAViewController: UIViewController {
     }
 
     @objc private func submitAction() {
-        // Check for network connectivity
-//        if Nexilis.shared.getNetworkState() != 1 {
-//            self.showToast(message: "Connection failed. Please check your network.")
-//            return
-//        }
-//        
+        if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
+            self.view.makeToast("Check your connection".localized(), duration: 2.0, position: .center)
+            return
+        }
         guard let password = passwordTextField.text, !password.trimmingCharacters(in: .whitespaces).isEmpty else {
-            self.showToast(message: "Password cannot be empty.")
+            self.view.makeToast("Password cannot be empty.".localized(), duration: 2.0, position: .center)
             return
         }
 
         guard password.count >= 6 else {
-            self.showToast(message: "Password must be at least 6 characters.")
+            self.view.makeToast("Password must be at least 6 characters.".localized(), duration: 2.0, position: .center)
             return
         }
 
-        if STEP_NEEDED == MFAViewController.STEP_NEEDED_FIDO_PWD_FINGER {
-            biometricAuth()
-        } else {
-            submit()
-        }
+        submit()
     }
 
     private func submit() {
         guard let password = passwordTextField.text else { return }
-        self.showToast(message: "Please wait...", duration: 2.0)
+        Nexilis.showLoader()
         
         DispatchQueue.global().async {
-            // 1. Encrypt password
-            let encryptedPwd = password
-            
-            // 2. Create message for the server
-            var tMessage = TMessage()
-            tMessage.mBodies[CoreMessage_TMessageKey.PSWD] = encryptedPwd
-            tMessage.mBodies[CoreMessage_TMessageKey.ACTVITY] =  self.METHOD
-            
-            // 3. Add FIDO and TOTP data to the message
-            self.getFIDO(tMessage: &tMessage)
             do {
-                let totp = try TOTPGenerator.generate(base32Secret: "JBSWY3DPEHPK3PXP", digits: 6, timeStepSeconds: 30)
-                tMessage.mBodies[CoreMessage_TMessageKey.TOTP] = totp
-            } catch {
-                os_log("Failed to generate TOTP: %{public}@", log: .default, type: .error, error.localizedDescription)
-            }
-
-            // 4. Send the message and wait for a response
-            if let response = Nexilis.writeAndWait(message: tMessage) {
-                DispatchQueue.main.async {
-//                    guard let response = response else {
-//                        self.showToast(message: "Connection failed.")
-//                        Nexilis.shared.mfaDelegate?.onFailed(error: "Connection failed.")
-//                        self.dismiss(animated: true)
-//                        return
-//                    }
-                    
+                // 1. Encrypt password
+                let encryptedPwd = password
+                
+                // 2. Create message for the server
+                let me = User.getMyPin() ?? ""
+                let tMessage = CoreMessage_TMessageBank.getMFAValidation(data: me)
+                tMessage.mBodies[CoreMessage_TMessageKey.PSWD] = encryptedPwd
+                tMessage.mBodies[CoreMessage_TMessageKey.ACTVITY] = self.METHOD
+                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 {
+                        Nexilis.hideLoader {
+                            let errorMessage = "Failed to get Private Key"
+                            let dialog = DialogErrorMFA()
+                            dialog.modalTransitionStyle = .crossDissolve
+                            dialog.modalPresentationStyle = .overCurrentContext
+                            dialog.errorDesc = errorMessage
+                            dialog.method = self.METHOD
+                            dialog.isDismiss = { res in
+                                if res == 0 {
+                                    self.navigationController?.dismiss(animated: true, completion: {
+                                        APIS.getMFACallback()?("Failed: \(errorMessage)")
+                                    })
+                                }
+                            }
+                            UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                        }
+                    }
+                    return
+                }
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
                     if response.isOk() {
-                        self.mfaState = "success"
-                        self.showToast(message: "Success!")
-                        Nexilis.shared.mfaDelegate?.onSuccess()
-                        self.dismiss(animated: true)
-                    } else {
-                        self.mfaState = "failed"
-                        let errorMessage = response.mBodies[CoreMessage_TMessageKey.MESSAGE_TEXT] ?? "An unknown error occurred."
-                        self.showToast(message: errorMessage)
-                        Nexilis.shared.mfaDelegate?.onFailed(error: errorMessage)
-                        self.dismiss(animated: true)
+                        let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
+                        if data.isEmpty {
+                            DispatchQueue.main.async {
+                                KeyManagerNexilis.deleteKey()
+                                KeyManagerNexilis.deleteMarker()
+                                Nexilis.hideLoader {
+                                    let errorMessage = "Failed to get Auth Data"
+                                    let dialog = DialogErrorMFA()
+                                    dialog.modalTransitionStyle = .crossDissolve
+                                    dialog.modalPresentationStyle = .overCurrentContext
+                                    dialog.errorDesc = errorMessage
+                                    dialog.method = self.METHOD
+                                    dialog.isDismiss = { res in
+                                        if res == 0 {
+                                            self.navigationController?.dismiss(animated: true, completion: {
+                                                APIS.getMFACallback()?("Failed: \(errorMessage)")
+                                            })
+                                        }
+                                    }
+                                    UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                                }
+                            }
+                            return
+                        }
+                        let df = HMACDeviceFingerprintNexilis.generate()
+                        tMessage.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()
+                                }
+                            }
+                            tMessage.mBodies[CoreMessage_TMessageKey.SIGNATURE] = sign
+                        } else {
+                            if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
+                                tMessage.mBodies[CoreMessage_TMessageKey.PUBLIC_KEY] = publicKey
+                            }
+                        }
+                        let secret = "JBSWY3DPEHPK3PXP" // Google Authenticator example
+                        let otp = try TOTPGenerator.generateTOTP(base32Secret: secret, digits: 6, timeStepSeconds: 30)
+                        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 {
+                                    self.biometricAuth()
+                                } else {
+                                    DispatchQueue.main.async {
+                                        Nexilis.hideLoader {
+                                            self.navigationController?.dismiss(animated: true, completion: {
+                                                UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
+                                                self.dismissKeyboard()
+                                                APIS.getMFACallback()?("Success")
+                                            })
+                                        }
+                                    }
+                                }
+                            }
+                            else {
+                                DispatchQueue.main.async {
+                                    KeyManagerNexilis.deleteKey()
+                                    KeyManagerNexilis.deleteMarker()
+                                    Nexilis.hideLoader {
+                                        let errorMessage = response.getBody(key: CoreMessage_TMessageKey.MESSAGE_TEXT)
+                                        let dialog = DialogErrorMFA()
+                                        dialog.modalTransitionStyle = .crossDissolve
+                                        dialog.modalPresentationStyle = .overCurrentContext
+                                        dialog.errorDesc = errorMessage
+                                        dialog.method = self.METHOD
+                                        dialog.isDismiss = { res in
+                                            if res == 0 {
+                                                self.navigationController?.dismiss(animated: true, completion: {
+                                                    APIS.getMFACallback()?("Failed: \(errorMessage)")
+                                                })
+                                            }
+                                        }
+                                        UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                                    }
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        KeyManagerNexilis.deleteKey()
+                        KeyManagerNexilis.deleteMarker()
+                        Nexilis.hideLoader {
+                            let errorMessage = "Failed to get Auth Data"
+                            let dialog = DialogErrorMFA()
+                            dialog.modalTransitionStyle = .crossDissolve
+                            dialog.modalPresentationStyle = .overCurrentContext
+                            dialog.errorDesc = errorMessage
+                            dialog.method = self.METHOD
+                            dialog.isDismiss = { res in
+                                if res == 0 {
+                                    self.navigationController?.dismiss(animated: true, completion: {
+                                        APIS.getMFACallback()?("Failed: \(errorMessage)")
+                                    })
+                                }
+                            }
+                            UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                        }
                     }
                 }
+            } catch {
+                
             }
         }
     }
-    
-    private func getFIDO(tMessage: inout TMessage) {
-//        do {
-//            let fingerprint = try HMACDeviceFingerprint.generate()
-//            tMessage.putBody(key: CoreMessage_TMessageKey.FINGERPRINT, value: fingerprint)
-//            
-//            if KeyManagerNexilis.hasGeneratedKey() {
-//                // If key exists, sign the challenge and fingerprint
-//                let signature = try KeyManagerNexilis.sign(data: self.challengeString + "!" + fingerprint)
-//                tMessage.putBody(key: CoreMessage_TMessageKey.SIGNATURE, value: signature)
-//            } else {
-//                // Otherwise, generate a new key and send the public part
-//                try KeyManagerNexilis.generateKey()
-//                let publicKey = try KeyManagerNexilis.getPublicKeyEncoded()
-//                tMessage.putBody(key: CoreMessage_TMessageKey.PUBLIC_KEY, value: publicKey)
-//            }
-//        } catch {
-//            os_log("FIDO operation failed: %{public}@", log: .default, type: .error, error.localizedDescription)
-//        }
-    }
 
     private func biometricAuth() {
-        let context = LAContext()
-        var error: NSError?
+        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")")
+            }
+            semaphore.signal()
+        }
 
-        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
-            let reason = "Confirm your identity to proceed"
-            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { [weak self] success, authenticationError in
-                DispatchQueue.main.async {
-                    guard let self = self else { return }
-                    if success {
-                        os_log("Biometric authentication succeeded.", log: .default, type: .info)
-                        self.showToast(message: "Please wait...", duration: 2.0)
-                        if KeyManagerNexilis.hasGeneratedKey() {
-                            self.requestAuth() // FIDO flow
-                        } else {
-                            self.submit() // Standard password submission
-                        }
-                    } else {
-                        os_log("Biometric authentication failed.", log: .default, type: .error)
-                        let errorMessage = authenticationError?.localizedDescription ?? "Authentication failed."
-                        self.showToast(message: errorMessage)
-                    }
+        semaphore.wait()
+
+        if result {
+            DispatchQueue.main.async {
+                Nexilis.hideLoader {
+                    self.navigationController?.dismiss(animated: true, completion: {
+                        UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
+                        self.dismissKeyboard()
+                        APIS.getMFACallback()?("Success")
+                    })
                 }
             }
         } else {
-            os_log("Biometrics not available. Falling back to standard submission.", log: .default, type: .info)
-            // If biometrics are not available on the device, proceed with the standard submission.
-            self.submit()
-        }
-    }
-    
-    private func requestAuth() {
-        DispatchQueue.global().async {
-            // Request a challenge from the server
-            var appname = APIS.getAppNm()
-            if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getAuthRequest(data: appname)) {
-                
-                if !response.isOk()  {
-                    self.showToast(message: "Action failed: Could not get challenge.")
-                    return
+            KeyManagerNexilis.deleteKey()
+            KeyManagerNexilis.deleteMarker()
+            DispatchQueue.main.async {
+                Nexilis.hideLoader {
+                    let errorMessage = "Gagal mendeteksi Biometric (Fingerprint/Face ID)"
+                    let dialog = DialogErrorMFA()
+                    dialog.modalTransitionStyle = .crossDissolve
+                    dialog.modalPresentationStyle = .overCurrentContext
+                    dialog.errorDesc = errorMessage
+                    dialog.method = self.METHOD
+                    dialog.isDismiss = { res in
+                        if res == 0 {
+                            self.navigationController?.dismiss(animated: true, completion: {
+                                APIS.getMFACallback()?("Failed: \(errorMessage)")
+                            })
+                        }
+                    }
+                    UIApplication.shared.visibleViewController?.present(dialog, animated: true)
                 }
-                
-                // On success, store the challenge and then proceed with the final submission
-                self.challengeString = response.getBody(key: CoreMessage_TMessageKey.DATA) ?? ""
-                self.submit()
             }
         }
     }
-
-    // MARK: - UI Updates
-    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
-        super.traitCollectionDidChange(previousTraitCollection)
-        if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
-            updateUIBasedOnMode()
-            setBackground()
-        }
-    }
-    
-    private func updateUIBasedOnMode() {
-        let isDarkMode = traitCollection.userInterfaceStyle == .dark
-        let textColor: UIColor = isDarkMode ? .white : .black
-        let placeholderColor: UIColor = .gray
-        
-        passwordTextField.textColor = textColor
-        passwordTextField.attributedPlaceholder = NSAttributedString(
-            string: "Type your password...",
-            attributes: [.foregroundColor: placeholderColor]
-        )
-        passwordVisibilityButton.tintColor = textColor
-        headerTitleLabel.textColor = textColor
-        subtitleLabel.textColor = isDarkMode ? .lightGray : .darkGray
-        poweredLabel.textColor = isDarkMode ? .lightGray : .darkGray
-        submitButton.backgroundColor = isDarkMode ? UIColor(hex: "#2E77AE") : UIColor(hex: "#14507f")
-    }
     
     private func updateUIBasedOnMethod() {
-        if METHOD.contains("Sign Up") {
-            headerTitleLabel.text = "Register"
-        } else {
-            headerTitleLabel.text = "Authenticate Your Account"
-        }
-    }
-}
-
-// MARK: - Helper Extensions & Mocks
-
-// Helper for Hex Color
-extension UIColor {
-    convenience init(hex: String) {
-        var cString: String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
-        if cString.hasPrefix("#") { cString.removeFirst() }
-        guard cString.count == 6 else { self.init(white: 0.6, alpha: 1.0); return }
-        
-        var rgbValue: UInt64 = 0
-        Scanner(string: cString).scanHexInt64(&rgbValue)
-        
-        self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
-                  green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
-                  blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
-                  alpha: 1.0)
+        headerTitleLabel.text = METHOD
     }
 }
 
-// Helper to show Android-style Toast messages
-extension UIViewController {
-    func showToast(message: String, duration: Double = 3.0) {
-        let toastLabel = UILabel()
-        toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.6)
-        toastLabel.textColor = .white
-        toastLabel.font = .systemFont(ofSize: 14.0)
-        toastLabel.textAlignment = .center
-        toastLabel.text = message
-        toastLabel.alpha = 1.0
-        toastLabel.layer.cornerRadius = 10
-        toastLabel.clipsToBounds = true
-        
-        self.view.addSubview(toastLabel)
-        
-        toastLabel.translatesAutoresizingMaskIntoConstraints = false
-        NSLayoutConstraint.activate([
-            toastLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
-            toastLabel.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -50),
-            toastLabel.widthAnchor.constraint(lessThanOrEqualTo: self.view.widthAnchor, constant: -40),
-            toastLabel.heightAnchor.constraint(equalToConstant: 40)
-        ])
-        
-        UIView.animate(withDuration: 0.5, delay: duration, options: .curveEaseOut, animations: {
-            toastLabel.alpha = 0.0
-        }, completion: { _ in
-            toastLabel.removeFromSuperview()
-        })
-    }
-}
-
-// Delegate protocol for callbacks
-public protocol MFADelegate: AnyObject {
-    func onSuccess()
-    func onFailed(error: String)
-}
-

+ 65 - 79
NexilisLite/NexilisLite/Source/View/Control/TOTPGenerator.swift

@@ -1,103 +1,89 @@
-//
-//  TOTPGenerator.swift
-//  Pods
-//
-//  Created by Maronakins on 01/08/25.
-//
-
-
 import Foundation
-import CryptoKit
-
-/// A utility for generating Time-based One-Time Passwords (TOTP).
-public struct TOTPGenerator {
+import CommonCrypto
 
-    /// Generates a Time-based One-Time Password (TOTP) using the given parameters.
-    ///
-    /// This implementation follows the standards defined in RFC 4226 (HOTP) and RFC 6238 (TOTP).
-    ///
-    /// - Parameters:
-    ///   - base32Secret: The shared secret key, encoded in Base32 string format.
-    ///   - digits: The number of digits the OTP should have (typically 6 or 8).
-    ///   - timeStepSeconds: The time interval in seconds for which an OTP is valid (typically 30 or 60).
-    /// - Returns: A formatted OTP string with the specified number of digits, or `nil` if the Base32 secret is invalid.
-    public static func generate(base32Secret: String, digits: Int, timeStepSeconds: Int) -> String? {
-        // 1. Decode the Base32 secret into raw bytes.
-        guard let secretData = base32Decode(base32Secret) else {
-            print("Error: Invalid Base32 secret string.")
-            return nil
+class TOTPGenerator {
+    
+    static func generateTOTP(base32Secret: String, digits: Int, timeStepSeconds: Int) throws -> String {
+        let secret = base32Decode(base32Secret)
+        var timeStep = UInt64(Date().timeIntervalSince1970) / UInt64(timeStepSeconds)
+        var data = [UInt8](repeating: 0, count: 8)
+        
+        // convert timeStep to byte[8]
+        for i in stride(from: 7, through: 0, by: -1) {
+            data[i] = UInt8(timeStep & 0xFF)
+            timeStep >>= 8
         }
-
-        // 2. Calculate the current time step (counter).
-        // This is the number of `timeStepSeconds` intervals that have passed since the Unix epoch.
-        let timeStep = UInt64(Date().timeIntervalSince1970) / UInt64(timeStepSeconds)
-
-        // 3. Convert the time step to an 8-byte, big-endian data representation.
-        let timeStepData = withUnsafeBytes(of: timeStep.bigEndian) { Data($0) }
         
-        // 4. Compute the HMAC-SHA1 hash.
-        let key = SymmetricKey(data: secretData)
-        let hmac = HMAC<Insecure.SHA1>.authenticationCode(for: timeStepData, using: key)
-
-        // 5. Perform dynamic truncation to get a 4-byte value.
-        // Convert HMAC to an array of bytes to work with.
-        let hashBytes = Array(hmac)
+        let hash = try hmacSha1(key: secret, data: data)
         
-        // The last 4 bits of the hash determine the offset.
-        let offset = Int(hashBytes.last! & 0x0F)
+        // dynamic truncation
+        let offset = Int(hash[hash.count - 1] & 0x0F)
+        let binary =
+            ((Int(hash[offset]) & 0x7f) << 24) |
+            ((Int(hash[offset + 1]) & 0xff) << 16) |
+            ((Int(hash[offset + 2]) & 0xff) << 8) |
+            (Int(hash[offset + 3]) & 0xff)
         
-        // Extract 4 bytes from the hash at the calculated offset.
-        let truncatedHash =
-            (UInt32(hashBytes[offset]     & 0x7F) << 24) |
-            (UInt32(hashBytes[offset + 1] & 0xFF) << 16) |
-            (UInt32(hashBytes[offset + 2] & 0xFF) << 8)  |
-            (UInt32(hashBytes[offset + 3] & 0xFF))
-
-        // 6. Generate the final OTP value.
-        // Calculate the divisor (10^digits).
-        let divisor = NSDecimalNumber(decimal: pow(10, digits)).uint32Value
+        let otp = binary % Int(pow(10.0, Double(digits)))
+        return String(format: "%0\(digits)d", otp)
+    }
+    
+    static func generateHOTP(base32Secret: String, digits: Int, counter: UInt64) throws -> String {
+        let secret = base32Decode(base32Secret)
+        var counterValue = counter
+        var data = [UInt8](repeating: 0, count: 8)
         
-        // The OTP is the remainder of the division.
-        let otp = truncatedHash % divisor
-
-        // 7. Format the OTP string, padding with leading zeros if necessary.
+        // convert counter to byte[8]
+        for i in stride(from: 7, through: 0, by: -1) {
+            data[i] = UInt8(counterValue & 0xFF)
+            counterValue >>= 8
+        }
+        
+        let hash = try hmacSha1(key: secret, data: data)
+        
+        // dynamic truncation
+        let offset = Int(hash[hash.count - 1] & 0x0F)
+        let binary =
+            ((Int(hash[offset]) & 0x7f) << 24) |
+            ((Int(hash[offset + 1]) & 0xff) << 16) |
+            ((Int(hash[offset + 2]) & 0xff) << 8) |
+            (Int(hash[offset + 3]) & 0xff)
+        
+        let otp = binary % Int(pow(10.0, Double(digits)))
         return String(format: "%0\(digits)d", otp)
     }
-
-    /// Decodes a Base32 encoded string into raw data bytes.
-    ///
-    /// This minimal decoder adheres to RFC 4648 and ignores padding characters.
-    ///
-    /// - Parameter base32: The Base32 encoded string.
-    /// - Returns: A `Data` object containing the decoded bytes, or `nil` if the input is malformed.
-    private static func base32Decode(_ base32: String) -> Data? {
+    
+    // MARK: - Base32 Decoder
+    private static func base32Decode(_ base32: String) -> [UInt8] {
         let base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
-        let base32CharsMap = Dictionary(uniqueKeysWithValues: base32Chars.enumerated().map { ($1, $0) })
-        
-        // Sanitize the input: convert to uppercase and remove any padding.
-        let sanitizedBase32 = base32.uppercased().replacingOccurrences(of: "=", with: "")
+        let clean = base32.replacingOccurrences(of: "=", with: "").uppercased()
         
-        var data = Data()
+        var bytes = [UInt8]()
         var buffer = 0
         var bitsLeft = 0
         
-        for char in sanitizedBase32 {
-            guard let value = base32CharsMap[char] else {
-                // Invalid character found in the Base32 string.
-                return nil
-            }
+        for char in clean {
+            guard let val = base32Chars.firstIndex(of: char) else { continue }
+            let idx = base32Chars.distance(from: base32Chars.startIndex, to: val)
             
             buffer <<= 5
-            buffer |= value
+            buffer |= idx
             bitsLeft += 5
             
             if bitsLeft >= 8 {
-                let byte = (buffer >> (bitsLeft - 8)) & 0xFF
-                data.append(UInt8(byte))
+                let byte = UInt8((buffer >> (bitsLeft - 8)) & 0xFF)
+                bytes.append(byte)
                 bitsLeft -= 8
             }
         }
         
-        return data
+        return bytes
+    }
+    
+    // MARK: - HMAC-SHA1
+    private static func hmacSha1(key: [UInt8], data: [UInt8]) throws -> [UInt8] {
+        var macData = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
+        CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), key, key.count, data, data.count, &macData)
+        return macData
     }
-}
+}