Explorar o código

fix bugs and release for 5.0.64

alqindiirsyam hai 1 semana
pai
achega
702350d905
Modificáronse 25 ficheiros con 945 adicións e 217 borrados
  1. 4 4
      AppBuilder/AppBuilder.xcodeproj/project.pbxproj
  2. 21 0
      NexilisLite/NexilisLite/Resource/Assets.xcassets/bank-transfer_white.imageset/Contents.json
  3. BIN=BIN
      NexilisLite/NexilisLite/Resource/Assets.xcassets/bank-transfer_white.imageset/bank-transfer_white.png
  4. 21 0
      NexilisLite/NexilisLite/Resource/Assets.xcassets/discount_white.imageset/Contents.json
  5. BIN=BIN
      NexilisLite/NexilisLite/Resource/Assets.xcassets/discount_white.imageset/discount_white.png
  6. 21 0
      NexilisLite/NexilisLite/Resource/Assets.xcassets/promo-code_white.imageset/Contents.json
  7. BIN=BIN
      NexilisLite/NexilisLite/Resource/Assets.xcassets/promo-code_white.imageset/promo-code_white.png
  8. 21 0
      NexilisLite/NexilisLite/Resource/Assets.xcassets/qris_logo.imageset/Contents.json
  9. BIN=BIN
      NexilisLite/NexilisLite/Resource/Assets.xcassets/qris_logo.imageset/qris_logo.png
  10. 21 0
      NexilisLite/NexilisLite/Resource/Assets.xcassets/qris_logo_white.imageset/Contents.json
  11. BIN=BIN
      NexilisLite/NexilisLite/Resource/Assets.xcassets/qris_logo_white.imageset/qris_logo_white.png
  12. 3 0
      NexilisLite/NexilisLite/Resource/id.lproj/Localizable.strings
  13. 11 4
      NexilisLite/NexilisLite/Source/APIS.swift
  14. 8 2
      NexilisLite/NexilisLite/Source/CoreMessage_TMessageBank.swift
  15. 23 19
      NexilisLite/NexilisLite/Source/Nexilis.swift
  16. 286 0
      NexilisLite/NexilisLite/Source/Utils.swift
  17. 9 0
      NexilisLite/NexilisLite/Source/View/BNIView/BNIBookingWebView.swift
  18. 246 7
      NexilisLite/NexilisLite/Source/View/Chat/ChatWALikeVC.swift
  19. 4 4
      NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift
  20. 4 4
      NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift
  21. 6 1
      NexilisLite/NexilisLite/Source/View/Chat/MessageInfo.swift
  22. 73 63
      NexilisLite/NexilisLite/Source/View/Control/ChangeDeviceViewController.swift
  23. 2 2
      NexilisLite/NexilisLite/Source/View/Control/MFAViewController.swift
  24. 122 107
      NexilisLite/NexilisLite/Source/View/Control/SignUpSignIn.swift
  25. 39 0
      NexilisLite/NexilisLite/Source/View/Control/TOTPGenerator.swift

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

@@ -568,7 +568,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.63;
+				MARKETING_VERSION = 5.0.64;
 				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.63;
+				MARKETING_VERSION = 5.0.64;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -640,7 +640,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.63;
+				MARKETING_VERSION = 5.0.64;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -679,7 +679,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.63;
+				MARKETING_VERSION = 5.0.64;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 21 - 0
NexilisLite/NexilisLite/Resource/Assets.xcassets/bank-transfer_white.imageset/Contents.json

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

BIN=BIN
NexilisLite/NexilisLite/Resource/Assets.xcassets/bank-transfer_white.imageset/bank-transfer_white.png


+ 21 - 0
NexilisLite/NexilisLite/Resource/Assets.xcassets/discount_white.imageset/Contents.json

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

BIN=BIN
NexilisLite/NexilisLite/Resource/Assets.xcassets/discount_white.imageset/discount_white.png


+ 21 - 0
NexilisLite/NexilisLite/Resource/Assets.xcassets/promo-code_white.imageset/Contents.json

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

BIN=BIN
NexilisLite/NexilisLite/Resource/Assets.xcassets/promo-code_white.imageset/promo-code_white.png


+ 21 - 0
NexilisLite/NexilisLite/Resource/Assets.xcassets/qris_logo.imageset/Contents.json

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

BIN=BIN
NexilisLite/NexilisLite/Resource/Assets.xcassets/qris_logo.imageset/qris_logo.png


+ 21 - 0
NexilisLite/NexilisLite/Resource/Assets.xcassets/qris_logo_white.imageset/Contents.json

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

BIN=BIN
NexilisLite/NexilisLite/Resource/Assets.xcassets/qris_logo_white.imageset/qris_logo_white.png


+ 3 - 0
NexilisLite/NexilisLite/Resource/id.lproj/Localizable.strings

@@ -86,6 +86,7 @@
 "Version" = "Versi";
 "Powered by Qmera" = "Dipersembahkan oleh Qmera";
 "Powered by Nexilis" = "Dipersembahkan oleh Nexilis";
+"Powered by" = "Dipersembahkan oleh";
 "Sign-Up (Change profile)" = "Daftar (Ubah Profil)";
 "Set Profile" = "Atur Profil";
 "Sign-In (Change Device)" = "Masuk (Ubah Perangkat)";
@@ -102,6 +103,7 @@
 "No call center history" = "Tidak ada riwayat pusat panggilan";
 "Username has been registered, please use another name" = "Nama yang kamu masukkan sudah terdaftar, silahkan gunakan nama lain";
 "Scan Gaspol QR" = "Memindai Gaspol QR";
+"Scan" = "Pindai";
 "Scan QR Code" = "Memindai kode QR";
 "To use Gaspol Web, go to gaspol.co.id on your computer." = "Untuk dapat menggunakan Gaspol Web, pergi ke halaman gaspol.co.id pada komputer kamu.";
 "Select" = "Pilih";
@@ -421,3 +423,4 @@
 "Later" = "Nanti";
 "Restart Required" = "Perlu Memulai Ulang Aplikasi";
 "Oops! Something went wrong. Please restart the app to continue." = "Ups! Terjadi kesalahan. Silakan mulai ulang aplikasi untuk melanjutkan.";
+"Show Code" = "Tunjukkan Kode";

+ 11 - 4
NexilisLite/NexilisLite/Source/APIS.swift

@@ -839,8 +839,8 @@ public class APIS: NSObject {
                                     message.mBodies[CoreMessage_TMessageKey.PUBLIC_KEY] = publicKey
                                 }
                             }
-                            let secret = "JBSWY3DPEHPK3PXP" // Google Authenticator example
-                            let otp = try TOTPGenerator.generateTOTP(base32Secret: secret, digits: 6, timeStepSeconds: 300)
+//                            let secret = "JBSWY3DPEHPK3PXP" // Google Authenticator example
+                            let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 30)
                             message.mBodies[CoreMessage_TMessageKey.TOTP] = otp
                             if let response = Nexilis.writeAndWait(message: message) {
                                 if response.isOk() {
@@ -3038,8 +3038,15 @@ public class APIS: NSObject {
         return nameGroupShared
     }
     
-    
-    
+    public static func openQris() {
+        let scannerVC = QRScannerViewController()
+        scannerVC.modalPresentationStyle = .fullScreen
+        if UIApplication.shared.visibleViewController?.navigationController != nil {
+            UIApplication.shared.visibleViewController?.navigationController?.present(scannerVC, animated: true, completion: nil)
+        } else {
+            UIApplication.shared.visibleViewController?.present(scannerVC, animated: true, completion: nil)
+        }
+    }
 }
 
 extension UINavigationController {

+ 8 - 2
NexilisLite/NexilisLite/Source/CoreMessage_TMessageBank.swift

@@ -1443,7 +1443,7 @@ public class CoreMessage_TMessageBank {
         return tmessage
     }
 
-    public static func getSendVerifyChangeDevice(p_email: String, p_vercode: String, number: String = "", deviceFingerprint: String = "", publicKey: String = "", signature: String = "") -> TMessage {
+    public static func getSendVerifyChangeDevice(p_email: String, p_vercode: String, number: String = "", deviceFingerprint: String = "", publicKey: String = "", signature: String = "", totp: String = "") -> TMessage {
         let me = User.getMyPin()!
         let tmessage = TMessage()
         tmessage.mCode = CoreMessage_TMessageCode.SEND_VERIFY_LOGIN
@@ -1465,6 +1465,9 @@ public class CoreMessage_TMessageBank {
         if !signature.isEmpty {
             tmessage.mBodies[CoreMessage_TMessageKey.SIGNATURE] = signature
         }
+        if !totp.isEmpty {
+            tmessage.mBodies[CoreMessage_TMessageKey.TOTP] = totp
+        }
         return tmessage;
     }
     
@@ -2327,7 +2330,7 @@ public class CoreMessage_TMessageBank {
         return tmessage
     }
     
-    public static func getSignUpSignInAPI(p_name: String, p_password: String, deviceFingerprint: String = "", publicKey: String = "", signature: String = "") -> TMessage {
+    public static func getSignUpSignInAPI(p_name: String, p_password: String, deviceFingerprint: String = "", publicKey: String = "", signature: String = "", totp: String = "") -> TMessage {
         let tmessage = TMessage()
         tmessage.mCode = CoreMessage_TMessageCode.SIGN_UP_AND_SIGN_IN_API
         tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
@@ -2347,6 +2350,9 @@ public class CoreMessage_TMessageBank {
         if !signature.isEmpty {
             tmessage.mBodies[CoreMessage_TMessageKey.SIGNATURE] = signature
         }
+        if !totp.isEmpty {
+            tmessage.mBodies[CoreMessage_TMessageKey.TOTP] = totp
+        }
         return tmessage
     }
     

+ 23 - 19
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.63"
+    public static var cpaasVersion = "5.0.64"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""
@@ -1680,6 +1680,7 @@ public class Nexilis: NSObject {
                         if scope == MessageScope.GROUP {
                             for pin in getGroupMembers(fmdb: fmdb, l_pin: l_pin) {
                                 if f_pin == pin { continue }
+//                                print("REPLACE STATUS MESSAGE: \(message_id) <><> \(status) <><> \(pin)")
                                 _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                     "message_id" : message_id,
                                     "status" : status,
@@ -2204,6 +2205,8 @@ public class Nexilis: NSObject {
         let latitude = message.getBody(key : CoreMessage_TMessageKey.LATITUDE, default_value : "")
         let longitude = message.getBody(key : CoreMessage_TMessageKey.LONGITUDE, default_value : "")
         let desc = message.getBody(key : CoreMessage_TMessageKey.DESCRIPTION, default_value : "")
+        let time = message.getBody(key : CoreMessage_TMessageKey.SERVER_DATE, default_value : String(Date().currentTimeMillis()))
+        let lastUpdate = message.getBody(key : CoreMessage_TMessageKey.LAST_UPDATE, default_value : String(Date().currentTimeMillis()))
         guard !status.isEmpty else {
             return
         }
@@ -2211,6 +2214,7 @@ public class Nexilis: NSObject {
         guard !l_pin.isEmpty else {
             return
         }
+//        print("COMING STATUS: \(message_id) <><> \(status) <><> \(l_pin) <><> \(time) <><> \(lastUpdate)")
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
                 if message_id.starts(with: "-1") || message_id.starts(with: "-2") {
@@ -2228,31 +2232,31 @@ public class Nexilis: NSObject {
                                         "longitude" : longitude,
                                         "latitude" : latitude,
                                         "location" : desc,
-                                        "time_delivered" : String(Date().currentTimeMillis()),
-                                        "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(t)' and f_pin = '\(l_pin)'")
+                                        "time_delivered" : time,
+                                        "last_update" : lastUpdate], _where: "message_id = '\(t)' and f_pin = '\(l_pin)'")
                                 } else if status == "4" {
                                     _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                         "status" : status,
-                                        "time_read" : String(Date().currentTimeMillis()),
+                                        "time_read" : time,
                                         "longitude" : longitude,
                                         "latitude" : latitude,
                                         "location" : desc,
-                                        "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(t)' and f_pin = '\(l_pin)'")
+                                        "last_update" : lastUpdate], _where: "message_id = '\(t)' and f_pin = '\(l_pin)'")
                                 } else if status == "8" {
                                     _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                         "status" : status,
-                                        "time_ack" : String(Date().currentTimeMillis()),
+                                        "time_ack" : time,
                                         "longitude" : longitude,
                                         "latitude" : latitude,
                                         "location" : desc,
-                                        "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(t)' and f_pin = '\(l_pin)'")
+                                        "last_update" : lastUpdate], _where: "message_id = '\(t)' and f_pin = '\(l_pin)'")
                                 } else {
                                     _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                         "status" : status,
                                         "longitude" : longitude,
                                         "latitude" : latitude,
                                         "location" : desc,
-                                        "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
+                                        "last_update" : lastUpdate], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
                                 }
                             }
                             cursorStatus.close()
@@ -2261,11 +2265,11 @@ public class Nexilis: NSObject {
                                 if Int(status)! == 2 {
                                     _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                         "status" : status,
-                                        "time_ack" : String(Date().currentTimeMillis()),
+                                        "time_ack" : time,
                                         "longitude" : longitude,
                                         "latitude" : latitude,
                                         "location" : desc,
-                                        "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(t)'")
+                                        "last_update" : lastUpdate], _where: "message_id = '\(t)'")
                                 }
                                 cursorStatus.close()
                             }
@@ -2278,34 +2282,34 @@ public class Nexilis: NSObject {
                             if status == "3" {
                                 _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                     "status" : status,
-                                    "time_delivered" : String(Date().currentTimeMillis()),
+                                    "time_delivered" : time,
                                     "longitude" : longitude,
                                     "latitude" : latitude,
                                     "location" : desc,
-                                    "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
+                                    "last_update" : lastUpdate], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
                             } else if status == "4" {
                                 _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                     "status" : status,
-                                    "time_read" : String(Date().currentTimeMillis()),
+                                    "time_read" : time,
                                     "longitude" : longitude,
                                     "latitude" : latitude,
                                     "location" : desc,
-                                    "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
+                                    "last_update" : lastUpdate], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
                             } else if status == "8" {
                                 _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                     "status" : status,
-                                    "time_ack" : String(Date().currentTimeMillis()),
+                                    "time_ack" : time,
                                     "longitude" : longitude,
                                     "latitude" : latitude,
                                     "location" : desc,
-                                    "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
+                                    "last_update" : lastUpdate], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
                             } else {
                                 _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                     "status" : status,
                                     "longitude" : longitude,
                                     "latitude" : latitude,
                                     "location" : desc,
-                                    "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
+                                    "last_update" : lastUpdate], _where: "message_id = '\(message_id)' and f_pin = '\(l_pin)'")
                             }
                         }
                         cursorStatus.close()
@@ -2313,11 +2317,11 @@ public class Nexilis: NSObject {
                         if Int(status)! == 2 {
                             _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
                                 "status" : status,
-                                "time_ack" : String(Date().currentTimeMillis()),
+                                "time_ack" : time,
                                 "longitude" : longitude,
                                 "latitude" : latitude,
                                 "location" : desc,
-                                "last_update" : String(Date().currentTimeMillis())], _where: "message_id = '\(message_id)'")
+                                "last_update" : lastUpdate], _where: "message_id = '\(message_id)'")
                         }
                         cursorStatus.close()
                     }

+ 286 - 0
NexilisLite/NexilisLite/Source/Utils.swift

@@ -4135,3 +4135,289 @@ public final class MessageGuardLite {
         return input.range(of: pattern, options: .regularExpression) != nil
     }
 }
+
+class QRScannerViewController: UIViewController {
+    
+    private var captureSession: AVCaptureSession!
+    private var previewLayer: AVCaptureVideoPreviewLayer!
+    
+    private let scanAreaSize: CGFloat = 280
+    
+    // Overlay
+    private let overlayView = UIView()
+    
+    private let backButton: UIButton = {
+        let button = UIButton(type: .system)
+        button.setImage(UIImage(systemName: "chevron.left"), for: .normal)
+        button.setTitle(" " + "Scan".localized(), for: .normal)
+        button.tintColor = .white
+        button.setTitleColor(.white, for: .normal)
+        button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
+        button.contentHorizontalAlignment = .leading
+        return button
+    }()
+    
+    private let showCodeButton: UIButton = {
+        let button = UIButton(type: .system)
+        button.setImage(UIImage(named: "promo-code_white", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.resized(to: CGSize(width: 20, height: 20)), for: .normal)
+        button.tintColor = .white
+        button.setTitle(" " + "Show Code".localized(), for: .normal)
+        button.setTitleColor(.white, for: .normal)
+        button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
+        button.contentHorizontalAlignment = .center
+        button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
+        button.layer.cornerRadius = 25
+        return button
+    }()
+    
+    private let promoButton: UIButton = {
+        let button = UIButton(type: .system)
+        button.setImage(UIImage(named: "discount_white", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.resized(to: CGSize(width: 20, height: 20)), for: .normal)
+        button.tintColor = .white
+        button.setTitle(" " + "Promo".localized(), for: .normal)
+        button.setTitleColor(.white, for: .normal)
+        button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium)
+        button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
+        button.layer.cornerRadius = 22
+        return button
+    }()
+    
+    private let transferButton: UIButton = {
+        let button = UIButton(type: .system)
+        button.setImage(UIImage(named: "bank-transfer_white", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.resized(to: CGSize(width: 20, height: 20)), for: .normal)
+        button.tintColor = .white
+        button.setTitle(" " + "Transfer".localized(), for: .normal)
+        button.setTitleColor(.white, for: .normal)
+        button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
+        button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
+        button.layer.cornerRadius = 25
+        return button
+    }()
+    
+    private let labelPoweredBy: UILabel = {
+        let label = UILabel()
+        label.text = "Powered by".localized()
+        label.font = UIFont.boldSystemFont(ofSize: 22)
+        label.textColor = .white
+        return label
+    }()
+    
+    private let qrisLogo: UIImageView = {
+        let imageView = UIImageView()
+        imageView.image = UIImage(named: "qris_logo_white", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!
+        imageView.contentMode = .scaleAspectFit
+        return imageView
+    }()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        view.backgroundColor = .black
+        setupCamera()
+        setupOverlay()
+        setupUI()
+    }
+    
+    private func setupCamera() {
+        captureSession = AVCaptureSession()
+        
+        guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
+              let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice)
+        else { return }
+        
+        if captureSession.canAddInput(videoInput) { captureSession.addInput(videoInput) }
+        
+        let metadataOutput = AVCaptureMetadataOutput()
+        if captureSession.canAddOutput(metadataOutput) {
+            captureSession.addOutput(metadataOutput)
+            metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
+            metadataOutput.metadataObjectTypes = [.qr]
+        }
+        
+        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
+        previewLayer.videoGravity = .resizeAspectFill
+        previewLayer.frame = view.bounds
+        view.layer.addSublayer(previewLayer)
+        
+        captureSession.startRunning()
+    }
+    
+    private func setupOverlay() {
+        overlayView.frame = view.bounds
+        overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
+        
+        // Create mask with transparent hole
+        let path = UIBezierPath(rect: overlayView.bounds)
+        let cutoutRect = CGRect(
+            x: (view.frame.width - scanAreaSize) / 2,
+            y: (view.frame.height - scanAreaSize) / 2,
+            width: scanAreaSize,
+            height: scanAreaSize
+        )
+        let cutoutPath = UIBezierPath(roundedRect: cutoutRect, cornerRadius: 8)
+        path.append(cutoutPath.reversing())
+        
+        let maskLayer = CAShapeLayer()
+        maskLayer.path = path.cgPath
+        overlayView.layer.mask = maskLayer
+        
+        view.addSubview(overlayView)
+        
+        // Add orange corners
+        addCornerIndicators(to: overlayView, rect: cutoutRect)
+        
+        // Combine label + logo
+        let poweredStack = UIStackView(arrangedSubviews: [labelPoweredBy, qrisLogo])
+        poweredStack.axis = .horizontal
+        poweredStack.alignment = .center
+        poweredStack.spacing = 6
+
+        view.addSubview(poweredStack)
+        poweredStack.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            poweredStack.topAnchor.constraint(equalTo: overlayView.topAnchor, constant: cutoutRect.maxY + 16),
+            poweredStack.centerXAnchor.constraint(equalTo: view.centerXAnchor)
+        ])
+        
+        qrisLogo.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            qrisLogo.widthAnchor.constraint(equalToConstant: 80),
+            qrisLogo.heightAnchor.constraint(equalToConstant: 80)
+        ])
+    }
+    
+    private func addCornerIndicators(to view: UIView, rect: CGRect) {
+        let lineLength: CGFloat = 30
+        let lineWidth: CGFloat = 4
+        let color = UIColor.mainColor.cgColor
+        
+        func addLine(from: CGPoint, to: CGPoint) {
+            let line = CAShapeLayer()
+            let path = UIBezierPath()
+            path.move(to: from)
+            path.addLine(to: to)
+            line.path = path.cgPath
+            line.strokeColor = color
+            line.lineWidth = lineWidth
+            view.layer.addSublayer(line)
+        }
+        
+        // Top-left
+        addLine(from: rect.origin, to: CGPoint(x: rect.minX + lineLength, y: rect.minY))
+        addLine(from: rect.origin, to: CGPoint(x: rect.minX, y: rect.minY + lineLength))
+        
+        // Top-right
+        addLine(from: CGPoint(x: rect.maxX, y: rect.minY),
+                to: CGPoint(x: rect.maxX - lineLength, y: rect.minY))
+        addLine(from: CGPoint(x: rect.maxX, y: rect.minY),
+                to: CGPoint(x: rect.maxX, y: rect.minY + lineLength))
+        
+        // Bottom-left
+        addLine(from: CGPoint(x: rect.minX, y: rect.maxY),
+                to: CGPoint(x: rect.minX + lineLength, y: rect.maxY))
+        addLine(from: CGPoint(x: rect.minX, y: rect.maxY),
+                to: CGPoint(x: rect.minX, y: rect.maxY - lineLength))
+        
+        // Bottom-right
+        addLine(from: CGPoint(x: rect.maxX, y: rect.maxY),
+                to: CGPoint(x: rect.maxX - lineLength, y: rect.maxY))
+        addLine(from: CGPoint(x: rect.maxX, y: rect.maxY),
+                to: CGPoint(x: rect.maxX, y: rect.maxY - lineLength))
+    }
+    
+    private func setupUI() {
+        // Back button
+        view.addSubview(backButton)
+        backButton.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            backButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
+            backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8)
+        ])
+        backButton.addTarget(self, action: #selector(didTapBack), for: .touchUpInside)
+        
+        // Bottom buttons
+        let stack = UIStackView(arrangedSubviews: [showCodeButton, promoButton, transferButton])
+        stack.axis = .horizontal
+        stack.spacing = 20
+        stack.alignment = .center
+        view.addSubview(stack)
+        
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            stack.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30),
+            stack.centerXAnchor.constraint(equalTo: view.centerXAnchor)
+        ])
+        
+        showCodeButton.translatesAutoresizingMaskIntoConstraints = false
+        transferButton.translatesAutoresizingMaskIntoConstraints = false
+        promoButton.translatesAutoresizingMaskIntoConstraints = false
+        
+        showCodeButton.addTarget(self, action: #selector(didTapShowCode), for: .touchUpInside)
+        transferButton.addTarget(self, action: #selector(didTapTransfer), for: .touchUpInside)
+        promoButton.addTarget(self, action: #selector(didTapPromo), for: .touchUpInside)
+        
+        NSLayoutConstraint.activate([
+            showCodeButton.widthAnchor.constraint(equalToConstant: 120),
+            showCodeButton.heightAnchor.constraint(equalToConstant: 50),
+            transferButton.widthAnchor.constraint(equalToConstant: 100),
+            transferButton.heightAnchor.constraint(equalToConstant: 50),
+            promoButton.widthAnchor.constraint(equalToConstant: 80),
+            promoButton.heightAnchor.constraint(equalToConstant: 50)
+        ])
+    }
+    
+    @objc private func didTapBack() {
+        captureSession.stopRunning()
+        dismiss(animated: true, completion: nil)
+    }
+    
+    @objc private func didTapShowCode() {
+        showWebview(url: Utils.getURLBase() + "nexilis/pages/read-qr?qr=")
+    }
+    
+    @objc private func didTapTransfer() {
+        showWebview(url: Utils.getURLBase() + "nexilis/pages/read-qr?qr=")
+    }
+    
+    @objc private func didTapPromo() {
+        showWebview(url: Utils.getURLBase() + "nexilis/pages/read-qr?qr=")
+    }
+    
+    func showWebview(url: String) {
+        let controller = BNIBookingWebView()
+        controller.customUrl = url
+        controller.onDismiss = {
+            self.captureSession.startRunning()
+        }
+        present(controller, animated: true)
+    }
+}
+
+extension QRScannerViewController: AVCaptureMetadataOutputObjectsDelegate {
+    func metadataOutput(_ output: AVCaptureMetadataOutput,
+                        didOutput metadataObjects: [AVMetadataObject],
+                        from connection: AVCaptureConnection) {
+        
+        if let metadataObject = metadataObjects.first,
+           let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
+           let stringValue = readableObject.stringValue {
+            
+            captureSession.stopRunning()
+//            print("Scanned: \(stringValue)")
+            showWebview(url: Utils.getURLBase() + "nexilis/pages/read-qr?qr=" + stringValue)
+//            let alert = UIAlertController(title: "QR Result", message: stringValue, preferredStyle: .alert)
+//            alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
+//                self.captureSession.startRunning()
+//            })
+//            present(alert, animated: true, completion: nil)
+        }
+    }
+}
+
+extension UIImage {
+    func resized(to size: CGSize) -> UIImage {
+        let renderer = UIGraphicsImageRenderer(size: size)
+        return renderer.image { _ in
+            self.draw(in: CGRect(origin: .zero, size: size))
+        }
+    }
+}

+ 9 - 0
NexilisLite/NexilisLite/Source/View/BNIView/BNIBookingWebView.swift

@@ -33,10 +33,19 @@ public class BNIBookingWebView: UIViewController, WKNavigationDelegate, UIScroll
     var allowedURLs = Set<String>()
     var loadingURL = false
     
+    var onDismiss: (() -> Void)?
+    
     public override var preferredStatusBarStyle: UIStatusBarStyle {
         return .default
     }
     
+    public override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        if self.isBeingDismissed || self.isMovingFromParent {
+            onDismiss?()
+        }
+    }
+    
     public override func viewDidLoad() {
         super.viewDidLoad()
         

+ 246 - 7
NexilisLite/NexilisLite/Source/View/Chat/ChatWALikeVC.swift

@@ -49,7 +49,20 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
     
     var selectedTag = 0
     
+    var archivedChats: [Chat] = []
+    var listMaxArchived: [String: [String]] = [:]
+    var isGettingData = false
+    
     public override func viewDidLoad() {
+        NotificationCenter.default.addObserver(self, selector: #selector(onStatusChat(notification:)), name: NSNotification.Name(rawValue: Nexilis.listenerStatusChat), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReceiveMessage(notification:)), name: NSNotification.Name(rawValue: Nexilis.listenerReceiveChat), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReload(notification:)), name: NSNotification.Name(rawValue: "onMember"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReload(notification:)), name: NSNotification.Name(rawValue: "onUpdatePersonInfo"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReloadTab(notification:)), name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReloadTab(notification:)), name: NSNotification.Name(rawValue: "onTopic"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onDisconnected(notification:)), name: NSNotification.Name(rawValue: "disconnected_nexilis"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onDatabaseOpened(notification:)), name: NSNotification.Name(rawValue: "databaseOpened"), object: nil)
+        
         tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellChatWA")
         tableView.dataSource = self
         tableView.delegate = self
@@ -65,6 +78,53 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
         setupTableView()
     }
     
+    @objc func onDatabaseOpened(notification: NSNotification) {
+        DispatchQueue.main.async {
+            if self.loadingData {
+                self.refresh()
+            }
+        }
+    }
+    
+    @objc func onDisconnected(notification: NSNotification) {
+        DispatchQueue.main.async {
+            self.loadingData = true
+            self.chatGroupMaps.removeAll()
+            self.chats.removeAll()
+            self.tableView.reloadData()
+            self.refresh()
+        }
+    }
+    
+    @objc func onReloadTab(notification: NSNotification) {
+        DispatchQueue.main.async {
+            self.refresh()
+        }
+    }
+    
+    @objc func onReload(notification: NSNotification) {
+        DispatchQueue.main.async {
+            let data:[AnyHashable : Any] = notification.userInfo!
+            if data["member"] as? String == User.getMyPin()! {
+                self.refresh()
+            } else if data["state"] as? Int == 99 {
+                self.refresh()
+            }
+        }
+    }
+    
+    @objc func onReceiveMessage(notification: NSNotification) {
+        DispatchQueue.main.async {
+            self.refresh()
+        }
+    }
+    
+    @objc func onStatusChat(notification: NSNotification) {
+        DispatchQueue.main.async {
+            self.refresh()
+        }
+    }
+    
     public override func viewWillAppear(_ animated: Bool) {
         navigationItem.title = "Chats".localized()
         navigationItem.hidesSearchBarWhenScrolling = true
@@ -126,6 +186,9 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
     }
     
     private func refresh() {
+        if self.isGettingData {
+            return
+        }
         getData { [self] in
             if tempChats.count == 0 {
                 searchController.searchBar.isHidden = true
@@ -151,6 +214,7 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
                 }
                 searchController.searchBar.isHidden = false
             }
+            self.isGettingData = false
         }
     }
     
@@ -173,14 +237,21 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
     
     func getChats(completion: @escaping ()->()) {
         DispatchQueue.global().async {
+            self.isGettingData = true
             self.chatGroupMaps.removeAll()
+            self.listMaxArchived.removeAll()
             let previousChat = self.chats
             let allChats = Chat.getData()
+            self.archivedChats = Chat.getData(isArchived: true)
             var tempChats: [Chat] = []
+            var lowestPinned: [String: Int64] = [:]
 
             for singleChat in allChats {
                 guard !singleChat.groupId.isEmpty else {
                     tempChats.append(singleChat)
+                    if singleChat.pinned > 0 {
+                        self.listMaxArchived[singleChat.pin] = [""]
+                    }
                     continue
                 }
                 
@@ -194,6 +265,31 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
                         if let counterParent = Int(tempChats[parentChatIndex].counter), let counterSingle = Int(singleChat.counter) {
                             tempChats[parentChatIndex].counter = "\(counterParent + counterSingle)"
                         }
+                        if singleChat.pinned != 0 && (lowestPinned[singleChat.groupId] == 0 || lowestPinned[singleChat.groupId]! > singleChat.pinned) {
+                            lowestPinned[singleChat.groupId] = singleChat.pinned
+                        }
+                        if singleChat.pinned != 0 {
+                            if !self.listMaxArchived.keys.contains(singleChat.groupId) {
+                                self.listMaxArchived[singleChat.groupId] = [singleChat.pin]
+                            } else {
+                                self.listMaxArchived[singleChat.groupId]?.append(singleChat.pin)
+                            }
+                        }
+                        if tempChats[parentChatIndex].pinned < singleChat.pinned {
+                            tempChats[parentChatIndex].pinned = singleChat.pinned
+                        }
+                        if tempChats[parentChatIndex].pinned > 0 {
+                            if singleChat.pinned == 0 {
+                                singleChat.pinned = lowestPinned[singleChat.groupId]! - 1
+                                singleChat.isFolPinned = true
+                            }
+                            tempChats.forEach { chat in
+                                if chat.groupId == singleChat.groupId && (chat.pinned == 0 || (chat.isFolPinned && chat.pinned != lowestPinned[singleChat.groupId]! - 1)) {
+                                    chat.pinned = lowestPinned[singleChat.groupId]! - 1
+                                    chat.isFolPinned = true
+                                }
+                            }
+                        }
                     }
                     
                     if let parentExist = chatParentInPreviousChats, parentExist.isSelected,
@@ -201,10 +297,21 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
                         tempChats.insert(singleChat, at: min(indexParent + existingGroup.count, tempChats.count))
                     }
                 } else {
+                    if lowestPinned[singleChat.groupId] == nil {
+                        lowestPinned[singleChat.groupId] = 0
+                    }
+                    if singleChat.pinned != 0 {
+                        lowestPinned[singleChat.groupId] = singleChat.pinned
+                    }
                     self.chatGroupMaps[singleChat.groupId] = [singleChat]
                     let parentChat = Chat(profile: singleChat.profile, groupName: singleChat.groupName, counter: singleChat.counter, groupId: singleChat.groupId)
                     parentChat.isParent = true
                     
+                    if parentChat.pinned != singleChat.pinned {
+                        parentChat.pinned = singleChat.pinned
+                        self.listMaxArchived[singleChat.groupId] = [singleChat.pin]
+                    }
+                    
                     if let parentExist = chatParentInPreviousChats, parentExist.isSelected {
                         parentChat.isSelected = true
                         tempChats.append(parentChat)
@@ -214,14 +321,11 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
                     }
                 }
             }
-            self.tempChats = tempChats
-            if ChatWALikeVC.filterMain == 0 {
-                self.chats = tempChats
-            } else if ChatWALikeVC.filterMain == 1 {
-                self.chats = tempChats.filter({ Int($0.counter) ?? 0 > 0 })
-            } else {
-                self.chats = tempChats.filter({ !$0.groupId.isEmpty })
+            tempChats.sort(by: { $0.pinned > $1.pinned })
+            if self.archivedChats.count > 0 {
+                tempChats.insert(Chat(pin: "Archived"), at: 0)
             }
+            self.chats = tempChats
             completion()
         }
     }
@@ -325,6 +429,9 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
         }
         let fontSize = Int(SecureUserDefaults.shared.value(forKey: "font_size") ?? "0")
         var finalHeight = 75.0
+        if self.archivedChats.count > 0 && indexPath.row == 0 {
+            finalHeight = 50.0
+        }
         if fontSize == 4 {
             finalHeight += 10
         } else if fontSize == 6 {
@@ -342,6 +449,102 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
         }
     }
     
+    public func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+        let data: Chat
+        if isFiltering {
+            data = fillteredData[indexPath.row] as! Chat
+        } else {
+            data = chats[indexPath.row]
+        }
+        if !data.isParent {
+            if self.archivedChats.count > 0 && indexPath.row == 0 {
+                return nil
+            }
+            let archiveAction = UIContextualAction(style: .normal, title: "Archive".localized()) { (_, _, completionHandler) in
+                DispatchQueue.global().async {
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                                "pinned" : 0,
+                                "archived" : Date().currentTimeMillis()
+                            ], _where: "l_pin = '\(data.pin)'")
+                        } catch {
+                            rollback.pointee = true
+                            print("Access database error: \(error.localizedDescription)")
+                        }
+                    })
+                }
+                self.chats.remove(at: indexPath.row)
+                tableView.deleteRows(at: [indexPath], with: .right)
+                self.getChats() {
+                    DispatchQueue.main.async {
+                        self.tableView.reloadData()
+                        self.isGettingData = false
+                    }
+                }
+                completionHandler(true)
+            }
+            let archiveIcon = UIImage(systemName: "archivebox.fill")!
+            archiveAction.backgroundColor = UIColor.mainColor
+            archiveAction.image = archiveIcon
+
+            var textPinned = "Pin".localized()
+            var pinnedDate = Date().currentTimeMillis()
+            var imagePinned = UIImage(systemName: "pin.fill")!
+            if data.pinned != 0 && !data.isFolPinned {
+                textPinned = "Unpin".localized()
+                pinnedDate = 0
+                imagePinned = UIImage(systemName: "pin.slash.fill")!
+            }
+            let unpinAction = UIContextualAction(style: .normal, title: textPinned) { (_, _, completionHandler) in
+                if pinnedDate != 0 && ((self.listMaxArchived[data.groupId] != nil && self.listMaxArchived[data.groupId]!.count == 3) || (self.listMaxArchived.count == 3)) {
+                    let alertController = LibAlertController(
+                        title: "You can only pin 3 chats".localized(),
+                        message: nil,
+                        preferredStyle: .alert
+                    )
+                    
+                    alertController.addAction(UIAlertAction(title: "OK".localized(), style: .cancel, handler: nil))
+                    
+                    if UIApplication.shared.visibleViewController?.navigationController != nil {
+                        UIApplication.shared.visibleViewController?.navigationController?.present(alertController, animated: true, completion: nil)
+                    } else {
+                        UIApplication.shared.visibleViewController?.present(alertController, animated: true, completion: nil)
+                    }
+                } else {
+                    DispatchQueue.global().async {
+                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                            do {
+                                _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
+                                    "pinned" : pinnedDate
+                                ], _where: "l_pin = '\(data.pin)'")
+                            } catch {
+                                rollback.pointee = true
+                                print("Access database error: \(error.localizedDescription)")
+                            }
+                        })
+                    }
+                    self.chats.remove(at: indexPath.row)
+                    tableView.deleteRows(at: [indexPath], with: .right)
+                    self.getChats() {
+                        DispatchQueue.main.async {
+                            self.tableView.reloadData()
+                            self.isGettingData = false
+                        }
+                    }
+                }
+                completionHandler(true)
+            }
+            let pinIcon = imagePinned
+            unpinAction.backgroundColor = UIColor.darkGray
+            unpinAction.image = pinIcon
+
+            let configuration = UISwipeActionsConfiguration(actions: [archiveAction, unpinAction])
+            return configuration
+        }
+        return nil
+    }
+    
     public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
         if isFiltering {
             return fillteredData.count
@@ -665,6 +868,35 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
             return cell
         }
         
+        if self.archivedChats.count > 0 && indexPath.row == 0 {
+            let imageView = UIImageView()
+            content.addSubview(imageView)
+            imageView.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                imageView.centerYAnchor.constraint(equalTo: content.centerYAnchor),
+                imageView.widthAnchor.constraint(equalToConstant: 20.0),
+                imageView.heightAnchor.constraint(equalToConstant: 20.0),
+                imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 25.0)
+            ])
+            imageView.image = UIImage(systemName: "archivebox")!
+            imageView.tintColor = .darkGray
+            
+            let titleView = UILabel()
+            content.addSubview(titleView)
+            titleView.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                titleView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 25.0),
+                titleView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -40.0),
+                titleView.centerYAnchor.constraint(equalTo: content.centerYAnchor),
+            ])
+            titleView.font = UIFont.boldSystemFont(ofSize: 14 + String.offset())
+            titleView.text = "Archived".localized()
+            titleView.textColor = .darkGray
+            cell.backgroundColor = .clear
+            cell.separatorInset = UIEdgeInsets(top: 0, left: 60.0, bottom: 0, right: 0)
+            return cell
+        }
+        
         let imageView = UIImageView()
         content.addSubview(imageView)
         imageView.translatesAutoresizingMaskIntoConstraints = false
@@ -1043,6 +1275,13 @@ public class ChatWALikeVC: UIViewController, UITableViewDataSource, UITableViewD
         } else {
             data = chats[indexPath.row]
         }
+        if self.archivedChats.count > 0 && indexPath.row == 0 {
+            let controller = ArchivedChatView()
+            controller.archivedChats = archivedChats
+            controller.hidesBottomBarWhenPushed = true
+            navigationController?.show(controller, sender: nil)
+            return
+        }
         if data.isParent {
             expandCollapseChats(tableView: tableView, indexPath: indexPath)
             return

+ 4 - 4
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -4071,10 +4071,10 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
             }
             if !(dataMessages[indexPath!.row][TypeDataMessage.message_text]  as? String ?? "").isEmpty {
                 if (dataMessages[indexPath!.row]["f_pin"]  as? String ?? "") == idMe && ((dataMessages[indexPath!.row][TypeDataMessage.is_forwarded] as? Int) ?? 0) == 0 && (dataMessages[indexPath!.row][TypeDataMessage.attachment_flag] as? String ?? "") != "11" {
-                    let date = Date(milliseconds: Int64(dataMessages[indexPath!.row][TypeDataMessage.server_date] as? String ?? "") ?? 0)
-                    let pastDate = date.addingTimeInterval(-10 * 60)
-                    let differenceInSeconds = date.timeIntervalSince(pastDate)
-                    if abs(differenceInSeconds) <= 15 * 60 {
+                    let valueDate = Date(milliseconds: Int64(dataMessages[indexPath!.row][TypeDataMessage.server_date] as? String ?? "") ?? 0)
+                    let nowDate = Date()
+                    let diffInSeconds = nowDate.timeIntervalSince(valueDate)
+                    if diffInSeconds <= 15 * 60 {
                         children.insert(edit, at: children.count - 1)
                     }
                 }

+ 4 - 4
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -5237,10 +5237,10 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
             }
             if !(dataMessages[indexPath!.row][TypeDataMessage.message_text]  as? String ?? "").isEmpty {
                 if (dataMessages[indexPath!.row]["f_pin"]  as? String ?? "") == idMe && ((dataMessages[indexPath!.row][TypeDataMessage.is_forwarded] as? Int) ?? 0) == 0 && (dataMessages[indexPath!.row][TypeDataMessage.attachment_flag] as? String ?? "") != "11" {
-                    let date = Date(milliseconds: Int64(dataMessages[indexPath!.row][TypeDataMessage.server_date] as? String ?? "") ?? 0)
-                    let pastDate = date.addingTimeInterval(-10 * 60)
-                    let differenceInSeconds = date.timeIntervalSince(pastDate)
-                    if abs(differenceInSeconds) <= 15 * 60 {
+                    let valueDate = Date(milliseconds: Int64(dataMessages[indexPath!.row][TypeDataMessage.server_date] as? String ?? "") ?? 0)
+                    let nowDate = Date()
+                    let diffInSeconds = nowDate.timeIntervalSince(valueDate)
+                    if diffInSeconds <= 15 * 60 {
                         children.insert(edit, at: children.count - 1)
                     }
                 }

+ 6 - 1
NexilisLite/NexilisLite/Source/View/Chat/MessageInfo.swift

@@ -63,17 +63,22 @@ class MessageInfo: UIViewController, UITableViewDelegate, UITableViewDataSource,
     private func getData() {
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
-                if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, status, time_delivered, time_read, time_ack, longitude, latitude FROM MESSAGE_STATUS where message_id='\(data["message_id"]!!)' ORDER BY time_ack DESC, time_read DESC, time_delivered DESC") {
+                if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, status, time_delivered, time_read, time_ack, longitude, latitude, message_id FROM MESSAGE_STATUS where message_id='\(data["message_id"]!!)' ORDER BY time_ack DESC, time_read DESC, time_delivered DESC") {
                     var listStatus: [Int] = []
                     while cursorData.next() {
+                        let messageId = cursorData.string(forColumnIndex: 7) ?? ""
                         var row: [String: Any?] = [:]
                         row["f_pin"] = cursorData.string(forColumnIndex: 0) ?? ""
+                        if dataStatus.count > 0 && dataStatus.contains(where: { $0["message_id"] as? String == messageId && $0["f_pin"] as? String == row["f_pin"] as? String }) {
+                            continue
+                        }
                         row["status"] = cursorData.string(forColumnIndex: 1) ?? ""
                         row["time_delivered"] = cursorData.string(forColumnIndex: 2) ?? ""
                         row["time_read"] = cursorData.string(forColumnIndex: 3) ?? ""
                         row["time_ack"] = cursorData.string(forColumnIndex: 4) ?? ""
                         row["longitude"] = cursorData.string(forColumnIndex: 5) ?? ""
                         row["latitude"] = cursorData.string(forColumnIndex: 6) ?? ""
+                        row["message_id"] = messageId
                         dataStatus.append(row)
                         listStatus.append(Int(row["status"] as! String)!)
                     }

+ 73 - 63
NexilisLite/NexilisLite/Source/View/Control/ChangeDeviceViewController.swift

@@ -234,54 +234,59 @@ public class ChangeDeviceViewController: UIViewController {
         
         func sendSVL() {
             DispatchQueue.global().async {
-                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 {
-                                self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                do {
+                    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 {
+                                    self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                                }
+                                return
                             }
-                            return
-                        }
-                        var pk = ""
-                        var sign = ""
-                        let df = HMACDeviceFingerprintNexilis.generate()
-                        if let dataSign = "\(data)!\(df)".data(using: .utf8) {
-                            if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
-                                sign = signature.base64EncodedString()
+                            var pk = ""
+                            var sign = ""
+                            let df = HMACDeviceFingerprintNexilis.generate()
+                            if let dataSign = "\(data)!\(df)".data(using: .utf8) {
+                                if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
+                                    sign = signature.base64EncodedString()
+                                }
                             }
-                        }
-                        if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
-                            pk = publicKey
-                        }
-                        if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: "", p_vercode: method == 0 ? "" : code, number: number, deviceFingerprint: df, publicKey: pk, signature: sign), timeout: 30 * 1000) {
-                            if !response.isOk() {
-                                if method == 1 {
-                                    DispatchQueue.main.async {
-                                        self.showPageOTP(phone: number, errCode: response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99"), method: method)
+                            if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
+                                pk = publicKey
+                            }
+                            let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 30)
+                            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: "", p_vercode: method == 0 ? "" : code, number: number, deviceFingerprint: df, publicKey: pk, signature: sign, totp: otp), timeout: 30 * 1000) {
+                                if !response.isOk() {
+                                    if method == 1 {
+                                        DispatchQueue.main.async {
+                                            self.showPageOTP(phone: number, errCode: response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99"), method: method)
+                                        }
+                                    } else {
+                                        DispatchQueue.main.async {
+                                            self.showFailedSignUpIn(title: "Failed".localized())
+                                        }
                                     }
                                 } else {
-                                    DispatchQueue.main.async {
-                                        self.showFailedSignUpIn(title: "Failed".localized())
-                                    }
+                                    self.successSubmit(response: response, name: "", number: number)
                                 }
                             } else {
-                                self.successSubmit(response: response, name: "", number: number)
+                                DispatchQueue.main.async {
+                                    self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                                }
                             }
                         } else {
                             DispatchQueue.main.async {
-                                self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                                self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
                             }
                         }
                     } else {
                         DispatchQueue.main.async {
-                            self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                            self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
                         }
                     }
-                } else {
-                    DispatchQueue.main.async {
-                        self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
-                    }
+                } catch {
+                    
                 }
             }
         }
@@ -357,50 +362,55 @@ public class ChangeDeviceViewController: UIViewController {
                 return
             }
             DispatchQueue.global().async {
-                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 {
-                                self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                do {
+                    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 {
+                                    self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                                }
+                                return
                             }
-                            return
-                        }
-                        var pk = ""
-                        var sign = ""
-                        let df = HMACDeviceFingerprintNexilis.generate()
-                        if let dataSign = "\(data)!\(df)".data(using: .utf8) {
-                            if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
-                                sign = signature.base64EncodedString()
+                            var pk = ""
+                            var sign = ""
+                            let df = HMACDeviceFingerprintNexilis.generate()
+                            if let dataSign = "\(data)!\(df)".data(using: .utf8) {
+                                if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
+                                    sign = signature.base64EncodedString()
+                                }
                             }
-                        }
-                        if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
-                            pk = publicKey
-                        }
-                        if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: email, p_vercode: code, deviceFingerprint: df, publicKey: pk, signature: sign), timeout: 30 * 1000) {
-                            if !response.isOk() {
-                                DispatchQueue.main.async {
-                                    Nexilis.hideLoader {
-                                        self.showPageOTP(email: email, errCode: response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99"))
+                            if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
+                                pk = publicKey
+                            }
+                            let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 30)
+                            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: email, p_vercode: code, deviceFingerprint: df, publicKey: pk, signature: sign, totp: otp), timeout: 30 * 1000) {
+                                if !response.isOk() {
+                                    DispatchQueue.main.async {
+                                        Nexilis.hideLoader {
+                                            self.showPageOTP(email: email, errCode: response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99"))
+                                        }
                                     }
+                                } else {
+                                    self.successSubmit(response: response, name: "", email: email)
                                 }
                             } else {
-                                self.successSubmit(response: response, name: "", email: email)
+                                DispatchQueue.main.async {
+                                    self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                                }
                             }
                         } else {
                             DispatchQueue.main.async {
-                                self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                                self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
                             }
                         }
                     } else {
                         DispatchQueue.main.async {
-                            self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                            self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
                         }
                     }
-                } else {
-                    DispatchQueue.main.async {
-                        self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
-                    }
+                } catch {
+                    
                 }
             }
         }

+ 2 - 2
NexilisLite/NexilisLite/Source/View/Control/MFAViewController.swift

@@ -349,8 +349,8 @@ class MFAViewController: UIViewController {
                                 tMessage.mBodies[CoreMessage_TMessageKey.PUBLIC_KEY] = publicKey
                             }
                         }
-                        let secret = "JBSWY3DPEHPK3PXP" // Google Authenticator example
-                        let otp = try TOTPGenerator.generateTOTP(base32Secret: secret, digits: 6, timeStepSeconds: 300)
+//                        let secret = "JBSWY3DPEHPK3PXP" // Google Authenticator example
+                        let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 30)
                         tMessage.mBodies[CoreMessage_TMessageKey.TOTP] = otp
                         if let response = Nexilis.writeAndWait(message: tMessage) {
                             if response.isOk() {

+ 122 - 107
NexilisLite/NexilisLite/Source/View/Control/SignUpSignIn.swift

@@ -233,54 +233,59 @@ public class SignUpSignIn: UIViewController {
         
         func sendSVL() {
             DispatchQueue.global().async {
-                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 {
-                                self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                do {
+                    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 {
+                                    self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                                }
+                                return
                             }
-                            return
-                        }
-                        var pk = ""
-                        var sign = ""
-                        let df = HMACDeviceFingerprintNexilis.generate()
-                        if let dataSign = "\(data)!\(df)".data(using: .utf8) {
-                            if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
-                                sign = signature.base64EncodedString()
+                            var pk = ""
+                            var sign = ""
+                            let df = HMACDeviceFingerprintNexilis.generate()
+                            if let dataSign = "\(data)!\(df)".data(using: .utf8) {
+                                if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
+                                    sign = signature.base64EncodedString()
+                                }
                             }
-                        }
-                        if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
-                            pk = publicKey
-                        }
-                        if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: "", p_vercode: method == 0 ? "" : code, number: number, deviceFingerprint: df, publicKey: pk, signature: sign), timeout: 30 * 1000) {
-                            if !response.isOk() {
-                                if method == 1 {
-                                    DispatchQueue.main.async {
-                                        self.showPageOTP(phone: number, errCode: response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99"), method: method)
+                            if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
+                                pk = publicKey
+                            }
+                            let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 30)
+                            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: "", p_vercode: method == 0 ? "" : code, number: number, deviceFingerprint: df, publicKey: pk, signature: sign, totp: otp), timeout: 30 * 1000) {
+                                if !response.isOk() {
+                                    if method == 1 {
+                                        DispatchQueue.main.async {
+                                            self.showPageOTP(phone: number, errCode: response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99"), method: method)
+                                        }
+                                    } else {
+                                        DispatchQueue.main.async {
+                                            self.showFailedSignUpIn(title: "Failed".localized())
+                                        }
                                     }
                                 } else {
-                                    DispatchQueue.main.async {
-                                        self.showFailedSignUpIn(title: "Failed".localized())
-                                    }
+                                    self.successSubmit(response: response, first: "", last: "", number: number)
                                 }
                             } else {
-                                self.successSubmit(response: response, first: "", last: "", number: number)
+                                DispatchQueue.main.async {
+                                    self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                                }
                             }
                         } else {
                             DispatchQueue.main.async {
-                                self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                                self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
                             }
                         }
                     } else {
                         DispatchQueue.main.async {
-                            self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                            self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
                         }
                     }
-                } else {
-                    DispatchQueue.main.async {
-                        self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
-                    }
+                } catch {
+                    
                 }
             }
         }
@@ -356,50 +361,55 @@ public class SignUpSignIn: UIViewController {
                 return
             }
             DispatchQueue.global().async {
-                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 {
-                                self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                do {
+                    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 {
+                                    self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                                }
+                                return
                             }
-                            return
-                        }
-                        var pk = ""
-                        var sign = ""
-                        let df = HMACDeviceFingerprintNexilis.generate()
-                        if let dataSign = "\(data)!\(df)".data(using: .utf8) {
-                            if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
-                                sign = signature.base64EncodedString()
+                            var pk = ""
+                            var sign = ""
+                            let df = HMACDeviceFingerprintNexilis.generate()
+                            if let dataSign = "\(data)!\(df)".data(using: .utf8) {
+                                if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
+                                    sign = signature.base64EncodedString()
+                                }
                             }
-                        }
-                        if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
-                            pk = publicKey
-                        }
-                        if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: email, p_vercode: code, deviceFingerprint: df, publicKey: pk, signature: sign), timeout: 30 * 1000) {
-                            if !response.isOk() {
-                                DispatchQueue.main.async {
-                                    Nexilis.hideLoader {
-                                        self.showPageOTP(email: email, errCode: response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99"))
+                            if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
+                                pk = publicKey
+                            }
+                            let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 30)
+                            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: email, p_vercode: code, deviceFingerprint: df, publicKey: pk, signature: sign, totp: otp), timeout: 30 * 1000) {
+                                if !response.isOk() {
+                                    DispatchQueue.main.async {
+                                        Nexilis.hideLoader {
+                                            self.showPageOTP(email: email, errCode: response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99"))
+                                        }
                                     }
+                                } else {
+                                    self.successSubmit(response: response, first: "", last: "", email: email)
                                 }
                             } else {
-                                self.successSubmit(response: response, first: "", last: "", email: email)
+                                DispatchQueue.main.async {
+                                    self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                                }
                             }
                         } else {
                             DispatchQueue.main.async {
-                                self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                                self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
                             }
                         }
                     } else {
                         DispatchQueue.main.async {
-                            self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                            self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
                         }
                     }
-                } else {
-                    DispatchQueue.main.async {
-                        self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
-                    }
+                } catch {
+                    
                 }
             }
         }
@@ -541,65 +551,70 @@ public class SignUpSignIn: UIViewController {
         }
         Nexilis.showLoader()
         DispatchQueue.global().async {
-            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 {
-                            self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
-                        }
-                        return
-                    }
-                    let md5Hex = password
-                    var pk = ""
-                    var sign = ""
-                    let df = HMACDeviceFingerprintNexilis.generate()
-                    if let dataSign = "\(data)!\(df)".data(using: .utf8) {
-                        if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
-                            sign = signature.base64EncodedString()
-                        }
-                    }
-                    if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
-                        pk = publicKey
-                    }
-                    if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpSignInAPI(p_name: name, p_password: md5Hex, deviceFingerprint: df, publicKey: pk, signature: sign), timeout: 30 * 1000) {
-                        if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "20" {
-                            DispatchQueue.main.async {
-                                self.showFailedSignUpIn(title: "Invalid user / Username and password does not match".localized())
-                            }
-                        } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "11" {
+            do {
+                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 {
-                                self.showFailedSignUpIn(title: "Failed, unknown user".localized())
+                                self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
                             }
-                        } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "4u" {
-                            DispatchQueue.main.async {
-                                self.showFailedSignUpIn(title: "Failed, blocked user".localized())
+                            return
+                        }
+                        let md5Hex = password
+                        var pk = ""
+                        var sign = ""
+                        let df = HMACDeviceFingerprintNexilis.generate()
+                        if let dataSign = "\(data)!\(df)".data(using: .utf8) {
+                            if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
+                                sign = signature.base64EncodedString()
                             }
-                        } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "13" {
-                            DispatchQueue.main.async {
-                                self.showFailedSignUpIn(title: "Failed, This user is not registered on this device".localized())
+                        }
+                        if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
+                            pk = publicKey
+                        }
+                        let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 30)
+                        if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpSignInAPI(p_name: name, p_password: md5Hex, deviceFingerprint: df, publicKey: pk, signature: sign, totp: otp), timeout: 30 * 1000) {
+                            if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "20" {
+                                DispatchQueue.main.async {
+                                    self.showFailedSignUpIn(title: "Invalid user / Username and password does not match".localized())
+                                }
+                            } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "11" {
+                                DispatchQueue.main.async {
+                                    self.showFailedSignUpIn(title: "Failed, unknown user".localized())
+                                }
+                            } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "4u" {
+                                DispatchQueue.main.async {
+                                    self.showFailedSignUpIn(title: "Failed, blocked user".localized())
+                                }
+                            } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "13" {
+                                DispatchQueue.main.async {
+                                    self.showFailedSignUpIn(title: "Failed, This user is not registered on this device".localized())
+                                }
+                            } else if !response.isOk() {
+                                DispatchQueue.main.async {
+                                    self.showFailedSignUpIn(title: "Failed".localized())
+                                }
+                            } else {
+                                self.successSubmit(response: response, first: first, last: last)
                             }
-                        } else if !response.isOk() {
+                        } else {
                             DispatchQueue.main.async {
-                                self.showFailedSignUpIn(title: "Failed".localized())
+                                self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
                             }
-                        } else {
-                            self.successSubmit(response: response, first: first, last: last)
                         }
                     } else {
                         DispatchQueue.main.async {
-                            self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                            self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
                         }
                     }
                 } else {
                     DispatchQueue.main.async {
-                        self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                        self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
                     }
                 }
-            } else {
-                DispatchQueue.main.async {
-                    self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
-                }
+            } catch {
+                
             }
         }
     }

+ 39 - 0
NexilisLite/NexilisLite/Source/View/Control/TOTPGenerator.swift

@@ -86,4 +86,43 @@ class TOTPGenerator {
         CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), key, key.count, data, data.count, &macData)
         return macData
     }
+    
+    static func getTOTP() -> String {
+        var session_id = Utils.getConnectionID()
+        if session_id.isEmpty {
+            let sDID = UIDevice.current.identifierForVendor?.uuidString ?? "UNK-DEVICE"
+            session_id = String(sDID[sDID.index(sDID.endIndex, offsetBy: -5)...]) + "\(Date().currentTimeMillis())"
+            Utils.setConnectionID(value: session_id)
+        }
+        let base32Secret = Base32.encode([UInt8](session_id.data(using: .utf8)!))
+        return base32Secret
+    }
+}
+
+struct Base32 {
+    private static let alphabet = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")
+
+    static func encode(_ bytes: [UInt8]) -> String {
+        var result = ""
+        var buffer = 0
+        var bitsLeft = 0
+
+        for b in bytes {
+            buffer = (buffer << 8) | Int(b & 0xFF)
+            bitsLeft += 8
+
+            while bitsLeft >= 5 {
+                let index = (buffer >> (bitsLeft - 5)) & 0x1F
+                result.append(alphabet[index])
+                bitsLeft -= 5
+            }
+        }
+
+        if bitsLeft > 0 {
+            let index = (buffer << (5 - bitsLeft)) & 0x1F
+            result.append(alphabet[index])
+        }
+
+        return result
+    }
 }