浏览代码

fix bugs, repair init conection flow, and release for 5.0.73

alqindiirsyam 1 周之前
父节点
当前提交
7926f60896
共有 40 个文件被更改,包括 5415 次插入538 次删除
  1. 二进制
      .DS_Store
  2. 二进制
      AppBuilder/.DS_Store
  3. 20 4
      AppBuilder/AppBuilder.xcodeproj/project.pbxproj
  4. 0 1
      AppBuilder/AppBuilder/AppDelegate.swift
  5. 13 4
      AppBuilder/AppBuilder/Base.lproj/Main.storyboard
  6. 38 0
      AppBuilder/AppBuilder/FirstTabViewController.swift
  7. 20 0
      AppBuilder/AppBuilder/PrefsUtil.swift
  8. 4 2
      AppBuilder/AppBuilder/SecondTabViewController.swift
  9. 38 0
      AppBuilder/AppBuilder/ThirdTabViewController.swift
  10. 105 9
      AppBuilder/AppBuilder/ViewController.swift
  11. 947 0
      AppBuilder/AppBuilder/WebView3.swift
  12. 947 0
      AppBuilder/AppBuilder/WebView4.swift
  13. 947 0
      AppBuilder/AppBuilder/WebView5.swift
  14. 947 0
      AppBuilder/AppBuilder/WebView6.swift
  15. 21 0
      NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_ic_attach_spc_badge.imageset/Contents.json
  16. 二进制
      NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_ic_attach_spc_badge.imageset/pb_ic_attach_spc_badge.png
  17. 39 11
      NexilisLite/NexilisLite/Source/APIS.swift
  18. 35 14
      NexilisLite/NexilisLite/Source/CoreMessage_TMessageBank.swift
  19. 3 0
      NexilisLite/NexilisLite/Source/CoreMessage_TMessageCode.swift
  20. 3 0
      NexilisLite/NexilisLite/Source/CoreMessage_TMessageKey.swift
  21. 3 3
      NexilisLite/NexilisLite/Source/Extension.swift
  22. 4 4
      NexilisLite/NexilisLite/Source/FileEncryption.swift
  23. 17 9
      NexilisLite/NexilisLite/Source/IncomingThread.swift
  24. 5 4
      NexilisLite/NexilisLite/Source/MasterKeyUtil.swift
  25. 333 224
      NexilisLite/NexilisLite/Source/Nexilis.swift
  26. 47 4
      NexilisLite/NexilisLite/Source/Utils.swift
  27. 二进制
      NexilisLite/NexilisLite/Source/View/.DS_Store
  28. 1 5
      NexilisLite/NexilisLite/Source/View/Call/CallManager.swift
  29. 1 11
      NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift
  30. 1 11
      NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift
  31. 9 3
      NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift
  32. 9 3
      NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift
  33. 10 7
      NexilisLite/NexilisLite/Source/View/Chat/SecureFolderView.swift
  34. 181 84
      NexilisLite/NexilisLite/Source/View/Control/ChangeDeviceViewController.swift
  35. 2 2
      NexilisLite/NexilisLite/Source/View/Control/ChangePasswordViewController.swift
  36. 8 0
      NexilisLite/NexilisLite/Source/View/Control/GroupDetailViewController.swift
  37. 7 1
      NexilisLite/NexilisLite/Source/View/Control/MFAViewController.swift
  38. 3 3
      NexilisLite/NexilisLite/Source/View/Control/SignInOption.swift
  39. 208 115
      NexilisLite/NexilisLite/Source/View/Control/SignUpSignIn.swift
  40. 439 0
      NexilisLite/NexilisLite/Source/View/Control/TFAPasswordVC.swift

二进制
.DS_Store


二进制
AppBuilder/.DS_Store


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

@@ -21,6 +21,10 @@
 		A42ED92627F439A200B0FAB7 /* ThirdTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42ED92527F439A200B0FAB7 /* ThirdTabViewController.swift */; };
 		CD08A1682D5EEDA5005B4EAC /* AppBuilderShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CD08A15E2D5EEDA5005B4EAC /* AppBuilderShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
 		CD5D8BCF2E2754D100F56410 /* StatusUpdateVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD5D8BCE2E2754C100F56410 /* StatusUpdateVC.swift */; };
+		CD7E04562EB465BF00C52437 /* WebView3.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7E04552EB465B700C52437 /* WebView3.swift */; };
+		CD7E04582EB465D900C52437 /* WebView4.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7E04572EB465D100C52437 /* WebView4.swift */; };
+		CD7E045A2EB465E400C52437 /* WebView5.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7E04592EB465DD00C52437 /* WebView5.swift */; };
+		CD7E045C2EB4660600C52437 /* WebView6.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7E045B2EB4660000C52437 /* WebView6.swift */; };
 		CD9D59D92BEE1D30008014B4 /* digisales_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = CD9D59CE2BEE1D2F008014B4 /* digisales_icon.png */; };
 		CD9D59DA2BEE1D30008014B4 /* ikn_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = CD9D59CF2BEE1D2F008014B4 /* ikn_icon.png */; };
 		CD9D59DB2BEE1D30008014B4 /* gudeg_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = CD9D59D02BEE1D2F008014B4 /* gudeg_icon.png */; };
@@ -90,6 +94,10 @@
 		A42ED92527F439A200B0FAB7 /* ThirdTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdTabViewController.swift; sourceTree = "<group>"; };
 		CD08A15E2D5EEDA5005B4EAC /* AppBuilderShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AppBuilderShare.appex; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD5D8BCE2E2754C100F56410 /* StatusUpdateVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdateVC.swift; sourceTree = "<group>"; };
+		CD7E04552EB465B700C52437 /* WebView3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView3.swift; sourceTree = "<group>"; };
+		CD7E04572EB465D100C52437 /* WebView4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView4.swift; sourceTree = "<group>"; };
+		CD7E04592EB465DD00C52437 /* WebView5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView5.swift; sourceTree = "<group>"; };
+		CD7E045B2EB4660000C52437 /* WebView6.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView6.swift; sourceTree = "<group>"; };
 		CD9D59CE2BEE1D2F008014B4 /* digisales_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = digisales_icon.png; sourceTree = "<group>"; };
 		CD9D59CF2BEE1D2F008014B4 /* ikn_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ikn_icon.png; sourceTree = "<group>"; };
 		CD9D59D02BEE1D2F008014B4 /* gudeg_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gudeg_icon.png; sourceTree = "<group>"; };
@@ -174,6 +182,10 @@
 				CD5D8BCE2E2754C100F56410 /* StatusUpdateVC.swift */,
 				A42ED92527F439A200B0FAB7 /* ThirdTabViewController.swift */,
 				2401CE9D275490DB00B323BB /* ViewController.swift */,
+				CD7E04552EB465B700C52437 /* WebView3.swift */,
+				CD7E04572EB465D100C52437 /* WebView4.swift */,
+				CD7E04592EB465DD00C52437 /* WebView5.swift */,
+				CD7E045B2EB4660000C52437 /* WebView6.swift */,
 				2401CEA2275490E600B323BB /* Assets.xcassets */,
 				2401CEA4275490E600B323BB /* LaunchScreen.storyboard */,
 				2401CE9F275490DB00B323BB /* Main.storyboard */,
@@ -374,9 +386,13 @@
 				A42ED92427F3FC2F00B0FAB7 /* SecondTabViewController.swift in Sources */,
 				A413B18727EACB20006D16EB /* PrefsUtil.swift in Sources */,
 				A42ED92227F30BA200B0FAB7 /* FirstTabViewController.swift in Sources */,
+				CD7E045A2EB465E400C52437 /* WebView5.swift in Sources */,
+				CD7E045C2EB4660600C52437 /* WebView6.swift in Sources */,
+				CD7E04562EB465BF00C52437 /* WebView3.swift in Sources */,
 				A42ED92627F439A200B0FAB7 /* ThirdTabViewController.swift in Sources */,
 				2401CE9E275490DB00B323BB /* ViewController.swift in Sources */,
 				2401CE9A275490DB00B323BB /* AppDelegate.swift in Sources */,
+				CD7E04582EB465D900C52437 /* WebView4.swift in Sources */,
 				CD5D8BCF2E2754D100F56410 /* StatusUpdateVC.swift in Sources */,
 				12960AE02892361000A467DD /* FourthTabViewController.swift in Sources */,
 				2401CE9C275490DB00B323BB /* SceneDelegate.swift in Sources */,
@@ -568,7 +584,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.72;
+				MARKETING_VERSION = 5.0.73;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -604,7 +620,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 5.0.72;
+				MARKETING_VERSION = 5.0.73;
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -640,7 +656,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.72;
+				MARKETING_VERSION = 5.0.73;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -679,7 +695,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 5.0.72;
+				MARKETING_VERSION = 5.0.73;
 				OTHER_CFLAGS = "-fstack-protector-strong";
 				PRODUCT_BUNDLE_IDENTIFIER = io.nexilis.appbuilder.AppBuilderShare;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 0 - 1
AppBuilder/AppBuilder/AppDelegate.swift

@@ -21,7 +21,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate {
         if PrefsUtil.getCpaasMode() == PrefsUtil.CPAAS_MODE_FLOATING || PrefsUtil.getCpaasMode() == PrefsUtil.CPAAS_MODE_MIX {
             showButton = true
         }
-        Nexilis.isShowForceSignIn = false
         APIS.connect(appName: appName , apiKey: apikey, delegate: self, showButton: showButton, fromMAB: true)
         registerForPushNotifications()
         registerForVoIPPushNotifications()

+ 13 - 4
AppBuilder/AppBuilder/Base.lproj/Main.storyboard

@@ -1,9 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="nD6-T3-59p">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24128" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="nD6-T3-59p">
     <device id="retina6_7" orientation="portrait" appearance="dark"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
@@ -12,9 +13,9 @@
             <objects>
                 <navigationController title="test" id="nD6-T3-59p" sceneMemberID="viewController">
                     <navigationBar key="navigationBar" opaque="NO" contentMode="scaleToFill" id="mEs-FV-nQc">
-                        <rect key="frame" x="0.0" y="118" width="430" height="44"/>
+                        <rect key="frame" x="0.0" y="118" width="430" height="54"/>
                         <autoresizingMask key="autoresizingMask"/>
-                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                         <color key="tintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                         <color key="barTintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                         <textAttributes key="titleTextAttributes">
@@ -22,10 +23,12 @@
                         </textAttributes>
                         <textAttributes key="largeTitleTextAttributes">
                             <color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <color key="textShadowColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                         </textAttributes>
                     </navigationBar>
                     <toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="7Tu-EZ-US7">
                         <autoresizingMask key="autoresizingMask"/>
+                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                     </toolbar>
                     <connections>
                         <segue destination="303-e4-dlv" kind="relationship" relationship="rootViewController" id="ax7-Vc-WBo"/>
@@ -43,6 +46,7 @@
                     <tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="aG7-6Q-gSS">
                         <rect key="frame" x="0.0" y="0.0" width="414" height="49"/>
                         <autoresizingMask key="autoresizingMask"/>
+                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                         <color key="barTintColor" white="0.91082411545973563" alpha="1" colorSpace="calibratedWhite"/>
                     </tabBar>
                 </tabBarController>
@@ -51,4 +55,9 @@
             <point key="canvasLocation" x="619" y="-202"/>
         </scene>
     </scenes>
+    <resources>
+        <systemColor name="systemBackgroundColor">
+            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+        </systemColor>
+    </resources>
 </document>

+ 38 - 0
AppBuilder/AppBuilder/FirstTabViewController.swift

@@ -103,6 +103,16 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         contentController.add(self, name: "shareText")
         contentController.add(self, name: "openGalleryiOS")
         contentController.add(self, name: "openChannel")
+        contentController.add(self, name: "openTabWebview3")
+        contentController.add(self, name: "openTabChat")
+        contentController.add(self, name: "openCallAudio")
+        contentController.add(self, name: "openCallVideo")
+        contentController.add(self, name: "openChatSmartbot")
+        contentController.add(self, name: "openPanicDialog")
+        contentController.add(self, name: "openContactCenter")
+        contentController.add(self, name: "openContactCenterChat")
+        contentController.add(self, name: "openContactCenterAudioCall")
+        contentController.add(self, name: "openContactCenterVideoCall")
         
         let source: String = "var meta = document.createElement('meta');" +
             "meta.name = 'viewport';" +
@@ -534,6 +544,34 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
             }
             alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
             self.present(alertController, animated: true)
+        } else if message.name == "openTabChat" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabChat()
+                }
+            }
+        } else if message.name == "openTabWebview3" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabWebview3()
+                }
+            }
+        } else if message.name == "openCallAudio" {
+            APIS.openAudioCall()
+        } else if message.name == "openCallVideo" {
+            APIS.openVideoCall()
+        } else if message.name == "openChatSmartbot" {
+            APIS.openSmartChatbot()
+        } else if message.name == "openPanicDialog" {
+            
+        } else if message.name == "openContactCenter" {
+            APIS.openContactCenter()
+        } else if message.name == "openContactCenterChat" {
+            APIS.openContactCenter(media: 0)
+        } else if message.name == "openContactCenterAudioCall" {
+            APIS.openContactCenter(media: 1)
+        } else if message.name == "openContactCenterVideoCall" {
+            APIS.openContactCenter(media: 2)
         }
     }
     

+ 20 - 0
AppBuilder/AppBuilder/PrefsUtil.swift

@@ -49,6 +49,26 @@ class PrefsUtil {
         return value
     }
     
+    static func getURLWV3() -> String? {
+        let value: String? = SecureUserDefaults.shared.value(forKey: "app_builder_url_webview_3")
+        return value
+    }
+    
+    static func getURLWV4() -> String? {
+        let value: String? = SecureUserDefaults.shared.value(forKey: "app_builder_url_webview_4")
+        return value
+    }
+    
+    static func getURLWV5() -> String? {
+        let value: String? = SecureUserDefaults.shared.value(forKey: "app_builder_url_webview_5")
+        return value
+    }
+    
+    static func getURLWV6() -> String? {
+        let value: String? = SecureUserDefaults.shared.value(forKey: "app_builder_url_webview_6")
+        return value
+    }
+    
     static func getURLStatusUpdate() -> String? {
         let value: String? = SecureUserDefaults.shared.value(forKey: "app_builder_url_status_update")
         return value

+ 4 - 2
AppBuilder/AppBuilder/SecondTabViewController.swift

@@ -1224,7 +1224,8 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 }
                 let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
                 editorPersonalVC.hidesBottomBarWhenPushed = true
-                editorPersonalVC.unique_l_pin = data.pin
+                editorPersonalVC.unique_l_pin = data.pin == User.getMyPin() ? data.fpin : data.pin
+                editorPersonalVC.referenceMessageId = (isFiltering && fillteredData.count != 0) ? data.messageId : ""
                 ViewController.hideDockedButton()
                 ViewController.removeMiddleButton()
                 navigationController?.show(editorPersonalVC, sender: nil)
@@ -1235,6 +1236,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
                 let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
                 editorGroupVC.hidesBottomBarWhenPushed = true
                 editorGroupVC.unique_l_pin = data.pin
+                editorGroupVC.referenceMessageId = (isFiltering && fillteredData.count != 0) ? data.messageId : ""
                 ViewController.hideDockedButton()
                 ViewController.removeMiddleButton()
                 navigationController?.show(editorGroupVC, sender: nil)
@@ -2659,7 +2661,7 @@ extension SecondTabViewController: UITableViewDelegate, UITableViewDataSource {
     }
     
     func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
-        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath)
+        guard let cell = collectionView.cellForItem(at: indexPath) else { return }
         let data = fillteredData[indexPath.row] as! Chat
         let imgData = data.image
         let vidData = data.video

+ 38 - 0
AppBuilder/AppBuilder/ThirdTabViewController.swift

@@ -103,6 +103,16 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         contentController.add(self, name: "shareText")
         contentController.add(self, name: "openGalleryiOS")
         contentController.add(self, name: "openChannel")
+        contentController.add(self, name: "openTabWebview3")
+        contentController.add(self, name: "openTabChat")
+        contentController.add(self, name: "openCallAudio")
+        contentController.add(self, name: "openCallVideo")
+        contentController.add(self, name: "openChatSmartbot")
+        contentController.add(self, name: "openPanicDialog")
+        contentController.add(self, name: "openContactCenter")
+        contentController.add(self, name: "openContactCenterChat")
+        contentController.add(self, name: "openContactCenterAudioCall")
+        contentController.add(self, name: "openContactCenterVideoCall")
         
         let source: String = "var meta = document.createElement('meta');" +
             "meta.name = 'viewport';" +
@@ -542,6 +552,34 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
             }
             alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
             self.present(alertController, animated: true)
+        } else if message.name == "openTabChat" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabChat()
+                }
+            }
+        } else if message.name == "openTabWebview3" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabWebview3()
+                }
+            }
+        } else if message.name == "openCallAudio" {
+            APIS.openAudioCall()
+        } else if message.name == "openCallVideo" {
+            APIS.openVideoCall()
+        } else if message.name == "openChatSmartbot" {
+            APIS.openSmartChatbot()
+        } else if message.name == "openPanicDialog" {
+            
+        } else if message.name == "openContactCenter" {
+            APIS.openContactCenter()
+        } else if message.name == "openContactCenterChat" {
+            APIS.openContactCenter(media: 0)
+        } else if message.name == "openContactCenterAudioCall" {
+            APIS.openContactCenter(media: 1)
+        } else if message.name == "openContactCenterVideoCall" {
+            APIS.openContactCenter(media: 2)
         }
     }
     

+ 105 - 9
AppBuilder/AppBuilder/ViewController.swift

@@ -18,6 +18,10 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
     let playerController = AVPlayerViewController()
     static var sURL = "https://www.google.com"
     static var tab3 = "0"
+    static var wv3 = "https://www.google.com"
+    static var wv4 = "https://www.google.com"
+    static var wv5 = "https://www.google.com"
+    static var wv6 = "https://www.google.com"
     public var isShow: Bool = false
     public static var chatButton = UIButton()
     public static var callButton = UIButton()
@@ -34,6 +38,10 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
     var secondTab : UINavigationController?
     var thirdTab : UINavigationController?
     var fourthTab : UINavigationController?
+    var wv3Tab : UINavigationController?
+    var wv4Tab : UINavigationController?
+    var wv5Tab : UINavigationController?
+    var wv6Tab : UINavigationController?
     var callTab : UINavigationController?
     var chatWATab : UINavigationController?
     var communityTab : UINavigationController?
@@ -69,21 +77,42 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
 
     override func viewDidLoad() {
         super.viewDidLoad()
+        navigationController?.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
         DispatchQueue.main.async { [self] in
-            while !Utils.getFinishInitPrefsr() || HTTPCookieStorage.shared.cookies(for: URL(string: Utils.getDomainOpr())!)!.count == 0 {
-                Thread.sleep(forTimeInterval: 1)
+            if (Utils.isHSAMode() || Utils.isMiddleMode()) {
+                if !Utils.getSetProfile() {
+                    self.willappear()
+                }
+                Nexilis.setSuccessSui() {
+                    DispatchQueue.main.async { [self] in
+                        if firstTab == nil {
+                            startView()
+                        }
+                    }
+                }
+            } else {
+                while !Utils.getFinishInitPrefsr() || (HTTPCookieStorage.shared.cookies(for: URL(string: Utils.getDomainOpr())!)!.count == 0 && Utils.getAppMode() == 3) {
+                    Thread.sleep(forTimeInterval: 1)
+                }
+                startView()
             }
-            startView()
         }
     }
     
     override func viewDidDisappear(_ animated: Bool) {
-        Nexilis.floatingButton.hideButton()
+        var stillOnRoot = true
+        if let nav = self.selectedViewController as? UINavigationController {
+            if nav.viewControllers.count > 1 {
+                stillOnRoot = false
+            }
+        }
+        if !stillOnRoot {
+            Nexilis.floatingButton.hideButton()
+        }
         Utils.randomizeBackground(view: self.navigationController?.view)
     }
     
     func startView() {
-        navigationController?.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
         Utils.addBackground(view: self.navigationController?.view)
         let topBorder = CALayer()
         topBorder.backgroundColor = UIColor.borderTabColor.cgColor
@@ -99,6 +128,10 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
         secondTab = UINavigationController(rootViewController: SecondTabViewController())
         thirdTab = UINavigationController(rootViewController: ThirdTabViewController())
         fourthTab = UINavigationController(rootViewController: FourthTabViewController())
+        wv3Tab = UINavigationController(rootViewController: WebView3())
+        wv4Tab = UINavigationController(rootViewController: WebView4())
+        wv5Tab = UINavigationController(rootViewController: WebView5())
+        wv6Tab = UINavigationController(rootViewController: WebView6())
         callTab = UINavigationController(rootViewController: CallLogVC())
         chatWATab = UINavigationController(rootViewController: ChatWALikeVC())
         communityTab = UINavigationController(rootViewController: CommunityList())
@@ -116,6 +149,10 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
         communityTab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_2_icon")!.withTintColor(.white) : UIImage(named: "tab_2_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_2_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
         secureFolderTab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_2_icon")!.withTintColor(.white) : UIImage(named: "tab_2_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_2_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
         statusUpdateTab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_1_icon")!.withTintColor(.white) : UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
+        wv3Tab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_1_icon")!.withTintColor(.white) : UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
+        wv4Tab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_1_icon")!.withTintColor(.white) : UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
+        wv5Tab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_1_icon")!.withTintColor(.white) : UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
+        wv6Tab?.tabBarItem = UITabBarItem(title: "", image: resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "tab_1_icon")!.withTintColor(.white) : UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(named: "tab_1_icon")!, targetSize: CGSize(width: 25, height: 25)).withRenderingMode(.alwaysOriginal).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor))
         
         var i = 0
         var j = 0
@@ -138,6 +175,14 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
                     tabs.append(secureFolderTab!)
                 case "6":
                     tabs.append(callTab!)
+                case "11":
+                    tabs.append(wv3Tab!)
+                case "12":
+                    tabs.append(wv4Tab!)
+                case "13":
+                    tabs.append(wv5Tab!)
+                case "14":
+                    tabs.append(wv6Tab!)
                 case "16":
                     tabs.append(communityTab!)
                 case "17":
@@ -244,6 +289,33 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
                     }
                 }
             }
+            if !Utils.getTab5Icon().isEmpty && tabs.count > 4 {
+                let urlString = "\(PrefsUtil.getURLBase())get_file_from_path?img=\(Utils.getTab5Icon())"
+                if let cachedImage = ImageCache.shared.image(forKey: urlString) {
+                    DispatchQueue.main.async() { [self] in
+                        if cpaasMode == PrefsUtil.CPAAS_MODE_DOCKED || cpaasMode == PrefsUtil.CPAAS_MODE_MIX {
+                            tabs[5].tabBarItem = UITabBarItem(title: "", image: resizeImage(image: cachedImage, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: cachedImage, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal))
+                        } else {
+                            tabs[4].tabBarItem = UITabBarItem(title: "", image: resizeImage(image: cachedImage, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: cachedImage, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal))
+                        }
+                    }
+                } else {
+                    Utils.fetchDataWithCookiesAndUserAgent(from: URL(string: urlString)!) { data, response, error in
+                        guard let data = data, error == nil else { return }
+                        // always update the UI from the main thread
+                        DispatchQueue.main.async() { [self] in
+                            if UIImage(data: data) != nil {
+                                if cpaasMode == PrefsUtil.CPAAS_MODE_DOCKED || cpaasMode == PrefsUtil.CPAAS_MODE_MIX {
+                                    tabs[4].tabBarItem = UITabBarItem(title: "", image: resizeImage(image: UIImage(data: data)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(data: data)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal))
+                                } else {
+                                    tabs[3].tabBarItem = UITabBarItem(title: "", image: resizeImage(image: UIImage(data: data)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal), selectedImage: resizeImage(image: UIImage(data: data)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal))
+                                }
+                                ImageCache.shared.save(image: UIImage(data: data)!, forKey: urlString)
+                            }
+                        }
+                    }
+                }
+            }
         }
         if((cpaasMode == PrefsUtil.CPAAS_MODE_DOCKED || cpaasMode == PrefsUtil.CPAAS_MODE_MIX)){
             createMidFloatingButton()
@@ -410,7 +482,31 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
 //    }
     
     override func viewDidAppear(_ animated: Bool) {
-        Nexilis.floatingButton.isHidden = false
+        var stillOnRoot = true
+        if let nav = self.selectedViewController as? UINavigationController {
+            if nav.viewControllers.count > 1 {
+                stillOnRoot = false
+            }
+        }
+        if self.presentedViewController == nil && stillOnRoot {
+            Nexilis.floatingButton.isHidden = false
+        }
+    }
+    
+    func openTabChat() {
+        self.selectedIndex = (self.viewControllers?.firstIndex(of: secondTab!))!
+        if indicatorImage != nil {
+            indicatorImage.removeFromSuperview()
+            addCustomViewAboveTabBarItem(at: selectedIndex, image: imageIndicator)
+        }
+    }
+    
+    func openTabWebview3() {
+        self.selectedIndex = (self.viewControllers?.firstIndex(of: wv3Tab!))!
+        if indicatorImage != nil {
+            indicatorImage.removeFromSuperview()
+            addCustomViewAboveTabBarItem(at: selectedIndex, image: imageIndicator)
+        }
     }
     
     func settingDelegate() {
@@ -682,7 +778,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
         let acceptTerm = PrefsUtil.getTerms()
         let enable_privacy_policy = PrefsUtil.getEnablePrivacyPolicy()
         if !acceptTerm {
-            if !Utils.getForceAnonymous() && !Utils.getSetProfile() {
+            if (Utils.isHSAMode() || Utils.isMiddleMode()) && !Utils.getSetProfile() {
                 Nexilis.showForceSignIn() {
                     self.showWelocomeView()
                 }
@@ -694,7 +790,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
             showPrivacyPolicyView()
             return
         } else if acceptTerm {
-            if !Utils.getForceAnonymous() && !Utils.getSetProfile() {
+            if (Utils.isHSAMode() || Utils.isMiddleMode()) && !Utils.getSetProfile() {
                 Nexilis.showForceSignIn()
                 return
             }
@@ -869,7 +965,7 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
         }
         welcomeVC.modalPresentationStyle = .fullScreen
         welcomeVC.modalTransitionStyle = .crossDissolve
-        UIApplication.shared.visibleViewController?.present(welcomeVC, animated: true)
+        UIApplication.shared.visibleViewController?.present(welcomeVC, animated: false)
     }
     
     func getHighResolutionAppIconName() -> String? {

+ 947 - 0
AppBuilder/AppBuilder/WebView3.swift

@@ -0,0 +1,947 @@
+//
+//  WebView3.swift
+//  AppBuilder
+//
+//  Created by Qindi on 31/10/25.
+//
+
+import UIKit
+import WebKit
+import NexilisLite
+import Speech
+import CommonCrypto
+
+class WebView3: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate, WKScriptMessageHandler, ImageVideoPickerDelegate {
+    
+    var webView: WKWebView!
+    var address = ""
+    private var lastContentOffset: CGFloat = 0
+    var progressView: UIProgressView!
+    
+    var isAllowSpeech = false
+    
+    let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "id"))
+
+    var recognitionRequest : SFSpeechAudioBufferRecognitionRequest?
+    var recognitionTask : SFSpeechRecognitionTask?
+    let audioEngine = AVAudioEngine()
+    var alertController = LibAlertController()
+    
+    public static var forceRefresh = true
+    public static var canLoadURL = false
+    public static var showModal = false
+    
+    var indexImageVideoWv = 0
+    var imageVideoPicker: ImageVideoPicker!
+    var blockedCertificate = ""
+    var allowedURLs = Set<String>()
+    var loadingURL = false
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        self.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
+
+        let configuration = WKWebViewConfiguration()
+        configuration.allowsInlineMediaPlayback = true
+        loadContentBlocker(into: configuration) { [self] in
+            DispatchQueue.main.async {
+                self.initializeWebView(with: configuration)
+            }
+        }
+    }
+    
+    func initializeWebView(with configuration: WKWebViewConfiguration) {
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(collapseDocked))
+        tapGesture.cancelsTouchesInView = false
+        tapGesture.delegate = self
+        let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
+        let finalUserAgent = "\(customUserAgent)"
+        configuration.applicationNameForUserAgent = finalUserAgent
+        webView = WKWebView(frame: .zero, configuration: configuration)
+        view.addSubview(webView)
+        webView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+        ])
+        webView.scrollView.addGestureRecognizer(tapGesture)
+        let refreshControl = UIRefreshControl()
+        refreshControl.addTarget(self, action: #selector(reloadWebView(_:)), for: .valueChanged)
+        webView.scrollView.addSubview(refreshControl)
+        webView.scrollView.delegate = self
+        webView.navigationDelegate = self
+        webView.allowsBackForwardNavigationGestures = true
+        
+        progressView = UIProgressView(progressViewStyle: .default)
+        progressView.sizeToFit()
+        progressView.tintColor = .systemBlue
+        view.addSubview(progressView)
+        
+        // Auto Layout for progress bar (stick to top)
+        progressView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            progressView.heightAnchor.constraint(equalToConstant: 4)
+        ])
+        
+        // Observe estimatedProgress
+        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+        
+        let contentController = self.webView.configuration.userContentController
+        contentController.add(self, name: "checkProfile")
+        contentController.add(self, name: "setIsProductModalOpen")
+        contentController.add(self, name: "toggleVoiceSearch")
+        contentController.add(self, name: "blockUser")
+        contentController.add(self, name: "showAlert")
+        contentController.add(self, name: "closeProfile")
+        contentController.add(self, name: "tabShowHide")
+        contentController.add(self, name: "shareText")
+        contentController.add(self, name: "openGalleryiOS")
+        contentController.add(self, name: "openChannel")
+        contentController.add(self, name: "openTabWebview3")
+        contentController.add(self, name: "openTabChat")
+        contentController.add(self, name: "openCallAudio")
+        contentController.add(self, name: "openCallVideo")
+        contentController.add(self, name: "openChatSmartbot")
+        contentController.add(self, name: "openPanicDialog")
+        contentController.add(self, name: "openContactCenter")
+        contentController.add(self, name: "openContactCenterChat")
+        contentController.add(self, name: "openContactCenterAudioCall")
+        contentController.add(self, name: "openContactCenterVideoCall")
+        
+        let source: String = "var meta = document.createElement('meta');" +
+            "meta.name = 'viewport';" +
+            "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" +
+            "var head = document.getElementsByTagName('head')[0];" +
+            "head.appendChild(meta);" +
+        "$('#header-layout').find('.col-8').removeClass('col-8').addClass('col');" +
+        "$('#header-layout').find('.col-4').removeClass('col-4').addClass('col');"
+        let script: WKUserScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
+        contentController.addUserScript(script)
+        
+        let cookieScript1 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[0])';"
+        let cookieScriptInjection1 = WKUserScript(source: cookieScript1, injectionTime: .atDocumentStart, forMainFrameOnly: false)
+        configuration.userContentController.addUserScript(cookieScriptInjection1)
+        
+        let cookieScript2 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[1])';"
+        let cookieScriptInjection2 = WKUserScript(source: cookieScript2, injectionTime: .atDocumentStart, forMainFrameOnly: false)
+        configuration.userContentController.addUserScript(cookieScriptInjection2)
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(onShowAC(notification:)), name: NSNotification.Name(rawValue: "onShowAC"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onRefreshWebView(notification:)), name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onResumeWebView(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil)
+        WebView3.canLoadURL = true
+        processURL()
+    }
+    
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progressView.progress = Float(webView.estimatedProgress)
+            
+            progressView.isHidden = webView.estimatedProgress >= 1
+        }
+    }
+    
+    func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
+        // Define ad-blocking rules directly in Swift as a string
+        let contentRules = PrefsUtil.contentRulesAds
+
+        WKContentRuleListStore.default().compileContentRuleList(forIdentifier: "AdBlocker", encodedContentRuleList: contentRules) { ruleList, error in
+            if let ruleList = ruleList {
+                config.userContentController.add(ruleList)
+            } else {
+                print("Failed to compile content rule list: \(error?.localizedDescription ?? "Unknown error")")
+            }
+            completion()
+        }
+    }
+    
+    func loadURLWithCookie(url: URL) {
+        var urlRequest = URLRequest(url: url)
+        let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
+        urlRequest.setValue(customUserAgent, forHTTPHeaderField: "User-Agent")
+        if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
+            for cookie in cookies {
+                webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
+            }
+            webView.load(urlRequest)
+        }
+    }
+    
+    func processURL() {
+        let me = User.getMyPin()
+        
+        var myURL : URL?
+        let lang: String = SecureUserDefaults.shared.value(forKey: "i18n_language") ?? "en"
+        var intLang = 0
+        if lang == "id" {
+            intLang = 1
+        }
+        if PrefsUtil.getURLWV3() != nil {
+            ViewController.wv3 = PrefsUtil.getURLWV3()!
+        }
+        switch(ViewController.wv3){
+        case "0":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-main-only?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "1":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab3-main-only?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "2":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-main?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "3":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab3-commerce?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "4":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-video?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        default:
+            if(!ViewController.wv3.isEmpty){
+                if(ViewController.wv3.lowercased().contains("https://") || ViewController.wv3.lowercased().contains("http://")){
+                    address = ViewController.wv3
+                    myURL = URL(string: address)
+                }
+                else {
+                    if ViewController.wv3.contains("nexilis/pages"){
+                        address = "\(PrefsUtil.getURLBase())\(ViewController.wv3)?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+                    } else {
+                        address = "https://\(ViewController.wv3)"
+                    }
+                    myURL = URL(string: address)
+                }
+            }
+        }
+        if let u = myURL {
+            webView.evaluateJavaScript("{window.localStorage.setItem('currentTab','\(ViewController.wv3)')}")
+            if WebView3.forceRefresh {
+                loadURLWithCookie(url: u)
+            } else {
+                webView.evaluateJavaScript("if(resumeAll){resumeAll();}")
+            }
+            WebView3.forceRefresh = false
+        }
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        if WebView3.canLoadURL {
+            processURL()
+        }
+        navigationController?.setNavigationBarHidden(true, animated: false)
+    }
+    
+    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+        if Utils.getIsLoadThemeFromOther() {
+            self.webView.evaluateJavaScript("{window.localStorage.setItem('mobileConfiguration','"+Utils.getMyTheme()+"')}")
+        }
+    }
+    
+    override func viewDidAppear(_ animated: Bool) {
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
+            var viewController = UIApplication.shared.windows.first!.rootViewController
+            if let nc = viewController as? UINavigationController {
+                viewController = nc.viewControllers.first
+            }
+            if ViewController.middleButton.isHidden {
+                ViewController.isExpandButton = false
+                if let viewController = viewController as? ViewController {
+                    if viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = false
+                        ViewController.middleButton.isHidden = false
+                        ViewController.alwaysHideButton = false
+                    }
+                    if ViewController.middleButton.isHidden && PrefsUtil.getCpaasMode() == PrefsUtil.CPAAS_MODE_DOCKED {
+                        viewController.showMiddleButtonAfterPush()
+                    }
+                }
+            } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+                DispatchQueue.main.async {
+                    if let viewController = viewController as? ViewController {
+                        if viewController.tabBar.isHidden {
+                            viewController.tabBar.isHidden = false
+                            ViewController.alwaysHideButton = false
+                        }
+                    }
+                }
+            }
+        })
+    }
+    
+    @objc func onShowAC(notification: NSNotification) {
+        self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+        view.endEditing(true)
+        resignFirstResponder()
+    }
+    
+    @objc func onRefreshWebView(notification: NSNotification) {
+        WebView3.forceRefresh = true
+        WebView3.showModal = false
+    }
+    
+    @objc func onResumeWebView(notification: NSNotification) {
+        Nexilis.reloadCookies(webView: webView)
+    }
+    
+    override func viewWillDisappear(_ animated: Bool) {
+        if self.webView != nil {
+            if self.webView.scrollView.contentOffset.y < 0 { // Move tableView to top
+                self.webView.scrollView.setContentOffset(CGPoint.zero, animated: true)
+            }
+            self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+            self.webView.evaluateJavaScript("hideAddToCart();")
+        }
+    }
+    
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        if (self.lastContentOffset > scrollView.contentOffset.y && scrollView.contentOffset.y < (scrollView.contentSize.height - scrollView.frame.size.height)) {
+            showTabBar();
+        }
+        else if (self.lastContentOffset != 0 && self.lastContentOffset < scrollView.contentOffset.y && self.lastContentOffset >= 0) {
+            hideTabBar();
+        }
+        self.lastContentOffset = scrollView.contentOffset.y
+        self.collapseDocked()
+    }
+    
+    @objc func collapseDocked() {
+        if ViewController.isExpandButton {
+            ViewController.expandButton()
+        }
+    }
+
+    @objc func reloadWebView(_ sender: UIRefreshControl) {
+//        WebView3.forceRefresh = true
+//        viewWillAppear(false)
+        webView.reload()
+        ViewController.alwaysHideButton = false
+        showTabBar()
+        sender.endRefreshing()
+    }
+    
+    func hideTabBar() {
+        if UIApplication.shared.windows.first == nil {
+            return
+        }
+        var viewController = UIApplication.shared.windows.first!.rootViewController
+        if let nc = viewController as? UINavigationController {
+            viewController = nc.viewControllers.first
+        }
+        if ViewController.middleButton.isDescendant(of: viewController!.view) {
+            DispatchQueue.main.async {
+                if ViewController.isExpandButton {
+                    ViewController.expandButton()
+                }
+                ViewController.hideDockedButton()
+                if let viewController = viewController as? ViewController {
+                    viewController.tabBar.isHidden = true
+                }
+                ViewController.removeMiddleButton()
+            }
+        } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+            DispatchQueue.main.async {
+                if let viewController = viewController as? ViewController {
+                    if !viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = true
+                    }
+                }
+            }
+        }
+    }
+
+    func showTabBar() {
+        if(ViewController.alwaysHideButton){
+            return
+        }
+        var viewController = UIApplication.shared.windows.first!.rootViewController
+        if let nc = viewController as? UINavigationController {
+            viewController = nc.viewControllers.first
+        }
+        if ViewController.middleButton.isHidden {
+            if let viewController = viewController as? ViewController {
+                viewController.tabBar.isHidden = false
+                ViewController.middleButton.isHidden = false
+            }
+        } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+            DispatchQueue.main.async {
+                if let viewController = viewController as? ViewController {
+                    if viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = false
+                    }
+                }
+            }
+        }
+    }
+
+    func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
+        scrollView.pinchGestureRecognizer?.isEnabled = false
+    }
+
+    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        if message.name == "checkProfile" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? String else {
+                return
+            }
+            if ViewController.checkIsChangePerson() {
+                if param2 == "like" {
+                    self.webView.evaluateJavaScript("likeProduct('\(param1)',1,true);")
+                } else if param2 == "comment" {
+                    self.webView.evaluateJavaScript("openComment('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
+                } else if param2 == "report_user" {
+                    self.webView.evaluateJavaScript("reportUser('\(param1)',true);")
+                } else if param2 == "report_content" {
+                    self.webView.evaluateJavaScript("reportContent('\(param1.split(separator: "|")[0])','\(param1.split(separator: "|")[1])',true);")
+                } else if param2 == "block_user" {
+                    self.webView.evaluateJavaScript("blockUser('\(param1)',true);")
+                } else if param2 == "follow_user" {
+                    self.webView.evaluateJavaScript("followUser('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
+                } else if param2 == "homepage" || param2 == "gif" {
+                    self.webView.evaluateJavaScript("window.location.href = '\(param1)';")
+                } else if param2 == "block_content" {
+                    self.webView.evaluateJavaScript("blockContent('\(param1)',true);")
+                } else {
+                    self.webView.evaluateJavaScript("openNewPost(true);")
+                }
+            } else {
+                self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+            }
+        } else if message.name == "setIsProductModalOpen" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Bool else {
+                return
+            }
+            if param1 {
+                if self.webView.scrollView.contentOffset.y < 0 { // Move tableView to top
+                    self.webView.scrollView.setContentOffset(CGPoint.zero, animated: true)
+                }
+            }
+            WebView3.showModal = param1
+        } else if message.name == "toggleVoiceSearch" {
+            if !isAllowSpeech {
+                setupSpeech()
+            } else {
+                runVoice()
+            }
+        } else if message.name == "blockUser" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? Bool else {
+                return
+            }
+            if param2 {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "1"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            } else {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "0"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            }
+        } else if message.name == "showAlert" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String else {
+                return
+            }
+            self.view.makeToast(param1, duration: 3)
+        } else if message.name == "blockUser" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? Bool else {
+                return
+            }
+            if param2 {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "1"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            } else {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "0"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            }
+        } else if message.name == "tabShowHide" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Bool else {
+                return
+            }
+            if param1 {
+                ViewController.alwaysHideButton = false
+                showTabBar()
+            } else {
+                if self.viewIfLoaded?.window != nil {
+                    ViewController.alwaysHideButton = true
+                    hideTabBar()
+                }
+            }
+        } else if message.name == "shareText" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String else {
+                return
+            }
+            if loadingURL {
+                return
+            }
+            let activityViewController = UIActivityViewController(activityItems: [param1], applicationActivities: nil)
+            self.present(activityViewController, animated: true, completion: nil)
+        } else if message.name == "openGalleryiOS" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Int else {
+                return
+            }
+            indexImageVideoWv = param1
+            let alertController = LibAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
+            
+            if let action = self.actionImageVideo(for: "image", title: "Choose Photo".localized()) {
+                alertController.addAction(action)
+            }
+            if let action = self.actionImageVideo(for: "video", title: "Choose Video".localized()) {
+                alertController.addAction(action)
+            }
+            alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
+            self.present(alertController, animated: true)
+        } else if message.name == "openTabChat" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabChat()
+                }
+            }
+        } else if message.name == "openTabWebview3" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabWebview3()
+                }
+            }
+        } else if message.name == "openCallAudio" {
+            APIS.openAudioCall()
+        } else if message.name == "openCallVideo" {
+            APIS.openVideoCall()
+        } else if message.name == "openChatSmartbot" {
+            APIS.openSmartChatbot()
+        } else if message.name == "openPanicDialog" {
+            
+        } else if message.name == "openContactCenter" {
+            APIS.openContactCenter()
+        } else if message.name == "openContactCenterChat" {
+            APIS.openContactCenter(media: 0)
+        } else if message.name == "openContactCenterAudioCall" {
+            APIS.openContactCenter(media: 1)
+        } else if message.name == "openContactCenterVideoCall" {
+            APIS.openContactCenter(media: 2)
+        }
+    }
+    
+    private func actionImageVideo(for type: String, title: String) -> UIAlertAction? {
+        return UIAlertAction(title: title, style: .default) { [unowned self] _ in
+            switch type {
+            case "image":
+                imageVideoPicker.present(source: .imageAlbum)
+            case "video":
+                imageVideoPicker.present(source: .videoAlbum)
+            default:
+                imageVideoPicker.present(source: .imageAlbum)
+            }
+        }
+    }
+    
+    func didSelect(imagevideo: Any?) {
+        if imagevideo != nil {
+            let imageData = imagevideo! as! [UIImagePickerController.InfoKey : Any]
+            if (imageData[.mediaType] as! String == "public.image") {
+                let compressedImage = (imageData[.originalImage] as! UIImage).pngData()!
+                let base64String = compressedImage.base64EncodedString()
+                let base64ToWeb = "data:image/jpeg;base64,\(base64String)"
+                webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
+                    if let error = error {
+                        print("Error executing JavaScript: \(error)")
+                    }
+                }
+            } else {
+                guard var dataVideo = try? Data(contentsOf: imageData[.mediaURL] as! URL) else {
+                    return
+                }
+                let sizeOfVideo = Double(dataVideo.count / 1048576)
+                if (sizeOfVideo > 10.0) {
+                    let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
+                    compressVideo(inputURL: imageData[.mediaURL] as! URL,
+                                  outputURL: compressedURL) { exportSession in
+                        guard let session = exportSession else {
+                            return
+                        }
+                        
+                        switch session.status {
+                        case .unknown:
+                            break
+                        case .waiting:
+                            break
+                        case .exporting:
+                            break
+                        case .completed:
+                            guard let compressedData = try? Data(contentsOf: compressedURL) else {
+                                return
+                            }
+                            dataVideo = compressedData
+                        case .failed:
+                            break
+                        case .cancelled:
+                            break
+                        @unknown default:
+                            break
+                        }
+                    }
+                }
+                let base64String = dataVideo.base64EncodedString()
+                let base64ToWeb = "data:video/mp4;base64,\(base64String)"
+                webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
+                    if let error = error {
+                        print("Error executing JavaScript: \(error)")
+                    }
+                }
+            }
+        }
+    }
+    
+    func compressVideo(inputURL: URL,
+                       outputURL: URL,
+                       handler:@escaping (_ exportSession: AVAssetExportSession?) -> Void) {
+        let urlAsset = AVURLAsset(url: inputURL, options: nil)
+        guard let exportSession = AVAssetExportSession(asset: urlAsset,
+                                                       presetName: AVAssetExportPresetMediumQuality) else {
+            handler(nil)
+            
+            return
+        }
+        
+        exportSession.outputURL = outputURL
+        exportSession.outputFileType = .mp4
+        exportSession.exportAsynchronously {
+            handler(exportSession)
+        }
+    }
+    
+    func setupSpeech() {
+
+        self.speechRecognizer?.delegate = self
+
+        SFSpeechRecognizer.requestAuthorization { (authStatus) in
+
+            var isButtonEnabled = false
+
+            switch authStatus {
+            case .authorized:
+                isButtonEnabled = true
+
+            case .denied:
+                isButtonEnabled = false
+                //print("User denied access to speech recognition")
+
+            case .restricted:
+                isButtonEnabled = false
+                //print("Speech recognition restricted on this device")
+
+            case .notDetermined:
+                isButtonEnabled = false
+                //print("Speech recognition not yet authorized")
+            @unknown default:
+                isButtonEnabled = false
+            }
+
+            OperationQueue.main.addOperation() {
+                self.isAllowSpeech = isButtonEnabled
+                if isButtonEnabled {
+                    SecureUserDefaults.shared.set(isButtonEnabled, forKey: "allowSpeech")
+                    self.runVoice()
+                }
+            }
+        }
+    }
+    
+    func runVoice() {
+        if !audioEngine.isRunning {
+            alertController = LibAlertController(title: "Start Recording".localized(), message: "Say something, I'm listening!".localized(), preferredStyle: .alert)
+            self.present(alertController, animated: true)
+            self.webView.evaluateJavaScript("toggleVoiceButton(true)")
+            self.startRecording()
+        }
+    }
+    
+    func startRecording() {
+
+        // Clear all previous session data and cancel task
+        if recognitionTask != nil {
+            recognitionTask?.cancel()
+            recognitionTask = nil
+        }
+
+        // Create instance of audio session to record voice
+        let audioSession = AVAudioSession.sharedInstance()
+        do {
+            try audioSession.setCategory(AVAudioSession.Category.record, mode: .default, options: [])
+            try audioSession.setMode(AVAudioSession.Mode.measurement)
+            try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
+        } catch {
+            //print("audioSession properties weren't set because of an error.")
+        }
+
+        self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
+
+        let inputNode = audioEngine.inputNode
+
+        guard let recognitionRequest = recognitionRequest else {
+            fatalError("Unable to create an SFSpeechAudioBufferRecognitionRequest object")
+        }
+
+        recognitionRequest.shouldReportPartialResults = true
+
+        self.recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in
+
+            var isFinal = false
+            var text = ""
+
+            if result != nil {
+                text = result?.bestTranscription.formattedString ?? ""
+                isFinal = (result?.isFinal)!
+                self.alertController.dismiss(animated: true)
+                self.audioEngine.stop()
+                self.recognitionRequest?.endAudio()
+            } else {
+                self.alertController.dismiss(animated: true)
+            }
+
+            if error != nil || isFinal {
+                if error == nil {
+                    self.webView.evaluateJavaScript("toggleVoiceButton(false)")
+                    self.webView.evaluateJavaScript("submitVoiceSearch('\(text)')")
+                } else {
+                    self.audioEngine.stop()
+                    self.recognitionRequest?.endAudio()
+                }
+                inputNode.removeTap(onBus: 0)
+
+                self.recognitionRequest = nil
+                self.recognitionTask = nil
+
+                self.isAllowSpeech = true
+            }
+        })
+
+        let recordingFormat = inputNode.outputFormat(forBus: 0)
+        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
+            self.recognitionRequest?.append(buffer)
+        }
+
+        self.audioEngine.prepare()
+
+        do {
+            try self.audioEngine.start()
+        } catch {
+            //print("audioEngine couldn't start because of an error.")
+        }
+    }
+    
+    func isUsingMyWebview() -> Bool{
+        return PrefsUtil.getURLWV3() == "0" || PrefsUtil.getURLWV3() == "1" || PrefsUtil.getURLWV3() == "2" || PrefsUtil.getURLWV3() == "3" || PrefsUtil.getURLWV3() == "4"
+    }
+
+}
+
+extension WebView3: SFSpeechRecognizerDelegate {
+
+    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
+        if available {
+            self.isAllowSpeech = true
+        } else {
+            self.isAllowSpeech = false
+        }
+    }
+}
+
+extension WebView3: WKUIDelegate, WKNavigationDelegate {
+    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
+        guard let url = navigationAction.request.url else {
+            decisionHandler(.cancel)
+            return
+        }
+        
+        if let scheme = url.scheme?.lowercased(), scheme.hasPrefix("itms") || scheme == "mailto" || scheme == "tel" {
+            DispatchQueue.main.async {
+                UIApplication.shared.open(url, options: [:], completionHandler: nil)
+            }
+            decisionHandler(.cancel)
+            return
+        }
+
+        guard navigationAction.targetFrame?.isMainFrame == true else {
+            decisionHandler(.allow)
+            return
+        }
+
+        if loadingURL {
+            decisionHandler(.cancel)
+            return
+        }
+
+        loadingURL = true
+
+        if allowedURLs.contains(url.absoluteString) {
+            loadingURL = false
+            decisionHandler(.allow)
+            return
+        }
+        
+        validateSSLCertificate(url: url) { [weak self] isValid in
+            guard let self = self else {
+                decisionHandler(.cancel)
+                return
+            }
+
+            DispatchQueue.main.async {
+                if isValid {
+                    self.allowedURLs.insert(url.absoluteString)
+                    self.loadingURL = false
+                    decisionHandler(.allow)
+                } else {
+                    let host = url.host ?? ""
+                    var messageText = "You're about to access a website that is not currently trusted by your Nexilis Browser. This website's security certificate is not recognized.\n\nDo you wish to proceed to <<domain>> and trust the website's security certificate?\n\nNote: Adding a website to the trusted list may increase your risk of security vulnerability".localized()
+                    messageText = messageText.replacingOccurrences(of: "<<domain>>", with: host)
+
+                    let alert = UIAlertController(title: "Warning Unknown Url!".localized(),
+                                                  message: messageText,
+                                                  preferredStyle: .alert)
+
+                    alert.addAction(UIAlertAction(title: "Yes", style: .default) { _ in
+                        let storedCertificate = Utils.getCertificatePinningWebview()
+                        if let jsonData = storedCertificate.data(using: .utf8),
+                           let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
+                            var certJson = certJson
+                            certJson[host] = self.blockedCertificate
+                            if let jsonData = try? JSONSerialization.data(withJSONObject: certJson, options: []),
+                               let jsonString = String(data: jsonData, encoding: .utf8) {
+                                Utils.setCertificatePinningWebview(value: jsonString)
+                            }
+                        }
+
+                        self.allowedURLs.insert(url.absoluteString)
+                        self.loadingURL = false
+                        decisionHandler(.allow)
+                    })
+
+                    alert.addAction(UIAlertAction(title: "No", style: .cancel) { _ in
+                        self.loadingURL = false
+                        decisionHandler(.cancel)
+                    })
+
+                    if self.presentedViewController == nil {
+                        self.present(alert, animated: true, completion: nil)
+                    } else {
+                        self.loadingURL = false
+                        decisionHandler(.cancel)
+                    }
+                }
+            }
+        }
+    }
+    
+    private func validateSSLCertificate(url: URL, completion: @escaping (Bool) -> Void) {
+        let session = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil)
+        let request = URLRequest(url: url)
+
+        let task = session.dataTask(with: request) { _, response, error in
+            if let error = error {
+                completion(false)
+                return
+            }
+            completion(true)
+        }
+        task.resume()
+    }
+}
+
+extension WebView3: URLSessionDelegate {
+    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+        guard let serverTrust = challenge.protectionSpace.serverTrust else {
+            completionHandler(.cancelAuthenticationChallenge, nil)
+            return
+        }
+        if let publicKeyHash = extractPublicKeyHash(from: serverTrust) {
+            let domain = challenge.protectionSpace.host
+            let storedCertificate = Utils.getCertificatePinningWebview()
+            if let jsonData = storedCertificate.data(using: .utf8),
+               let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
+                if publicKeyHash == certJson[domain] {
+                    completionHandler(.useCredential, URLCredential(trust: serverTrust))
+                } else {
+                    blockedCertificate = publicKeyHash
+                    completionHandler(.cancelAuthenticationChallenge, nil)
+                }
+            }
+        } else {
+            completionHandler(.cancelAuthenticationChallenge, nil)
+        }
+    }
+    
+    func extractPublicKeyHash(from serverTrust: SecTrust) -> String? {
+        guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else { return nil }
+        guard let publicKey = SecCertificateCopyKey(certificate) else { return nil }
+        
+        var error: Unmanaged<CFError>?
+        guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
+            return nil
+        }
+        
+        // Compute SHA-256 hash
+        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
+        publicKeyData.withUnsafeBytes {
+            _ = CC_SHA256($0.baseAddress, CC_LONG(publicKeyData.count), &hash)
+        }
+        
+        let hashData = Data(hash)
+        let base64Hash = hashData.base64EncodedString()
+        
+        return base64Hash
+    }
+}

+ 947 - 0
AppBuilder/AppBuilder/WebView4.swift

@@ -0,0 +1,947 @@
+//
+//  WebView4.swift
+//  AppBuilder
+//
+//  Created by Qindi on 31/10/25.
+//
+
+import UIKit
+import WebKit
+import NexilisLite
+import Speech
+import CommonCrypto
+
+class WebView4: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate, WKScriptMessageHandler, ImageVideoPickerDelegate {
+    
+    var webView: WKWebView!
+    var address = ""
+    private var lastContentOffset: CGFloat = 0
+    var progressView: UIProgressView!
+    
+    var isAllowSpeech = false
+    
+    let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "id"))
+
+    var recognitionRequest : SFSpeechAudioBufferRecognitionRequest?
+    var recognitionTask : SFSpeechRecognitionTask?
+    let audioEngine = AVAudioEngine()
+    var alertController = LibAlertController()
+    
+    public static var forceRefresh = true
+    public static var canLoadURL = false
+    public static var showModal = false
+    
+    var indexImageVideoWv = 0
+    var imageVideoPicker: ImageVideoPicker!
+    var blockedCertificate = ""
+    var allowedURLs = Set<String>()
+    var loadingURL = false
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        self.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
+
+        let configuration = WKWebViewConfiguration()
+        configuration.allowsInlineMediaPlayback = true
+        loadContentBlocker(into: configuration) { [self] in
+            DispatchQueue.main.async {
+                self.initializeWebView(with: configuration)
+            }
+        }
+    }
+    
+    func initializeWebView(with configuration: WKWebViewConfiguration) {
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(collapseDocked))
+        tapGesture.cancelsTouchesInView = false
+        tapGesture.delegate = self
+        let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
+        let finalUserAgent = "\(customUserAgent)"
+        configuration.applicationNameForUserAgent = finalUserAgent
+        webView = WKWebView(frame: .zero, configuration: configuration)
+        view.addSubview(webView)
+        webView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+        ])
+        webView.scrollView.addGestureRecognizer(tapGesture)
+        let refreshControl = UIRefreshControl()
+        refreshControl.addTarget(self, action: #selector(reloadWebView(_:)), for: .valueChanged)
+        webView.scrollView.addSubview(refreshControl)
+        webView.scrollView.delegate = self
+        webView.navigationDelegate = self
+        webView.allowsBackForwardNavigationGestures = true
+        
+        progressView = UIProgressView(progressViewStyle: .default)
+        progressView.sizeToFit()
+        progressView.tintColor = .systemBlue
+        view.addSubview(progressView)
+        
+        // Auto Layout for progress bar (stick to top)
+        progressView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            progressView.heightAnchor.constraint(equalToConstant: 4)
+        ])
+        
+        // Observe estimatedProgress
+        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+        
+        let contentController = self.webView.configuration.userContentController
+        contentController.add(self, name: "checkProfile")
+        contentController.add(self, name: "setIsProductModalOpen")
+        contentController.add(self, name: "toggleVoiceSearch")
+        contentController.add(self, name: "blockUser")
+        contentController.add(self, name: "showAlert")
+        contentController.add(self, name: "closeProfile")
+        contentController.add(self, name: "tabShowHide")
+        contentController.add(self, name: "shareText")
+        contentController.add(self, name: "openGalleryiOS")
+        contentController.add(self, name: "openChannel")
+        contentController.add(self, name: "openTabWebview3")
+        contentController.add(self, name: "openTabChat")
+        contentController.add(self, name: "openCallAudio")
+        contentController.add(self, name: "openCallVideo")
+        contentController.add(self, name: "openChatSmartbot")
+        contentController.add(self, name: "openPanicDialog")
+        contentController.add(self, name: "openContactCenter")
+        contentController.add(self, name: "openContactCenterChat")
+        contentController.add(self, name: "openContactCenterAudioCall")
+        contentController.add(self, name: "openContactCenterVideoCall")
+        
+        let source: String = "var meta = document.createElement('meta');" +
+            "meta.name = 'viewport';" +
+            "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" +
+            "var head = document.getElementsByTagName('head')[0];" +
+            "head.appendChild(meta);" +
+        "$('#header-layout').find('.col-8').removeClass('col-8').addClass('col');" +
+        "$('#header-layout').find('.col-4').removeClass('col-4').addClass('col');"
+        let script: WKUserScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
+        contentController.addUserScript(script)
+        
+        let cookieScript1 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[0])';"
+        let cookieScriptInjection1 = WKUserScript(source: cookieScript1, injectionTime: .atDocumentStart, forMainFrameOnly: false)
+        configuration.userContentController.addUserScript(cookieScriptInjection1)
+        
+        let cookieScript2 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[1])';"
+        let cookieScriptInjection2 = WKUserScript(source: cookieScript2, injectionTime: .atDocumentStart, forMainFrameOnly: false)
+        configuration.userContentController.addUserScript(cookieScriptInjection2)
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(onShowAC(notification:)), name: NSNotification.Name(rawValue: "onShowAC"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onRefreshWebView(notification:)), name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onResumeWebView(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil)
+        WebView4.canLoadURL = true
+        processURL()
+    }
+    
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progressView.progress = Float(webView.estimatedProgress)
+            
+            progressView.isHidden = webView.estimatedProgress >= 1
+        }
+    }
+    
+    func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
+        // Define ad-blocking rules directly in Swift as a string
+        let contentRules = PrefsUtil.contentRulesAds
+
+        WKContentRuleListStore.default().compileContentRuleList(forIdentifier: "AdBlocker", encodedContentRuleList: contentRules) { ruleList, error in
+            if let ruleList = ruleList {
+                config.userContentController.add(ruleList)
+            } else {
+                print("Failed to compile content rule list: \(error?.localizedDescription ?? "Unknown error")")
+            }
+            completion()
+        }
+    }
+    
+    func loadURLWithCookie(url: URL) {
+        var urlRequest = URLRequest(url: url)
+        let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
+        urlRequest.setValue(customUserAgent, forHTTPHeaderField: "User-Agent")
+        if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
+            for cookie in cookies {
+                webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
+            }
+            webView.load(urlRequest)
+        }
+    }
+    
+    func processURL() {
+        let me = User.getMyPin()
+        
+        var myURL : URL?
+        let lang: String = SecureUserDefaults.shared.value(forKey: "i18n_language") ?? "en"
+        var intLang = 0
+        if lang == "id" {
+            intLang = 1
+        }
+        if PrefsUtil.getURLWV4() != nil {
+            ViewController.wv4 = PrefsUtil.getURLWV4()!
+        }
+        switch(ViewController.wv4){
+        case "0":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-main-only?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "1":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab3-main-only?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "2":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-main?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "3":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab3-commerce?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "4":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-video?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        default:
+            if(!ViewController.wv4.isEmpty){
+                if(ViewController.wv4.lowercased().contains("https://") || ViewController.wv4.lowercased().contains("http://")){
+                    address = ViewController.wv4
+                    myURL = URL(string: address)
+                }
+                else {
+                    if ViewController.wv4.contains("nexilis/pages"){
+                        address = "\(PrefsUtil.getURLBase())\(ViewController.wv4)?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+                    } else {
+                        address = "https://\(ViewController.wv4)"
+                    }
+                    myURL = URL(string: address)
+                }
+            }
+        }
+        if let u = myURL {
+            webView.evaluateJavaScript("{window.localStorage.setItem('currentTab','\(ViewController.wv4)')}")
+            if WebView4.forceRefresh {
+                loadURLWithCookie(url: u)
+            } else {
+                webView.evaluateJavaScript("if(resumeAll){resumeAll();}")
+            }
+            WebView4.forceRefresh = false
+        }
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        if WebView4.canLoadURL {
+            processURL()
+        }
+        navigationController?.setNavigationBarHidden(true, animated: false)
+    }
+    
+    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+        if Utils.getIsLoadThemeFromOther() {
+            self.webView.evaluateJavaScript("{window.localStorage.setItem('mobileConfiguration','"+Utils.getMyTheme()+"')}")
+        }
+    }
+    
+    override func viewDidAppear(_ animated: Bool) {
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
+            var viewController = UIApplication.shared.windows.first!.rootViewController
+            if let nc = viewController as? UINavigationController {
+                viewController = nc.viewControllers.first
+            }
+            if ViewController.middleButton.isHidden {
+                ViewController.isExpandButton = false
+                if let viewController = viewController as? ViewController {
+                    if viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = false
+                        ViewController.middleButton.isHidden = false
+                        ViewController.alwaysHideButton = false
+                    }
+                    if ViewController.middleButton.isHidden && PrefsUtil.getCpaasMode() == PrefsUtil.CPAAS_MODE_DOCKED {
+                        viewController.showMiddleButtonAfterPush()
+                    }
+                }
+            } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+                DispatchQueue.main.async {
+                    if let viewController = viewController as? ViewController {
+                        if viewController.tabBar.isHidden {
+                            viewController.tabBar.isHidden = false
+                            ViewController.alwaysHideButton = false
+                        }
+                    }
+                }
+            }
+        })
+    }
+    
+    @objc func onShowAC(notification: NSNotification) {
+        self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+        view.endEditing(true)
+        resignFirstResponder()
+    }
+    
+    @objc func onRefreshWebView(notification: NSNotification) {
+        WebView4.forceRefresh = true
+        WebView4.showModal = false
+    }
+    
+    @objc func onResumeWebView(notification: NSNotification) {
+        Nexilis.reloadCookies(webView: webView)
+    }
+    
+    override func viewWillDisappear(_ animated: Bool) {
+        if self.webView != nil {
+            if self.webView.scrollView.contentOffset.y < 0 { // Move tableView to top
+                self.webView.scrollView.setContentOffset(CGPoint.zero, animated: true)
+            }
+            self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+            self.webView.evaluateJavaScript("hideAddToCart();")
+        }
+    }
+    
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        if (self.lastContentOffset > scrollView.contentOffset.y && scrollView.contentOffset.y < (scrollView.contentSize.height - scrollView.frame.size.height)) {
+            showTabBar();
+        }
+        else if (self.lastContentOffset != 0 && self.lastContentOffset < scrollView.contentOffset.y && self.lastContentOffset >= 0) {
+            hideTabBar();
+        }
+        self.lastContentOffset = scrollView.contentOffset.y
+        self.collapseDocked()
+    }
+    
+    @objc func collapseDocked() {
+        if ViewController.isExpandButton {
+            ViewController.expandButton()
+        }
+    }
+
+    @objc func reloadWebView(_ sender: UIRefreshControl) {
+//        WebView4.forceRefresh = true
+//        viewWillAppear(false)
+        webView.reload()
+        ViewController.alwaysHideButton = false
+        showTabBar()
+        sender.endRefreshing()
+    }
+    
+    func hideTabBar() {
+        if UIApplication.shared.windows.first == nil {
+            return
+        }
+        var viewController = UIApplication.shared.windows.first!.rootViewController
+        if let nc = viewController as? UINavigationController {
+            viewController = nc.viewControllers.first
+        }
+        if ViewController.middleButton.isDescendant(of: viewController!.view) {
+            DispatchQueue.main.async {
+                if ViewController.isExpandButton {
+                    ViewController.expandButton()
+                }
+                ViewController.hideDockedButton()
+                if let viewController = viewController as? ViewController {
+                    viewController.tabBar.isHidden = true
+                }
+                ViewController.removeMiddleButton()
+            }
+        } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+            DispatchQueue.main.async {
+                if let viewController = viewController as? ViewController {
+                    if !viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = true
+                    }
+                }
+            }
+        }
+    }
+
+    func showTabBar() {
+        if(ViewController.alwaysHideButton){
+            return
+        }
+        var viewController = UIApplication.shared.windows.first!.rootViewController
+        if let nc = viewController as? UINavigationController {
+            viewController = nc.viewControllers.first
+        }
+        if ViewController.middleButton.isHidden {
+            if let viewController = viewController as? ViewController {
+                viewController.tabBar.isHidden = false
+                ViewController.middleButton.isHidden = false
+            }
+        } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+            DispatchQueue.main.async {
+                if let viewController = viewController as? ViewController {
+                    if viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = false
+                    }
+                }
+            }
+        }
+    }
+
+    func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
+        scrollView.pinchGestureRecognizer?.isEnabled = false
+    }
+
+    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        if message.name == "checkProfile" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? String else {
+                return
+            }
+            if ViewController.checkIsChangePerson() {
+                if param2 == "like" {
+                    self.webView.evaluateJavaScript("likeProduct('\(param1)',1,true);")
+                } else if param2 == "comment" {
+                    self.webView.evaluateJavaScript("openComment('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
+                } else if param2 == "report_user" {
+                    self.webView.evaluateJavaScript("reportUser('\(param1)',true);")
+                } else if param2 == "report_content" {
+                    self.webView.evaluateJavaScript("reportContent('\(param1.split(separator: "|")[0])','\(param1.split(separator: "|")[1])',true);")
+                } else if param2 == "block_user" {
+                    self.webView.evaluateJavaScript("blockUser('\(param1)',true);")
+                } else if param2 == "follow_user" {
+                    self.webView.evaluateJavaScript("followUser('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
+                } else if param2 == "homepage" || param2 == "gif" {
+                    self.webView.evaluateJavaScript("window.location.href = '\(param1)';")
+                } else if param2 == "block_content" {
+                    self.webView.evaluateJavaScript("blockContent('\(param1)',true);")
+                } else {
+                    self.webView.evaluateJavaScript("openNewPost(true);")
+                }
+            } else {
+                self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+            }
+        } else if message.name == "setIsProductModalOpen" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Bool else {
+                return
+            }
+            if param1 {
+                if self.webView.scrollView.contentOffset.y < 0 { // Move tableView to top
+                    self.webView.scrollView.setContentOffset(CGPoint.zero, animated: true)
+                }
+            }
+            WebView4.showModal = param1
+        } else if message.name == "toggleVoiceSearch" {
+            if !isAllowSpeech {
+                setupSpeech()
+            } else {
+                runVoice()
+            }
+        } else if message.name == "blockUser" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? Bool else {
+                return
+            }
+            if param2 {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "1"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            } else {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "0"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            }
+        } else if message.name == "showAlert" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String else {
+                return
+            }
+            self.view.makeToast(param1, duration: 3)
+        } else if message.name == "blockUser" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? Bool else {
+                return
+            }
+            if param2 {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "1"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            } else {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "0"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            }
+        } else if message.name == "tabShowHide" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Bool else {
+                return
+            }
+            if param1 {
+                ViewController.alwaysHideButton = false
+                showTabBar()
+            } else {
+                if self.viewIfLoaded?.window != nil {
+                    ViewController.alwaysHideButton = true
+                    hideTabBar()
+                }
+            }
+        } else if message.name == "shareText" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String else {
+                return
+            }
+            if loadingURL {
+                return
+            }
+            let activityViewController = UIActivityViewController(activityItems: [param1], applicationActivities: nil)
+            self.present(activityViewController, animated: true, completion: nil)
+        } else if message.name == "openGalleryiOS" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Int else {
+                return
+            }
+            indexImageVideoWv = param1
+            let alertController = LibAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
+            
+            if let action = self.actionImageVideo(for: "image", title: "Choose Photo".localized()) {
+                alertController.addAction(action)
+            }
+            if let action = self.actionImageVideo(for: "video", title: "Choose Video".localized()) {
+                alertController.addAction(action)
+            }
+            alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
+            self.present(alertController, animated: true)
+        } else if message.name == "openTabChat" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabChat()
+                }
+            }
+        } else if message.name == "openTabWebview3" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabWebview3()
+                }
+            }
+        } else if message.name == "openCallAudio" {
+            APIS.openAudioCall()
+        } else if message.name == "openCallVideo" {
+            APIS.openVideoCall()
+        } else if message.name == "openChatSmartbot" {
+            APIS.openSmartChatbot()
+        } else if message.name == "openPanicDialog" {
+            
+        } else if message.name == "openContactCenter" {
+            APIS.openContactCenter()
+        } else if message.name == "openContactCenterChat" {
+            APIS.openContactCenter(media: 0)
+        } else if message.name == "openContactCenterAudioCall" {
+            APIS.openContactCenter(media: 1)
+        } else if message.name == "openContactCenterVideoCall" {
+            APIS.openContactCenter(media: 2)
+        }
+    }
+    
+    private func actionImageVideo(for type: String, title: String) -> UIAlertAction? {
+        return UIAlertAction(title: title, style: .default) { [unowned self] _ in
+            switch type {
+            case "image":
+                imageVideoPicker.present(source: .imageAlbum)
+            case "video":
+                imageVideoPicker.present(source: .videoAlbum)
+            default:
+                imageVideoPicker.present(source: .imageAlbum)
+            }
+        }
+    }
+    
+    func didSelect(imagevideo: Any?) {
+        if imagevideo != nil {
+            let imageData = imagevideo! as! [UIImagePickerController.InfoKey : Any]
+            if (imageData[.mediaType] as! String == "public.image") {
+                let compressedImage = (imageData[.originalImage] as! UIImage).pngData()!
+                let base64String = compressedImage.base64EncodedString()
+                let base64ToWeb = "data:image/jpeg;base64,\(base64String)"
+                webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
+                    if let error = error {
+                        print("Error executing JavaScript: \(error)")
+                    }
+                }
+            } else {
+                guard var dataVideo = try? Data(contentsOf: imageData[.mediaURL] as! URL) else {
+                    return
+                }
+                let sizeOfVideo = Double(dataVideo.count / 1048576)
+                if (sizeOfVideo > 10.0) {
+                    let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
+                    compressVideo(inputURL: imageData[.mediaURL] as! URL,
+                                  outputURL: compressedURL) { exportSession in
+                        guard let session = exportSession else {
+                            return
+                        }
+                        
+                        switch session.status {
+                        case .unknown:
+                            break
+                        case .waiting:
+                            break
+                        case .exporting:
+                            break
+                        case .completed:
+                            guard let compressedData = try? Data(contentsOf: compressedURL) else {
+                                return
+                            }
+                            dataVideo = compressedData
+                        case .failed:
+                            break
+                        case .cancelled:
+                            break
+                        @unknown default:
+                            break
+                        }
+                    }
+                }
+                let base64String = dataVideo.base64EncodedString()
+                let base64ToWeb = "data:video/mp4;base64,\(base64String)"
+                webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
+                    if let error = error {
+                        print("Error executing JavaScript: \(error)")
+                    }
+                }
+            }
+        }
+    }
+    
+    func compressVideo(inputURL: URL,
+                       outputURL: URL,
+                       handler:@escaping (_ exportSession: AVAssetExportSession?) -> Void) {
+        let urlAsset = AVURLAsset(url: inputURL, options: nil)
+        guard let exportSession = AVAssetExportSession(asset: urlAsset,
+                                                       presetName: AVAssetExportPresetMediumQuality) else {
+            handler(nil)
+            
+            return
+        }
+        
+        exportSession.outputURL = outputURL
+        exportSession.outputFileType = .mp4
+        exportSession.exportAsynchronously {
+            handler(exportSession)
+        }
+    }
+    
+    func setupSpeech() {
+
+        self.speechRecognizer?.delegate = self
+
+        SFSpeechRecognizer.requestAuthorization { (authStatus) in
+
+            var isButtonEnabled = false
+
+            switch authStatus {
+            case .authorized:
+                isButtonEnabled = true
+
+            case .denied:
+                isButtonEnabled = false
+                //print("User denied access to speech recognition")
+
+            case .restricted:
+                isButtonEnabled = false
+                //print("Speech recognition restricted on this device")
+
+            case .notDetermined:
+                isButtonEnabled = false
+                //print("Speech recognition not yet authorized")
+            @unknown default:
+                isButtonEnabled = false
+            }
+
+            OperationQueue.main.addOperation() {
+                self.isAllowSpeech = isButtonEnabled
+                if isButtonEnabled {
+                    SecureUserDefaults.shared.set(isButtonEnabled, forKey: "allowSpeech")
+                    self.runVoice()
+                }
+            }
+        }
+    }
+    
+    func runVoice() {
+        if !audioEngine.isRunning {
+            alertController = LibAlertController(title: "Start Recording".localized(), message: "Say something, I'm listening!".localized(), preferredStyle: .alert)
+            self.present(alertController, animated: true)
+            self.webView.evaluateJavaScript("toggleVoiceButton(true)")
+            self.startRecording()
+        }
+    }
+    
+    func startRecording() {
+
+        // Clear all previous session data and cancel task
+        if recognitionTask != nil {
+            recognitionTask?.cancel()
+            recognitionTask = nil
+        }
+
+        // Create instance of audio session to record voice
+        let audioSession = AVAudioSession.sharedInstance()
+        do {
+            try audioSession.setCategory(AVAudioSession.Category.record, mode: .default, options: [])
+            try audioSession.setMode(AVAudioSession.Mode.measurement)
+            try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
+        } catch {
+            //print("audioSession properties weren't set because of an error.")
+        }
+
+        self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
+
+        let inputNode = audioEngine.inputNode
+
+        guard let recognitionRequest = recognitionRequest else {
+            fatalError("Unable to create an SFSpeechAudioBufferRecognitionRequest object")
+        }
+
+        recognitionRequest.shouldReportPartialResults = true
+
+        self.recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in
+
+            var isFinal = false
+            var text = ""
+
+            if result != nil {
+                text = result?.bestTranscription.formattedString ?? ""
+                isFinal = (result?.isFinal)!
+                self.alertController.dismiss(animated: true)
+                self.audioEngine.stop()
+                self.recognitionRequest?.endAudio()
+            } else {
+                self.alertController.dismiss(animated: true)
+            }
+
+            if error != nil || isFinal {
+                if error == nil {
+                    self.webView.evaluateJavaScript("toggleVoiceButton(false)")
+                    self.webView.evaluateJavaScript("submitVoiceSearch('\(text)')")
+                } else {
+                    self.audioEngine.stop()
+                    self.recognitionRequest?.endAudio()
+                }
+                inputNode.removeTap(onBus: 0)
+
+                self.recognitionRequest = nil
+                self.recognitionTask = nil
+
+                self.isAllowSpeech = true
+            }
+        })
+
+        let recordingFormat = inputNode.outputFormat(forBus: 0)
+        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
+            self.recognitionRequest?.append(buffer)
+        }
+
+        self.audioEngine.prepare()
+
+        do {
+            try self.audioEngine.start()
+        } catch {
+            //print("audioEngine couldn't start because of an error.")
+        }
+    }
+    
+    func isUsingMyWebview() -> Bool{
+        return PrefsUtil.getURLWV4() == "0" || PrefsUtil.getURLWV4() == "1" || PrefsUtil.getURLWV4() == "2" || PrefsUtil.getURLWV4() == "3" || PrefsUtil.getURLWV4() == "4"
+    }
+
+}
+
+extension WebView4: SFSpeechRecognizerDelegate {
+
+    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
+        if available {
+            self.isAllowSpeech = true
+        } else {
+            self.isAllowSpeech = false
+        }
+    }
+}
+
+extension WebView4: WKUIDelegate, WKNavigationDelegate {
+    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
+        guard let url = navigationAction.request.url else {
+            decisionHandler(.cancel)
+            return
+        }
+        
+        if let scheme = url.scheme?.lowercased(), scheme.hasPrefix("itms") || scheme == "mailto" || scheme == "tel" {
+            DispatchQueue.main.async {
+                UIApplication.shared.open(url, options: [:], completionHandler: nil)
+            }
+            decisionHandler(.cancel)
+            return
+        }
+
+        guard navigationAction.targetFrame?.isMainFrame == true else {
+            decisionHandler(.allow)
+            return
+        }
+
+        if loadingURL {
+            decisionHandler(.cancel)
+            return
+        }
+
+        loadingURL = true
+
+        if allowedURLs.contains(url.absoluteString) {
+            loadingURL = false
+            decisionHandler(.allow)
+            return
+        }
+        
+        validateSSLCertificate(url: url) { [weak self] isValid in
+            guard let self = self else {
+                decisionHandler(.cancel)
+                return
+            }
+
+            DispatchQueue.main.async {
+                if isValid {
+                    self.allowedURLs.insert(url.absoluteString)
+                    self.loadingURL = false
+                    decisionHandler(.allow)
+                } else {
+                    let host = url.host ?? ""
+                    var messageText = "You're about to access a website that is not currently trusted by your Nexilis Browser. This website's security certificate is not recognized.\n\nDo you wish to proceed to <<domain>> and trust the website's security certificate?\n\nNote: Adding a website to the trusted list may increase your risk of security vulnerability".localized()
+                    messageText = messageText.replacingOccurrences(of: "<<domain>>", with: host)
+
+                    let alert = UIAlertController(title: "Warning Unknown Url!".localized(),
+                                                  message: messageText,
+                                                  preferredStyle: .alert)
+
+                    alert.addAction(UIAlertAction(title: "Yes", style: .default) { _ in
+                        let storedCertificate = Utils.getCertificatePinningWebview()
+                        if let jsonData = storedCertificate.data(using: .utf8),
+                           let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
+                            var certJson = certJson
+                            certJson[host] = self.blockedCertificate
+                            if let jsonData = try? JSONSerialization.data(withJSONObject: certJson, options: []),
+                               let jsonString = String(data: jsonData, encoding: .utf8) {
+                                Utils.setCertificatePinningWebview(value: jsonString)
+                            }
+                        }
+
+                        self.allowedURLs.insert(url.absoluteString)
+                        self.loadingURL = false
+                        decisionHandler(.allow)
+                    })
+
+                    alert.addAction(UIAlertAction(title: "No", style: .cancel) { _ in
+                        self.loadingURL = false
+                        decisionHandler(.cancel)
+                    })
+
+                    if self.presentedViewController == nil {
+                        self.present(alert, animated: true, completion: nil)
+                    } else {
+                        self.loadingURL = false
+                        decisionHandler(.cancel)
+                    }
+                }
+            }
+        }
+    }
+    
+    private func validateSSLCertificate(url: URL, completion: @escaping (Bool) -> Void) {
+        let session = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil)
+        let request = URLRequest(url: url)
+
+        let task = session.dataTask(with: request) { _, response, error in
+            if let error = error {
+                completion(false)
+                return
+            }
+            completion(true)
+        }
+        task.resume()
+    }
+}
+
+extension WebView4: URLSessionDelegate {
+    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+        guard let serverTrust = challenge.protectionSpace.serverTrust else {
+            completionHandler(.cancelAuthenticationChallenge, nil)
+            return
+        }
+        if let publicKeyHash = extractPublicKeyHash(from: serverTrust) {
+            let domain = challenge.protectionSpace.host
+            let storedCertificate = Utils.getCertificatePinningWebview()
+            if let jsonData = storedCertificate.data(using: .utf8),
+               let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
+                if publicKeyHash == certJson[domain] {
+                    completionHandler(.useCredential, URLCredential(trust: serverTrust))
+                } else {
+                    blockedCertificate = publicKeyHash
+                    completionHandler(.cancelAuthenticationChallenge, nil)
+                }
+            }
+        } else {
+            completionHandler(.cancelAuthenticationChallenge, nil)
+        }
+    }
+    
+    func extractPublicKeyHash(from serverTrust: SecTrust) -> String? {
+        guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else { return nil }
+        guard let publicKey = SecCertificateCopyKey(certificate) else { return nil }
+        
+        var error: Unmanaged<CFError>?
+        guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
+            return nil
+        }
+        
+        // Compute SHA-256 hash
+        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
+        publicKeyData.withUnsafeBytes {
+            _ = CC_SHA256($0.baseAddress, CC_LONG(publicKeyData.count), &hash)
+        }
+        
+        let hashData = Data(hash)
+        let base64Hash = hashData.base64EncodedString()
+        
+        return base64Hash
+    }
+}

+ 947 - 0
AppBuilder/AppBuilder/WebView5.swift

@@ -0,0 +1,947 @@
+//
+//  WebView5.swift
+//  AppBuilder
+//
+//  Created by Qindi on 31/10/25.
+//
+
+import UIKit
+import WebKit
+import NexilisLite
+import Speech
+import CommonCrypto
+
+class WebView5: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate, WKScriptMessageHandler, ImageVideoPickerDelegate {
+    
+    var webView: WKWebView!
+    var address = ""
+    private var lastContentOffset: CGFloat = 0
+    var progressView: UIProgressView!
+    
+    var isAllowSpeech = false
+    
+    let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "id"))
+
+    var recognitionRequest : SFSpeechAudioBufferRecognitionRequest?
+    var recognitionTask : SFSpeechRecognitionTask?
+    let audioEngine = AVAudioEngine()
+    var alertController = LibAlertController()
+    
+    public static var forceRefresh = true
+    public static var canLoadURL = false
+    public static var showModal = false
+    
+    var indexImageVideoWv = 0
+    var imageVideoPicker: ImageVideoPicker!
+    var blockedCertificate = ""
+    var allowedURLs = Set<String>()
+    var loadingURL = false
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        self.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
+
+        let configuration = WKWebViewConfiguration()
+        configuration.allowsInlineMediaPlayback = true
+        loadContentBlocker(into: configuration) { [self] in
+            DispatchQueue.main.async {
+                self.initializeWebView(with: configuration)
+            }
+        }
+    }
+    
+    func initializeWebView(with configuration: WKWebViewConfiguration) {
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(collapseDocked))
+        tapGesture.cancelsTouchesInView = false
+        tapGesture.delegate = self
+        let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
+        let finalUserAgent = "\(customUserAgent)"
+        configuration.applicationNameForUserAgent = finalUserAgent
+        webView = WKWebView(frame: .zero, configuration: configuration)
+        view.addSubview(webView)
+        webView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+        ])
+        webView.scrollView.addGestureRecognizer(tapGesture)
+        let refreshControl = UIRefreshControl()
+        refreshControl.addTarget(self, action: #selector(reloadWebView(_:)), for: .valueChanged)
+        webView.scrollView.addSubview(refreshControl)
+        webView.scrollView.delegate = self
+        webView.navigationDelegate = self
+        webView.allowsBackForwardNavigationGestures = true
+        
+        progressView = UIProgressView(progressViewStyle: .default)
+        progressView.sizeToFit()
+        progressView.tintColor = .systemBlue
+        view.addSubview(progressView)
+        
+        // Auto Layout for progress bar (stick to top)
+        progressView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            progressView.heightAnchor.constraint(equalToConstant: 4)
+        ])
+        
+        // Observe estimatedProgress
+        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+        
+        let contentController = self.webView.configuration.userContentController
+        contentController.add(self, name: "checkProfile")
+        contentController.add(self, name: "setIsProductModalOpen")
+        contentController.add(self, name: "toggleVoiceSearch")
+        contentController.add(self, name: "blockUser")
+        contentController.add(self, name: "showAlert")
+        contentController.add(self, name: "closeProfile")
+        contentController.add(self, name: "tabShowHide")
+        contentController.add(self, name: "shareText")
+        contentController.add(self, name: "openGalleryiOS")
+        contentController.add(self, name: "openChannel")
+        contentController.add(self, name: "openTabWebview3")
+        contentController.add(self, name: "openTabChat")
+        contentController.add(self, name: "openCallAudio")
+        contentController.add(self, name: "openCallVideo")
+        contentController.add(self, name: "openChatSmartbot")
+        contentController.add(self, name: "openPanicDialog")
+        contentController.add(self, name: "openContactCenter")
+        contentController.add(self, name: "openContactCenterChat")
+        contentController.add(self, name: "openContactCenterAudioCall")
+        contentController.add(self, name: "openContactCenterVideoCall")
+        
+        let source: String = "var meta = document.createElement('meta');" +
+            "meta.name = 'viewport';" +
+            "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" +
+            "var head = document.getElementsByTagName('head')[0];" +
+            "head.appendChild(meta);" +
+        "$('#header-layout').find('.col-8').removeClass('col-8').addClass('col');" +
+        "$('#header-layout').find('.col-4').removeClass('col-4').addClass('col');"
+        let script: WKUserScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
+        contentController.addUserScript(script)
+        
+        let cookieScript1 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[0])';"
+        let cookieScriptInjection1 = WKUserScript(source: cookieScript1, injectionTime: .atDocumentStart, forMainFrameOnly: false)
+        configuration.userContentController.addUserScript(cookieScriptInjection1)
+        
+        let cookieScript2 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[1])';"
+        let cookieScriptInjection2 = WKUserScript(source: cookieScript2, injectionTime: .atDocumentStart, forMainFrameOnly: false)
+        configuration.userContentController.addUserScript(cookieScriptInjection2)
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(onShowAC(notification:)), name: NSNotification.Name(rawValue: "onShowAC"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onRefreshWebView(notification:)), name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onResumeWebView(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil)
+        WebView5.canLoadURL = true
+        processURL()
+    }
+    
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progressView.progress = Float(webView.estimatedProgress)
+            
+            progressView.isHidden = webView.estimatedProgress >= 1
+        }
+    }
+    
+    func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
+        // Define ad-blocking rules directly in Swift as a string
+        let contentRules = PrefsUtil.contentRulesAds
+
+        WKContentRuleListStore.default().compileContentRuleList(forIdentifier: "AdBlocker", encodedContentRuleList: contentRules) { ruleList, error in
+            if let ruleList = ruleList {
+                config.userContentController.add(ruleList)
+            } else {
+                print("Failed to compile content rule list: \(error?.localizedDescription ?? "Unknown error")")
+            }
+            completion()
+        }
+    }
+    
+    func loadURLWithCookie(url: URL) {
+        var urlRequest = URLRequest(url: url)
+        let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
+        urlRequest.setValue(customUserAgent, forHTTPHeaderField: "User-Agent")
+        if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
+            for cookie in cookies {
+                webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
+            }
+            webView.load(urlRequest)
+        }
+    }
+    
+    func processURL() {
+        let me = User.getMyPin()
+        
+        var myURL : URL?
+        let lang: String = SecureUserDefaults.shared.value(forKey: "i18n_language") ?? "en"
+        var intLang = 0
+        if lang == "id" {
+            intLang = 1
+        }
+        if PrefsUtil.getURLWV5() != nil {
+            ViewController.wv5 = PrefsUtil.getURLWV5()!
+        }
+        switch(ViewController.wv5){
+        case "0":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-main-only?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "1":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab3-main-only?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "2":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-main?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "3":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab3-commerce?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "4":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-video?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        default:
+            if(!ViewController.wv5.isEmpty){
+                if(ViewController.wv5.lowercased().contains("https://") || ViewController.wv5.lowercased().contains("http://")){
+                    address = ViewController.wv5
+                    myURL = URL(string: address)
+                }
+                else {
+                    if ViewController.wv5.contains("nexilis/pages"){
+                        address = "\(PrefsUtil.getURLBase())\(ViewController.wv5)?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+                    } else {
+                        address = "https://\(ViewController.wv5)"
+                    }
+                    myURL = URL(string: address)
+                }
+            }
+        }
+        if let u = myURL {
+            webView.evaluateJavaScript("{window.localStorage.setItem('currentTab','\(ViewController.wv5)')}")
+            if WebView5.forceRefresh {
+                loadURLWithCookie(url: u)
+            } else {
+                webView.evaluateJavaScript("if(resumeAll){resumeAll();}")
+            }
+            WebView5.forceRefresh = false
+        }
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        if WebView5.canLoadURL {
+            processURL()
+        }
+        navigationController?.setNavigationBarHidden(true, animated: false)
+    }
+    
+    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+        if Utils.getIsLoadThemeFromOther() {
+            self.webView.evaluateJavaScript("{window.localStorage.setItem('mobileConfiguration','"+Utils.getMyTheme()+"')}")
+        }
+    }
+    
+    override func viewDidAppear(_ animated: Bool) {
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
+            var viewController = UIApplication.shared.windows.first!.rootViewController
+            if let nc = viewController as? UINavigationController {
+                viewController = nc.viewControllers.first
+            }
+            if ViewController.middleButton.isHidden {
+                ViewController.isExpandButton = false
+                if let viewController = viewController as? ViewController {
+                    if viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = false
+                        ViewController.middleButton.isHidden = false
+                        ViewController.alwaysHideButton = false
+                    }
+                    if ViewController.middleButton.isHidden && PrefsUtil.getCpaasMode() == PrefsUtil.CPAAS_MODE_DOCKED {
+                        viewController.showMiddleButtonAfterPush()
+                    }
+                }
+            } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+                DispatchQueue.main.async {
+                    if let viewController = viewController as? ViewController {
+                        if viewController.tabBar.isHidden {
+                            viewController.tabBar.isHidden = false
+                            ViewController.alwaysHideButton = false
+                        }
+                    }
+                }
+            }
+        })
+    }
+    
+    @objc func onShowAC(notification: NSNotification) {
+        self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+        view.endEditing(true)
+        resignFirstResponder()
+    }
+    
+    @objc func onRefreshWebView(notification: NSNotification) {
+        WebView5.forceRefresh = true
+        WebView5.showModal = false
+    }
+    
+    @objc func onResumeWebView(notification: NSNotification) {
+        Nexilis.reloadCookies(webView: webView)
+    }
+    
+    override func viewWillDisappear(_ animated: Bool) {
+        if self.webView != nil {
+            if self.webView.scrollView.contentOffset.y < 0 { // Move tableView to top
+                self.webView.scrollView.setContentOffset(CGPoint.zero, animated: true)
+            }
+            self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+            self.webView.evaluateJavaScript("hideAddToCart();")
+        }
+    }
+    
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        if (self.lastContentOffset > scrollView.contentOffset.y && scrollView.contentOffset.y < (scrollView.contentSize.height - scrollView.frame.size.height)) {
+            showTabBar();
+        }
+        else if (self.lastContentOffset != 0 && self.lastContentOffset < scrollView.contentOffset.y && self.lastContentOffset >= 0) {
+            hideTabBar();
+        }
+        self.lastContentOffset = scrollView.contentOffset.y
+        self.collapseDocked()
+    }
+    
+    @objc func collapseDocked() {
+        if ViewController.isExpandButton {
+            ViewController.expandButton()
+        }
+    }
+
+    @objc func reloadWebView(_ sender: UIRefreshControl) {
+//        WebView5.forceRefresh = true
+//        viewWillAppear(false)
+        webView.reload()
+        ViewController.alwaysHideButton = false
+        showTabBar()
+        sender.endRefreshing()
+    }
+    
+    func hideTabBar() {
+        if UIApplication.shared.windows.first == nil {
+            return
+        }
+        var viewController = UIApplication.shared.windows.first!.rootViewController
+        if let nc = viewController as? UINavigationController {
+            viewController = nc.viewControllers.first
+        }
+        if ViewController.middleButton.isDescendant(of: viewController!.view) {
+            DispatchQueue.main.async {
+                if ViewController.isExpandButton {
+                    ViewController.expandButton()
+                }
+                ViewController.hideDockedButton()
+                if let viewController = viewController as? ViewController {
+                    viewController.tabBar.isHidden = true
+                }
+                ViewController.removeMiddleButton()
+            }
+        } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+            DispatchQueue.main.async {
+                if let viewController = viewController as? ViewController {
+                    if !viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = true
+                    }
+                }
+            }
+        }
+    }
+
+    func showTabBar() {
+        if(ViewController.alwaysHideButton){
+            return
+        }
+        var viewController = UIApplication.shared.windows.first!.rootViewController
+        if let nc = viewController as? UINavigationController {
+            viewController = nc.viewControllers.first
+        }
+        if ViewController.middleButton.isHidden {
+            if let viewController = viewController as? ViewController {
+                viewController.tabBar.isHidden = false
+                ViewController.middleButton.isHidden = false
+            }
+        } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+            DispatchQueue.main.async {
+                if let viewController = viewController as? ViewController {
+                    if viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = false
+                    }
+                }
+            }
+        }
+    }
+
+    func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
+        scrollView.pinchGestureRecognizer?.isEnabled = false
+    }
+
+    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        if message.name == "checkProfile" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? String else {
+                return
+            }
+            if ViewController.checkIsChangePerson() {
+                if param2 == "like" {
+                    self.webView.evaluateJavaScript("likeProduct('\(param1)',1,true);")
+                } else if param2 == "comment" {
+                    self.webView.evaluateJavaScript("openComment('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
+                } else if param2 == "report_user" {
+                    self.webView.evaluateJavaScript("reportUser('\(param1)',true);")
+                } else if param2 == "report_content" {
+                    self.webView.evaluateJavaScript("reportContent('\(param1.split(separator: "|")[0])','\(param1.split(separator: "|")[1])',true);")
+                } else if param2 == "block_user" {
+                    self.webView.evaluateJavaScript("blockUser('\(param1)',true);")
+                } else if param2 == "follow_user" {
+                    self.webView.evaluateJavaScript("followUser('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
+                } else if param2 == "homepage" || param2 == "gif" {
+                    self.webView.evaluateJavaScript("window.location.href = '\(param1)';")
+                } else if param2 == "block_content" {
+                    self.webView.evaluateJavaScript("blockContent('\(param1)',true);")
+                } else {
+                    self.webView.evaluateJavaScript("openNewPost(true);")
+                }
+            } else {
+                self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+            }
+        } else if message.name == "setIsProductModalOpen" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Bool else {
+                return
+            }
+            if param1 {
+                if self.webView.scrollView.contentOffset.y < 0 { // Move tableView to top
+                    self.webView.scrollView.setContentOffset(CGPoint.zero, animated: true)
+                }
+            }
+            WebView5.showModal = param1
+        } else if message.name == "toggleVoiceSearch" {
+            if !isAllowSpeech {
+                setupSpeech()
+            } else {
+                runVoice()
+            }
+        } else if message.name == "blockUser" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? Bool else {
+                return
+            }
+            if param2 {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "1"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            } else {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "0"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            }
+        } else if message.name == "showAlert" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String else {
+                return
+            }
+            self.view.makeToast(param1, duration: 3)
+        } else if message.name == "blockUser" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? Bool else {
+                return
+            }
+            if param2 {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "1"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            } else {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "0"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            }
+        } else if message.name == "tabShowHide" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Bool else {
+                return
+            }
+            if param1 {
+                ViewController.alwaysHideButton = false
+                showTabBar()
+            } else {
+                if self.viewIfLoaded?.window != nil {
+                    ViewController.alwaysHideButton = true
+                    hideTabBar()
+                }
+            }
+        } else if message.name == "shareText" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String else {
+                return
+            }
+            if loadingURL {
+                return
+            }
+            let activityViewController = UIActivityViewController(activityItems: [param1], applicationActivities: nil)
+            self.present(activityViewController, animated: true, completion: nil)
+        } else if message.name == "openGalleryiOS" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Int else {
+                return
+            }
+            indexImageVideoWv = param1
+            let alertController = LibAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
+            
+            if let action = self.actionImageVideo(for: "image", title: "Choose Photo".localized()) {
+                alertController.addAction(action)
+            }
+            if let action = self.actionImageVideo(for: "video", title: "Choose Video".localized()) {
+                alertController.addAction(action)
+            }
+            alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
+            self.present(alertController, animated: true)
+        } else if message.name == "openTabChat" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabChat()
+                }
+            }
+        } else if message.name == "openTabWebview3" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabWebview3()
+                }
+            }
+        } else if message.name == "openCallAudio" {
+            APIS.openAudioCall()
+        } else if message.name == "openCallVideo" {
+            APIS.openVideoCall()
+        } else if message.name == "openChatSmartbot" {
+            APIS.openSmartChatbot()
+        } else if message.name == "openPanicDialog" {
+            
+        } else if message.name == "openContactCenter" {
+            APIS.openContactCenter()
+        } else if message.name == "openContactCenterChat" {
+            APIS.openContactCenter(media: 0)
+        } else if message.name == "openContactCenterAudioCall" {
+            APIS.openContactCenter(media: 1)
+        } else if message.name == "openContactCenterVideoCall" {
+            APIS.openContactCenter(media: 2)
+        }
+    }
+    
+    private func actionImageVideo(for type: String, title: String) -> UIAlertAction? {
+        return UIAlertAction(title: title, style: .default) { [unowned self] _ in
+            switch type {
+            case "image":
+                imageVideoPicker.present(source: .imageAlbum)
+            case "video":
+                imageVideoPicker.present(source: .videoAlbum)
+            default:
+                imageVideoPicker.present(source: .imageAlbum)
+            }
+        }
+    }
+    
+    func didSelect(imagevideo: Any?) {
+        if imagevideo != nil {
+            let imageData = imagevideo! as! [UIImagePickerController.InfoKey : Any]
+            if (imageData[.mediaType] as! String == "public.image") {
+                let compressedImage = (imageData[.originalImage] as! UIImage).pngData()!
+                let base64String = compressedImage.base64EncodedString()
+                let base64ToWeb = "data:image/jpeg;base64,\(base64String)"
+                webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
+                    if let error = error {
+                        print("Error executing JavaScript: \(error)")
+                    }
+                }
+            } else {
+                guard var dataVideo = try? Data(contentsOf: imageData[.mediaURL] as! URL) else {
+                    return
+                }
+                let sizeOfVideo = Double(dataVideo.count / 1048576)
+                if (sizeOfVideo > 10.0) {
+                    let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
+                    compressVideo(inputURL: imageData[.mediaURL] as! URL,
+                                  outputURL: compressedURL) { exportSession in
+                        guard let session = exportSession else {
+                            return
+                        }
+                        
+                        switch session.status {
+                        case .unknown:
+                            break
+                        case .waiting:
+                            break
+                        case .exporting:
+                            break
+                        case .completed:
+                            guard let compressedData = try? Data(contentsOf: compressedURL) else {
+                                return
+                            }
+                            dataVideo = compressedData
+                        case .failed:
+                            break
+                        case .cancelled:
+                            break
+                        @unknown default:
+                            break
+                        }
+                    }
+                }
+                let base64String = dataVideo.base64EncodedString()
+                let base64ToWeb = "data:video/mp4;base64,\(base64String)"
+                webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
+                    if let error = error {
+                        print("Error executing JavaScript: \(error)")
+                    }
+                }
+            }
+        }
+    }
+    
+    func compressVideo(inputURL: URL,
+                       outputURL: URL,
+                       handler:@escaping (_ exportSession: AVAssetExportSession?) -> Void) {
+        let urlAsset = AVURLAsset(url: inputURL, options: nil)
+        guard let exportSession = AVAssetExportSession(asset: urlAsset,
+                                                       presetName: AVAssetExportPresetMediumQuality) else {
+            handler(nil)
+            
+            return
+        }
+        
+        exportSession.outputURL = outputURL
+        exportSession.outputFileType = .mp4
+        exportSession.exportAsynchronously {
+            handler(exportSession)
+        }
+    }
+    
+    func setupSpeech() {
+
+        self.speechRecognizer?.delegate = self
+
+        SFSpeechRecognizer.requestAuthorization { (authStatus) in
+
+            var isButtonEnabled = false
+
+            switch authStatus {
+            case .authorized:
+                isButtonEnabled = true
+
+            case .denied:
+                isButtonEnabled = false
+                //print("User denied access to speech recognition")
+
+            case .restricted:
+                isButtonEnabled = false
+                //print("Speech recognition restricted on this device")
+
+            case .notDetermined:
+                isButtonEnabled = false
+                //print("Speech recognition not yet authorized")
+            @unknown default:
+                isButtonEnabled = false
+            }
+
+            OperationQueue.main.addOperation() {
+                self.isAllowSpeech = isButtonEnabled
+                if isButtonEnabled {
+                    SecureUserDefaults.shared.set(isButtonEnabled, forKey: "allowSpeech")
+                    self.runVoice()
+                }
+            }
+        }
+    }
+    
+    func runVoice() {
+        if !audioEngine.isRunning {
+            alertController = LibAlertController(title: "Start Recording".localized(), message: "Say something, I'm listening!".localized(), preferredStyle: .alert)
+            self.present(alertController, animated: true)
+            self.webView.evaluateJavaScript("toggleVoiceButton(true)")
+            self.startRecording()
+        }
+    }
+    
+    func startRecording() {
+
+        // Clear all previous session data and cancel task
+        if recognitionTask != nil {
+            recognitionTask?.cancel()
+            recognitionTask = nil
+        }
+
+        // Create instance of audio session to record voice
+        let audioSession = AVAudioSession.sharedInstance()
+        do {
+            try audioSession.setCategory(AVAudioSession.Category.record, mode: .default, options: [])
+            try audioSession.setMode(AVAudioSession.Mode.measurement)
+            try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
+        } catch {
+            //print("audioSession properties weren't set because of an error.")
+        }
+
+        self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
+
+        let inputNode = audioEngine.inputNode
+
+        guard let recognitionRequest = recognitionRequest else {
+            fatalError("Unable to create an SFSpeechAudioBufferRecognitionRequest object")
+        }
+
+        recognitionRequest.shouldReportPartialResults = true
+
+        self.recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in
+
+            var isFinal = false
+            var text = ""
+
+            if result != nil {
+                text = result?.bestTranscription.formattedString ?? ""
+                isFinal = (result?.isFinal)!
+                self.alertController.dismiss(animated: true)
+                self.audioEngine.stop()
+                self.recognitionRequest?.endAudio()
+            } else {
+                self.alertController.dismiss(animated: true)
+            }
+
+            if error != nil || isFinal {
+                if error == nil {
+                    self.webView.evaluateJavaScript("toggleVoiceButton(false)")
+                    self.webView.evaluateJavaScript("submitVoiceSearch('\(text)')")
+                } else {
+                    self.audioEngine.stop()
+                    self.recognitionRequest?.endAudio()
+                }
+                inputNode.removeTap(onBus: 0)
+
+                self.recognitionRequest = nil
+                self.recognitionTask = nil
+
+                self.isAllowSpeech = true
+            }
+        })
+
+        let recordingFormat = inputNode.outputFormat(forBus: 0)
+        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
+            self.recognitionRequest?.append(buffer)
+        }
+
+        self.audioEngine.prepare()
+
+        do {
+            try self.audioEngine.start()
+        } catch {
+            //print("audioEngine couldn't start because of an error.")
+        }
+    }
+    
+    func isUsingMyWebview() -> Bool{
+        return PrefsUtil.getURLWV5() == "0" || PrefsUtil.getURLWV5() == "1" || PrefsUtil.getURLWV5() == "2" || PrefsUtil.getURLWV5() == "3" || PrefsUtil.getURLWV5() == "4"
+    }
+
+}
+
+extension WebView5: SFSpeechRecognizerDelegate {
+
+    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
+        if available {
+            self.isAllowSpeech = true
+        } else {
+            self.isAllowSpeech = false
+        }
+    }
+}
+
+extension WebView5: WKUIDelegate, WKNavigationDelegate {
+    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
+        guard let url = navigationAction.request.url else {
+            decisionHandler(.cancel)
+            return
+        }
+        
+        if let scheme = url.scheme?.lowercased(), scheme.hasPrefix("itms") || scheme == "mailto" || scheme == "tel" {
+            DispatchQueue.main.async {
+                UIApplication.shared.open(url, options: [:], completionHandler: nil)
+            }
+            decisionHandler(.cancel)
+            return
+        }
+
+        guard navigationAction.targetFrame?.isMainFrame == true else {
+            decisionHandler(.allow)
+            return
+        }
+
+        if loadingURL {
+            decisionHandler(.cancel)
+            return
+        }
+
+        loadingURL = true
+
+        if allowedURLs.contains(url.absoluteString) {
+            loadingURL = false
+            decisionHandler(.allow)
+            return
+        }
+        
+        validateSSLCertificate(url: url) { [weak self] isValid in
+            guard let self = self else {
+                decisionHandler(.cancel)
+                return
+            }
+
+            DispatchQueue.main.async {
+                if isValid {
+                    self.allowedURLs.insert(url.absoluteString)
+                    self.loadingURL = false
+                    decisionHandler(.allow)
+                } else {
+                    let host = url.host ?? ""
+                    var messageText = "You're about to access a website that is not currently trusted by your Nexilis Browser. This website's security certificate is not recognized.\n\nDo you wish to proceed to <<domain>> and trust the website's security certificate?\n\nNote: Adding a website to the trusted list may increase your risk of security vulnerability".localized()
+                    messageText = messageText.replacingOccurrences(of: "<<domain>>", with: host)
+
+                    let alert = UIAlertController(title: "Warning Unknown Url!".localized(),
+                                                  message: messageText,
+                                                  preferredStyle: .alert)
+
+                    alert.addAction(UIAlertAction(title: "Yes", style: .default) { _ in
+                        let storedCertificate = Utils.getCertificatePinningWebview()
+                        if let jsonData = storedCertificate.data(using: .utf8),
+                           let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
+                            var certJson = certJson
+                            certJson[host] = self.blockedCertificate
+                            if let jsonData = try? JSONSerialization.data(withJSONObject: certJson, options: []),
+                               let jsonString = String(data: jsonData, encoding: .utf8) {
+                                Utils.setCertificatePinningWebview(value: jsonString)
+                            }
+                        }
+
+                        self.allowedURLs.insert(url.absoluteString)
+                        self.loadingURL = false
+                        decisionHandler(.allow)
+                    })
+
+                    alert.addAction(UIAlertAction(title: "No", style: .cancel) { _ in
+                        self.loadingURL = false
+                        decisionHandler(.cancel)
+                    })
+
+                    if self.presentedViewController == nil {
+                        self.present(alert, animated: true, completion: nil)
+                    } else {
+                        self.loadingURL = false
+                        decisionHandler(.cancel)
+                    }
+                }
+            }
+        }
+    }
+    
+    private func validateSSLCertificate(url: URL, completion: @escaping (Bool) -> Void) {
+        let session = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil)
+        let request = URLRequest(url: url)
+
+        let task = session.dataTask(with: request) { _, response, error in
+            if let error = error {
+                completion(false)
+                return
+            }
+            completion(true)
+        }
+        task.resume()
+    }
+}
+
+extension WebView5: URLSessionDelegate {
+    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+        guard let serverTrust = challenge.protectionSpace.serverTrust else {
+            completionHandler(.cancelAuthenticationChallenge, nil)
+            return
+        }
+        if let publicKeyHash = extractPublicKeyHash(from: serverTrust) {
+            let domain = challenge.protectionSpace.host
+            let storedCertificate = Utils.getCertificatePinningWebview()
+            if let jsonData = storedCertificate.data(using: .utf8),
+               let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
+                if publicKeyHash == certJson[domain] {
+                    completionHandler(.useCredential, URLCredential(trust: serverTrust))
+                } else {
+                    blockedCertificate = publicKeyHash
+                    completionHandler(.cancelAuthenticationChallenge, nil)
+                }
+            }
+        } else {
+            completionHandler(.cancelAuthenticationChallenge, nil)
+        }
+    }
+    
+    func extractPublicKeyHash(from serverTrust: SecTrust) -> String? {
+        guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else { return nil }
+        guard let publicKey = SecCertificateCopyKey(certificate) else { return nil }
+        
+        var error: Unmanaged<CFError>?
+        guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
+            return nil
+        }
+        
+        // Compute SHA-256 hash
+        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
+        publicKeyData.withUnsafeBytes {
+            _ = CC_SHA256($0.baseAddress, CC_LONG(publicKeyData.count), &hash)
+        }
+        
+        let hashData = Data(hash)
+        let base64Hash = hashData.base64EncodedString()
+        
+        return base64Hash
+    }
+}

+ 947 - 0
AppBuilder/AppBuilder/WebView6.swift

@@ -0,0 +1,947 @@
+//
+//  WebView6.swift
+//  AppBuilder
+//
+//  Created by Qindi on 31/10/25.
+//
+
+import UIKit
+import WebKit
+import NexilisLite
+import Speech
+import CommonCrypto
+
+class WebView6: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate, WKScriptMessageHandler, ImageVideoPickerDelegate {
+    
+    var webView: WKWebView!
+    var address = ""
+    private var lastContentOffset: CGFloat = 0
+    var progressView: UIProgressView!
+    
+    var isAllowSpeech = false
+    
+    let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "id"))
+
+    var recognitionRequest : SFSpeechAudioBufferRecognitionRequest?
+    var recognitionTask : SFSpeechRecognitionTask?
+    let audioEngine = AVAudioEngine()
+    var alertController = LibAlertController()
+    
+    public static var forceRefresh = true
+    public static var canLoadURL = false
+    public static var showModal = false
+    
+    var indexImageVideoWv = 0
+    var imageVideoPicker: ImageVideoPicker!
+    var blockedCertificate = ""
+    var allowedURLs = Set<String>()
+    var loadingURL = false
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        self.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
+
+        let configuration = WKWebViewConfiguration()
+        configuration.allowsInlineMediaPlayback = true
+        loadContentBlocker(into: configuration) { [self] in
+            DispatchQueue.main.async {
+                self.initializeWebView(with: configuration)
+            }
+        }
+    }
+    
+    func initializeWebView(with configuration: WKWebViewConfiguration) {
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(collapseDocked))
+        tapGesture.cancelsTouchesInView = false
+        tapGesture.delegate = self
+        let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
+        let finalUserAgent = "\(customUserAgent)"
+        configuration.applicationNameForUserAgent = finalUserAgent
+        webView = WKWebView(frame: .zero, configuration: configuration)
+        view.addSubview(webView)
+        webView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+        ])
+        webView.scrollView.addGestureRecognizer(tapGesture)
+        let refreshControl = UIRefreshControl()
+        refreshControl.addTarget(self, action: #selector(reloadWebView(_:)), for: .valueChanged)
+        webView.scrollView.addSubview(refreshControl)
+        webView.scrollView.delegate = self
+        webView.navigationDelegate = self
+        webView.allowsBackForwardNavigationGestures = true
+        
+        progressView = UIProgressView(progressViewStyle: .default)
+        progressView.sizeToFit()
+        progressView.tintColor = .systemBlue
+        view.addSubview(progressView)
+        
+        // Auto Layout for progress bar (stick to top)
+        progressView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            progressView.heightAnchor.constraint(equalToConstant: 4)
+        ])
+        
+        // Observe estimatedProgress
+        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+        
+        let contentController = self.webView.configuration.userContentController
+        contentController.add(self, name: "checkProfile")
+        contentController.add(self, name: "setIsProductModalOpen")
+        contentController.add(self, name: "toggleVoiceSearch")
+        contentController.add(self, name: "blockUser")
+        contentController.add(self, name: "showAlert")
+        contentController.add(self, name: "closeProfile")
+        contentController.add(self, name: "tabShowHide")
+        contentController.add(self, name: "shareText")
+        contentController.add(self, name: "openGalleryiOS")
+        contentController.add(self, name: "openChannel")
+        contentController.add(self, name: "openTabWebview3")
+        contentController.add(self, name: "openTabChat")
+        contentController.add(self, name: "openCallAudio")
+        contentController.add(self, name: "openCallVideo")
+        contentController.add(self, name: "openChatSmartbot")
+        contentController.add(self, name: "openPanicDialog")
+        contentController.add(self, name: "openContactCenter")
+        contentController.add(self, name: "openContactCenterChat")
+        contentController.add(self, name: "openContactCenterAudioCall")
+        contentController.add(self, name: "openContactCenterVideoCall")
+        
+        let source: String = "var meta = document.createElement('meta');" +
+            "meta.name = 'viewport';" +
+            "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" +
+            "var head = document.getElementsByTagName('head')[0];" +
+            "head.appendChild(meta);" +
+        "$('#header-layout').find('.col-8').removeClass('col-8').addClass('col');" +
+        "$('#header-layout').find('.col-4').removeClass('col-4').addClass('col');"
+        let script: WKUserScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
+        contentController.addUserScript(script)
+        
+        let cookieScript1 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[0])';"
+        let cookieScriptInjection1 = WKUserScript(source: cookieScript1, injectionTime: .atDocumentStart, forMainFrameOnly: false)
+        configuration.userContentController.addUserScript(cookieScriptInjection1)
+        
+        let cookieScript2 = "document.cookie = '\(Utils.getCookiesMobile().components(separatedBy: ";")[1])';"
+        let cookieScriptInjection2 = WKUserScript(source: cookieScript2, injectionTime: .atDocumentStart, forMainFrameOnly: false)
+        configuration.userContentController.addUserScript(cookieScriptInjection2)
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(onShowAC(notification:)), name: NSNotification.Name(rawValue: "onShowAC"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onRefreshWebView(notification:)), name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onResumeWebView(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil)
+        WebView6.canLoadURL = true
+        processURL()
+    }
+    
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progressView.progress = Float(webView.estimatedProgress)
+            
+            progressView.isHidden = webView.estimatedProgress >= 1
+        }
+    }
+    
+    func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
+        // Define ad-blocking rules directly in Swift as a string
+        let contentRules = PrefsUtil.contentRulesAds
+
+        WKContentRuleListStore.default().compileContentRuleList(forIdentifier: "AdBlocker", encodedContentRuleList: contentRules) { ruleList, error in
+            if let ruleList = ruleList {
+                config.userContentController.add(ruleList)
+            } else {
+                print("Failed to compile content rule list: \(error?.localizedDescription ?? "Unknown error")")
+            }
+            completion()
+        }
+    }
+    
+    func loadURLWithCookie(url: URL) {
+        var urlRequest = URLRequest(url: url)
+        let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
+        urlRequest.setValue(customUserAgent, forHTTPHeaderField: "User-Agent")
+        if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
+            for cookie in cookies {
+                webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
+            }
+            webView.load(urlRequest)
+        }
+    }
+    
+    func processURL() {
+        let me = User.getMyPin()
+        
+        var myURL : URL?
+        let lang: String = SecureUserDefaults.shared.value(forKey: "i18n_language") ?? "en"
+        var intLang = 0
+        if lang == "id" {
+            intLang = 1
+        }
+        if PrefsUtil.getURLWV6() != nil {
+            ViewController.wv6 = PrefsUtil.getURLWV6()!
+        }
+        switch(ViewController.wv6){
+        case "0":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-main-only?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "1":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab3-main-only?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "2":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-main?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "3":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab3-commerce?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        case "4":
+            address = "\(PrefsUtil.getURLBase())nexilis/pages/tab1-video?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+            myURL = URL(string: address)
+        default:
+            if(!ViewController.wv6.isEmpty){
+                if(ViewController.wv6.lowercased().contains("https://") || ViewController.wv6.lowercased().contains("http://")){
+                    address = ViewController.wv6
+                    myURL = URL(string: address)
+                }
+                else {
+                    if ViewController.wv6.contains("nexilis/pages"){
+                        address = "\(PrefsUtil.getURLBase())\(ViewController.wv6)?f_pin=\(me ?? "")&lang=\(intLang)&theme=\(self.traitCollection.userInterfaceStyle == .dark ? "0" : "1")"
+                    } else {
+                        address = "https://\(ViewController.wv6)"
+                    }
+                    myURL = URL(string: address)
+                }
+            }
+        }
+        if let u = myURL {
+            webView.evaluateJavaScript("{window.localStorage.setItem('currentTab','\(ViewController.wv6)')}")
+            if WebView6.forceRefresh {
+                loadURLWithCookie(url: u)
+            } else {
+                webView.evaluateJavaScript("if(resumeAll){resumeAll();}")
+            }
+            WebView6.forceRefresh = false
+        }
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        if WebView6.canLoadURL {
+            processURL()
+        }
+        navigationController?.setNavigationBarHidden(true, animated: false)
+    }
+    
+    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+        if Utils.getIsLoadThemeFromOther() {
+            self.webView.evaluateJavaScript("{window.localStorage.setItem('mobileConfiguration','"+Utils.getMyTheme()+"')}")
+        }
+    }
+    
+    override func viewDidAppear(_ animated: Bool) {
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
+            var viewController = UIApplication.shared.windows.first!.rootViewController
+            if let nc = viewController as? UINavigationController {
+                viewController = nc.viewControllers.first
+            }
+            if ViewController.middleButton.isHidden {
+                ViewController.isExpandButton = false
+                if let viewController = viewController as? ViewController {
+                    if viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = false
+                        ViewController.middleButton.isHidden = false
+                        ViewController.alwaysHideButton = false
+                    }
+                    if ViewController.middleButton.isHidden && PrefsUtil.getCpaasMode() == PrefsUtil.CPAAS_MODE_DOCKED {
+                        viewController.showMiddleButtonAfterPush()
+                    }
+                }
+            } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+                DispatchQueue.main.async {
+                    if let viewController = viewController as? ViewController {
+                        if viewController.tabBar.isHidden {
+                            viewController.tabBar.isHidden = false
+                            ViewController.alwaysHideButton = false
+                        }
+                    }
+                }
+            }
+        })
+    }
+    
+    @objc func onShowAC(notification: NSNotification) {
+        self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+        view.endEditing(true)
+        resignFirstResponder()
+    }
+    
+    @objc func onRefreshWebView(notification: NSNotification) {
+        WebView6.forceRefresh = true
+        WebView6.showModal = false
+    }
+    
+    @objc func onResumeWebView(notification: NSNotification) {
+        Nexilis.reloadCookies(webView: webView)
+    }
+    
+    override func viewWillDisappear(_ animated: Bool) {
+        if self.webView != nil {
+            if self.webView.scrollView.contentOffset.y < 0 { // Move tableView to top
+                self.webView.scrollView.setContentOffset(CGPoint.zero, animated: true)
+            }
+            self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+            self.webView.evaluateJavaScript("hideAddToCart();")
+        }
+    }
+    
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        if (self.lastContentOffset > scrollView.contentOffset.y && scrollView.contentOffset.y < (scrollView.contentSize.height - scrollView.frame.size.height)) {
+            showTabBar();
+        }
+        else if (self.lastContentOffset != 0 && self.lastContentOffset < scrollView.contentOffset.y && self.lastContentOffset >= 0) {
+            hideTabBar();
+        }
+        self.lastContentOffset = scrollView.contentOffset.y
+        self.collapseDocked()
+    }
+    
+    @objc func collapseDocked() {
+        if ViewController.isExpandButton {
+            ViewController.expandButton()
+        }
+    }
+
+    @objc func reloadWebView(_ sender: UIRefreshControl) {
+//        WebView6.forceRefresh = true
+//        viewWillAppear(false)
+        webView.reload()
+        ViewController.alwaysHideButton = false
+        showTabBar()
+        sender.endRefreshing()
+    }
+    
+    func hideTabBar() {
+        if UIApplication.shared.windows.first == nil {
+            return
+        }
+        var viewController = UIApplication.shared.windows.first!.rootViewController
+        if let nc = viewController as? UINavigationController {
+            viewController = nc.viewControllers.first
+        }
+        if ViewController.middleButton.isDescendant(of: viewController!.view) {
+            DispatchQueue.main.async {
+                if ViewController.isExpandButton {
+                    ViewController.expandButton()
+                }
+                ViewController.hideDockedButton()
+                if let viewController = viewController as? ViewController {
+                    viewController.tabBar.isHidden = true
+                }
+                ViewController.removeMiddleButton()
+            }
+        } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+            DispatchQueue.main.async {
+                if let viewController = viewController as? ViewController {
+                    if !viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = true
+                    }
+                }
+            }
+        }
+    }
+
+    func showTabBar() {
+        if(ViewController.alwaysHideButton){
+            return
+        }
+        var viewController = UIApplication.shared.windows.first!.rootViewController
+        if let nc = viewController as? UINavigationController {
+            viewController = nc.viewControllers.first
+        }
+        if ViewController.middleButton.isHidden {
+            if let viewController = viewController as? ViewController {
+                viewController.tabBar.isHidden = false
+                ViewController.middleButton.isHidden = false
+            }
+        } else if PrefsUtil.getCpaasMode() != PrefsUtil.CPAAS_MODE_DOCKED {
+            DispatchQueue.main.async {
+                if let viewController = viewController as? ViewController {
+                    if viewController.tabBar.isHidden {
+                        viewController.tabBar.isHidden = false
+                    }
+                }
+            }
+        }
+    }
+
+    func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
+        scrollView.pinchGestureRecognizer?.isEnabled = false
+    }
+
+    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        if message.name == "checkProfile" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? String else {
+                return
+            }
+            if ViewController.checkIsChangePerson() {
+                if param2 == "like" {
+                    self.webView.evaluateJavaScript("likeProduct('\(param1)',1,true);")
+                } else if param2 == "comment" {
+                    self.webView.evaluateJavaScript("openComment('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
+                } else if param2 == "report_user" {
+                    self.webView.evaluateJavaScript("reportUser('\(param1)',true);")
+                } else if param2 == "report_content" {
+                    self.webView.evaluateJavaScript("reportContent('\(param1.split(separator: "|")[0])','\(param1.split(separator: "|")[1])',true);")
+                } else if param2 == "block_user" {
+                    self.webView.evaluateJavaScript("blockUser('\(param1)',true);")
+                } else if param2 == "follow_user" {
+                    self.webView.evaluateJavaScript("followUser('\(param1.split(separator: "|")[0])',\(param1.split(separator: "|")[1]),true);")
+                } else if param2 == "homepage" || param2 == "gif" {
+                    self.webView.evaluateJavaScript("window.location.href = '\(param1)';")
+                } else if param2 == "block_content" {
+                    self.webView.evaluateJavaScript("blockContent('\(param1)',true);")
+                } else {
+                    self.webView.evaluateJavaScript("openNewPost(true);")
+                }
+            } else {
+                self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
+            }
+        } else if message.name == "setIsProductModalOpen" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Bool else {
+                return
+            }
+            if param1 {
+                if self.webView.scrollView.contentOffset.y < 0 { // Move tableView to top
+                    self.webView.scrollView.setContentOffset(CGPoint.zero, animated: true)
+                }
+            }
+            WebView6.showModal = param1
+        } else if message.name == "toggleVoiceSearch" {
+            if !isAllowSpeech {
+                setupSpeech()
+            } else {
+                runVoice()
+            }
+        } else if message.name == "blockUser" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? Bool else {
+                return
+            }
+            if param2 {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "1"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            } else {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "0"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            }
+        } else if message.name == "showAlert" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String else {
+                return
+            }
+            self.view.makeToast(param1, duration: 3)
+        } else if message.name == "blockUser" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String,
+                  let param2 = dict["param2"] as? Bool else {
+                return
+            }
+            if param2 {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "1"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            } else {
+                DispatchQueue.global().async {
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getUnBlock(l_pin: param1)) {
+                        if response.isOk() {
+                            DispatchQueue.main.async {
+                                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                                    _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                                        "ex_block" : "0"
+                                    ], _where: "f_pin = '\(param1)'")
+                                })
+                            }
+                        }
+                    }
+                }
+            }
+        } else if message.name == "tabShowHide" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Bool else {
+                return
+            }
+            if param1 {
+                ViewController.alwaysHideButton = false
+                showTabBar()
+            } else {
+                if self.viewIfLoaded?.window != nil {
+                    ViewController.alwaysHideButton = true
+                    hideTabBar()
+                }
+            }
+        } else if message.name == "shareText" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String else {
+                return
+            }
+            if loadingURL {
+                return
+            }
+            let activityViewController = UIActivityViewController(activityItems: [param1], applicationActivities: nil)
+            self.present(activityViewController, animated: true, completion: nil)
+        } else if message.name == "openGalleryiOS" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? Int else {
+                return
+            }
+            indexImageVideoWv = param1
+            let alertController = LibAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
+            
+            if let action = self.actionImageVideo(for: "image", title: "Choose Photo".localized()) {
+                alertController.addAction(action)
+            }
+            if let action = self.actionImageVideo(for: "video", title: "Choose Video".localized()) {
+                alertController.addAction(action)
+            }
+            alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
+            self.present(alertController, animated: true)
+        } else if message.name == "openTabChat" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabChat()
+                }
+            }
+        } else if message.name == "openTabWebview3" {
+            if let nc = UIApplication.shared.windows.first?.rootViewController as? UINavigationController {
+                if let vc = nc.topViewController as? ViewController {
+                    vc.openTabWebview3()
+                }
+            }
+        } else if message.name == "openCallAudio" {
+            APIS.openAudioCall()
+        } else if message.name == "openCallVideo" {
+            APIS.openVideoCall()
+        } else if message.name == "openChatSmartbot" {
+            APIS.openSmartChatbot()
+        } else if message.name == "openPanicDialog" {
+            
+        } else if message.name == "openContactCenter" {
+            APIS.openContactCenter()
+        } else if message.name == "openContactCenterChat" {
+            APIS.openContactCenter(media: 0)
+        } else if message.name == "openContactCenterAudioCall" {
+            APIS.openContactCenter(media: 1)
+        } else if message.name == "openContactCenterVideoCall" {
+            APIS.openContactCenter(media: 2)
+        }
+    }
+    
+    private func actionImageVideo(for type: String, title: String) -> UIAlertAction? {
+        return UIAlertAction(title: title, style: .default) { [unowned self] _ in
+            switch type {
+            case "image":
+                imageVideoPicker.present(source: .imageAlbum)
+            case "video":
+                imageVideoPicker.present(source: .videoAlbum)
+            default:
+                imageVideoPicker.present(source: .imageAlbum)
+            }
+        }
+    }
+    
+    func didSelect(imagevideo: Any?) {
+        if imagevideo != nil {
+            let imageData = imagevideo! as! [UIImagePickerController.InfoKey : Any]
+            if (imageData[.mediaType] as! String == "public.image") {
+                let compressedImage = (imageData[.originalImage] as! UIImage).pngData()!
+                let base64String = compressedImage.base64EncodedString()
+                let base64ToWeb = "data:image/jpeg;base64,\(base64String)"
+                webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
+                    if let error = error {
+                        print("Error executing JavaScript: \(error)")
+                    }
+                }
+            } else {
+                guard var dataVideo = try? Data(contentsOf: imageData[.mediaURL] as! URL) else {
+                    return
+                }
+                let sizeOfVideo = Double(dataVideo.count / 1048576)
+                if (sizeOfVideo > 10.0) {
+                    let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
+                    compressVideo(inputURL: imageData[.mediaURL] as! URL,
+                                  outputURL: compressedURL) { exportSession in
+                        guard let session = exportSession else {
+                            return
+                        }
+                        
+                        switch session.status {
+                        case .unknown:
+                            break
+                        case .waiting:
+                            break
+                        case .exporting:
+                            break
+                        case .completed:
+                            guard let compressedData = try? Data(contentsOf: compressedURL) else {
+                                return
+                            }
+                            dataVideo = compressedData
+                        case .failed:
+                            break
+                        case .cancelled:
+                            break
+                        @unknown default:
+                            break
+                        }
+                    }
+                }
+                let base64String = dataVideo.base64EncodedString()
+                let base64ToWeb = "data:video/mp4;base64,\(base64String)"
+                webView.evaluateJavaScript("loadFromMobile('\(base64ToWeb)',\(indexImageVideoWv))") { (result, error) in
+                    if let error = error {
+                        print("Error executing JavaScript: \(error)")
+                    }
+                }
+            }
+        }
+    }
+    
+    func compressVideo(inputURL: URL,
+                       outputURL: URL,
+                       handler:@escaping (_ exportSession: AVAssetExportSession?) -> Void) {
+        let urlAsset = AVURLAsset(url: inputURL, options: nil)
+        guard let exportSession = AVAssetExportSession(asset: urlAsset,
+                                                       presetName: AVAssetExportPresetMediumQuality) else {
+            handler(nil)
+            
+            return
+        }
+        
+        exportSession.outputURL = outputURL
+        exportSession.outputFileType = .mp4
+        exportSession.exportAsynchronously {
+            handler(exportSession)
+        }
+    }
+    
+    func setupSpeech() {
+
+        self.speechRecognizer?.delegate = self
+
+        SFSpeechRecognizer.requestAuthorization { (authStatus) in
+
+            var isButtonEnabled = false
+
+            switch authStatus {
+            case .authorized:
+                isButtonEnabled = true
+
+            case .denied:
+                isButtonEnabled = false
+                //print("User denied access to speech recognition")
+
+            case .restricted:
+                isButtonEnabled = false
+                //print("Speech recognition restricted on this device")
+
+            case .notDetermined:
+                isButtonEnabled = false
+                //print("Speech recognition not yet authorized")
+            @unknown default:
+                isButtonEnabled = false
+            }
+
+            OperationQueue.main.addOperation() {
+                self.isAllowSpeech = isButtonEnabled
+                if isButtonEnabled {
+                    SecureUserDefaults.shared.set(isButtonEnabled, forKey: "allowSpeech")
+                    self.runVoice()
+                }
+            }
+        }
+    }
+    
+    func runVoice() {
+        if !audioEngine.isRunning {
+            alertController = LibAlertController(title: "Start Recording".localized(), message: "Say something, I'm listening!".localized(), preferredStyle: .alert)
+            self.present(alertController, animated: true)
+            self.webView.evaluateJavaScript("toggleVoiceButton(true)")
+            self.startRecording()
+        }
+    }
+    
+    func startRecording() {
+
+        // Clear all previous session data and cancel task
+        if recognitionTask != nil {
+            recognitionTask?.cancel()
+            recognitionTask = nil
+        }
+
+        // Create instance of audio session to record voice
+        let audioSession = AVAudioSession.sharedInstance()
+        do {
+            try audioSession.setCategory(AVAudioSession.Category.record, mode: .default, options: [])
+            try audioSession.setMode(AVAudioSession.Mode.measurement)
+            try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
+        } catch {
+            //print("audioSession properties weren't set because of an error.")
+        }
+
+        self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
+
+        let inputNode = audioEngine.inputNode
+
+        guard let recognitionRequest = recognitionRequest else {
+            fatalError("Unable to create an SFSpeechAudioBufferRecognitionRequest object")
+        }
+
+        recognitionRequest.shouldReportPartialResults = true
+
+        self.recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in
+
+            var isFinal = false
+            var text = ""
+
+            if result != nil {
+                text = result?.bestTranscription.formattedString ?? ""
+                isFinal = (result?.isFinal)!
+                self.alertController.dismiss(animated: true)
+                self.audioEngine.stop()
+                self.recognitionRequest?.endAudio()
+            } else {
+                self.alertController.dismiss(animated: true)
+            }
+
+            if error != nil || isFinal {
+                if error == nil {
+                    self.webView.evaluateJavaScript("toggleVoiceButton(false)")
+                    self.webView.evaluateJavaScript("submitVoiceSearch('\(text)')")
+                } else {
+                    self.audioEngine.stop()
+                    self.recognitionRequest?.endAudio()
+                }
+                inputNode.removeTap(onBus: 0)
+
+                self.recognitionRequest = nil
+                self.recognitionTask = nil
+
+                self.isAllowSpeech = true
+            }
+        })
+
+        let recordingFormat = inputNode.outputFormat(forBus: 0)
+        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
+            self.recognitionRequest?.append(buffer)
+        }
+
+        self.audioEngine.prepare()
+
+        do {
+            try self.audioEngine.start()
+        } catch {
+            //print("audioEngine couldn't start because of an error.")
+        }
+    }
+    
+    func isUsingMyWebview() -> Bool{
+        return PrefsUtil.getURLWV6() == "0" || PrefsUtil.getURLWV6() == "1" || PrefsUtil.getURLWV6() == "2" || PrefsUtil.getURLWV6() == "3" || PrefsUtil.getURLWV6() == "4"
+    }
+
+}
+
+extension WebView6: SFSpeechRecognizerDelegate {
+
+    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
+        if available {
+            self.isAllowSpeech = true
+        } else {
+            self.isAllowSpeech = false
+        }
+    }
+}
+
+extension WebView6: WKUIDelegate, WKNavigationDelegate {
+    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
+        guard let url = navigationAction.request.url else {
+            decisionHandler(.cancel)
+            return
+        }
+        
+        if let scheme = url.scheme?.lowercased(), scheme.hasPrefix("itms") || scheme == "mailto" || scheme == "tel" {
+            DispatchQueue.main.async {
+                UIApplication.shared.open(url, options: [:], completionHandler: nil)
+            }
+            decisionHandler(.cancel)
+            return
+        }
+
+        guard navigationAction.targetFrame?.isMainFrame == true else {
+            decisionHandler(.allow)
+            return
+        }
+
+        if loadingURL {
+            decisionHandler(.cancel)
+            return
+        }
+
+        loadingURL = true
+
+        if allowedURLs.contains(url.absoluteString) {
+            loadingURL = false
+            decisionHandler(.allow)
+            return
+        }
+        
+        validateSSLCertificate(url: url) { [weak self] isValid in
+            guard let self = self else {
+                decisionHandler(.cancel)
+                return
+            }
+
+            DispatchQueue.main.async {
+                if isValid {
+                    self.allowedURLs.insert(url.absoluteString)
+                    self.loadingURL = false
+                    decisionHandler(.allow)
+                } else {
+                    let host = url.host ?? ""
+                    var messageText = "You're about to access a website that is not currently trusted by your Nexilis Browser. This website's security certificate is not recognized.\n\nDo you wish to proceed to <<domain>> and trust the website's security certificate?\n\nNote: Adding a website to the trusted list may increase your risk of security vulnerability".localized()
+                    messageText = messageText.replacingOccurrences(of: "<<domain>>", with: host)
+
+                    let alert = UIAlertController(title: "Warning Unknown Url!".localized(),
+                                                  message: messageText,
+                                                  preferredStyle: .alert)
+
+                    alert.addAction(UIAlertAction(title: "Yes", style: .default) { _ in
+                        let storedCertificate = Utils.getCertificatePinningWebview()
+                        if let jsonData = storedCertificate.data(using: .utf8),
+                           let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
+                            var certJson = certJson
+                            certJson[host] = self.blockedCertificate
+                            if let jsonData = try? JSONSerialization.data(withJSONObject: certJson, options: []),
+                               let jsonString = String(data: jsonData, encoding: .utf8) {
+                                Utils.setCertificatePinningWebview(value: jsonString)
+                            }
+                        }
+
+                        self.allowedURLs.insert(url.absoluteString)
+                        self.loadingURL = false
+                        decisionHandler(.allow)
+                    })
+
+                    alert.addAction(UIAlertAction(title: "No", style: .cancel) { _ in
+                        self.loadingURL = false
+                        decisionHandler(.cancel)
+                    })
+
+                    if self.presentedViewController == nil {
+                        self.present(alert, animated: true, completion: nil)
+                    } else {
+                        self.loadingURL = false
+                        decisionHandler(.cancel)
+                    }
+                }
+            }
+        }
+    }
+    
+    private func validateSSLCertificate(url: URL, completion: @escaping (Bool) -> Void) {
+        let session = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil)
+        let request = URLRequest(url: url)
+
+        let task = session.dataTask(with: request) { _, response, error in
+            if let error = error {
+                completion(false)
+                return
+            }
+            completion(true)
+        }
+        task.resume()
+    }
+}
+
+extension WebView6: URLSessionDelegate {
+    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+        guard let serverTrust = challenge.protectionSpace.serverTrust else {
+            completionHandler(.cancelAuthenticationChallenge, nil)
+            return
+        }
+        if let publicKeyHash = extractPublicKeyHash(from: serverTrust) {
+            let domain = challenge.protectionSpace.host
+            let storedCertificate = Utils.getCertificatePinningWebview()
+            if let jsonData = storedCertificate.data(using: .utf8),
+               let certJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
+                if publicKeyHash == certJson[domain] {
+                    completionHandler(.useCredential, URLCredential(trust: serverTrust))
+                } else {
+                    blockedCertificate = publicKeyHash
+                    completionHandler(.cancelAuthenticationChallenge, nil)
+                }
+            }
+        } else {
+            completionHandler(.cancelAuthenticationChallenge, nil)
+        }
+    }
+    
+    func extractPublicKeyHash(from serverTrust: SecTrust) -> String? {
+        guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else { return nil }
+        guard let publicKey = SecCertificateCopyKey(certificate) else { return nil }
+        
+        var error: Unmanaged<CFError>?
+        guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
+            return nil
+        }
+        
+        // Compute SHA-256 hash
+        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
+        publicKeyData.withUnsafeBytes {
+            _ = CC_SHA256($0.baseAddress, CC_LONG(publicKeyData.count), &hash)
+        }
+        
+        let hashData = Data(hash)
+        let base64Hash = hashData.base64EncodedString()
+        
+        return base64Hash
+    }
+}

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

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

二进制
NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_ic_attach_spc_badge.imageset/pb_ic_attach_spc_badge.png


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

@@ -645,7 +645,13 @@ public class APIS: NSObject {
     private static func signUpSignInMFA(method: String, userId: String, policyLevel: String) {
         Nexilis.showLoader()
         DispatchQueue.global().async {
-            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpSignInAPI(p_name: userId, p_password: ""), timeout: 15 * 1000) {
+            var id = ""
+            if Utils.isMiddleMode() || Utils.isHSAMode() {
+                id = Nexilis.justInit()
+            } else {
+                id = User.getMyPin() ?? ""
+            }
+            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpSignInAPI(p_name: userId, p_password: "", xPin: id), timeout: 15 * 1000) {
                 if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "20" {
                     DispatchQueue.main.async {
                         Nexilis.hideLoader {
@@ -813,7 +819,13 @@ public class APIS: NSObject {
                         }
                         return
                     }
-                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
+                    var id = ""
+                    if Utils.isMiddleMode() || Utils.isHSAMode() {
+                        id = Nexilis.justInit()
+                    } else {
+                        id = User.getMyPin() ?? ""
+                    }
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
                         if response.isOk() {
                             let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
                             if data.isEmpty {
@@ -2264,20 +2276,14 @@ public class APIS: NSObject {
             self.notifTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: false) { _ in
                 stopNotif = false
             }
-            do {
-                if !Nexilis.afterConnect && API.nGetCLXConnState() == 0 {
-                    let id = Utils.getConnectionID()
-                    try API.initConnection(sAPIK: Nexilis.sAPIKey, cbiI: Callback(), sTCPAddr: Nexilis.ADDRESS, nTCPPort: Nexilis.PORT, sUserID: id, sStartWH: "09:00")
-                }
-//                listIdentifierNotif.removeAll()
-                Nexilis.afterConnect = false
-            } catch {
+            if !Utils.isHSAMode() && !Utils.isMiddleMode(){
+                _ = Nexilis.justInit(isChecking: true)
             }
         }
         checkDataForShareExtension()
         UIApplication.shared.applicationIconBadgeNumber = 0
         UNUserNotificationCenter.current().removeAllDeliveredNotifications()
-        if Utils.getSecureFolderOffline() == "0" && afterEnterBackground && Database.shared.database == nil && Utils.getSetProfile() {
+        if Utils.getSecureFolderOffline() == "0" && afterEnterBackground && Database.shared.database == nil && Utils.getSetProfile() && !Utils.isHSAMode() {
             Database.recreateInstance()
             NotificationCenter.default.post(name: NSNotification.Name(rawValue: "disconnected_nexilis"), object: nil, userInfo: nil)
             if let navigationC = UIApplication.shared.visibleViewController as? UINavigationController {
@@ -2304,6 +2310,22 @@ public class APIS: NSObject {
                 }
                 NotificationCenter.default.post(name: NSNotification.Name(rawValue: "checkNewMessagesNexilis"), object: nil, userInfo: nil)
             }
+            
+            DispatchQueue.global(qos: .userInitiated).async {
+                if Utils.shouldRequestAuthentication() && Utils.getSetProfile() && (Utils.isMiddleMode() || Utils.isHSAMode()) && Nexilis.hasInit {
+                    DispatchQueue.main.async {
+                        var viewController = UIApplication.shared.windows.first?.rootViewController
+                        var notNull = false
+                        while !notNull {
+                            viewController = UIApplication.shared.windows.first?.rootViewController
+                            if viewController != nil {
+                                notNull = true
+                            }
+                        }
+                        Nexilis.showPassSignIn()
+                    }
+                }
+            }
         }
         afterEnterBackground = true
     }
@@ -2786,6 +2808,8 @@ public class APIS: NSObject {
                                                 }
                                             }
                                         }
+                                    } else {
+                                        sendIt()
                                     }
                                     func sendIt() {
                                         message = CoreMessage_TMessageBank.sendMessage(l_pin: groupId.isEmpty ? idContact : groupId, message_scope_id: scopeId, status: scopeId == "3" ? "1" : "2", message_text: data, credential: "0", attachment_flag: attachmentFlag, ex_blog_id: "", message_large_text: "", ex_format: "", image_id: imageId, audio_id: renamedAudioId, video_id: videoId, file_id: renamedFileId, thumb_id: thumb, reff_id: "", read_receipts: "4", chat_id: chatId, is_call_center: "0", call_center_id: "", opposite_pin: scopeId == "3" ? (User.getMyPin() ?? "") : idContact, gif_id: "", isForwarded: "0", isSecret: "0", specFile: "")
@@ -3021,6 +3045,10 @@ public class APIS: NSObject {
         }
     }
     
+    public static func setAppMode(mode: Int) {
+        Utils.selectedAppMode = mode
+    }
+    
     public static func checkSignMethod() -> (Int, Int) {
         var countMethod = 0
         var typeMethod = 0

+ 35 - 14
NexilisLite/NexilisLite/Source/CoreMessage_TMessageBank.swift

@@ -41,6 +41,7 @@ public class CoreMessage_TMessageBank {
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_APP_NAME] = APIS.getAppNm()
         tmessage.mBodies[CoreMessage_TMessageKey.CPAAS_VERSION] = Utils.CPAAS_VERSION
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_PACKAGE_NAME] = (Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String) ?? ""
+        tmessage.mBodies[CoreMessage_TMessageKey.API] = Nexilis.sAPIKey
         tmessage.mPIN = p_pin
         return tmessage
     }
@@ -55,6 +56,7 @@ public class CoreMessage_TMessageBank {
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_APP_NAME] = APIS.getAppNm()
         tmessage.mBodies[CoreMessage_TMessageKey.CPAAS_VERSION] = Utils.CPAAS_VERSION
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_PACKAGE_NAME] = (Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String) ?? ""
+        tmessage.mBodies[CoreMessage_TMessageKey.API] = Nexilis.sAPIKey
 //        tmessage.mBodies[CoreMessage_TMessageKey.BUSINESS_ENTITY] = "74"
         return tmessage
     }
@@ -1425,12 +1427,11 @@ public class CoreMessage_TMessageBank {
         return tmessage
     }
     
-    public static func getSendOTPLogin(p_email: String = "", p_number: String = "") -> TMessage {
-        let me = User.getMyPin()!
+    public static func getSendOTPLogin(p_email: String = "", p_number: String = "", xpin: String) -> TMessage {
         let tmessage = TMessage()
         tmessage.mCode = CoreMessage_TMessageCode.SEND_OTP_LOGIN
         tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
-        tmessage.mPIN = me
+        tmessage.mPIN = xpin
         if !p_email.isEmpty {
             tmessage.mBodies[CoreMessage_TMessageKey.EMAIL] = p_email
         }
@@ -1440,15 +1441,15 @@ public class CoreMessage_TMessageBank {
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_APP_NAME] = APIS.getAppNm()
         tmessage.mBodies[CoreMessage_TMessageKey.CPAAS_VERSION] = Utils.CPAAS_VERSION
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_PACKAGE_NAME] = (Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String) ?? ""
+        tmessage.mBodies[CoreMessage_TMessageKey.API] = Nexilis.sAPIKey
         return 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()!
+    public static func getSendVerifyChangeDevice(p_email: String, p_vercode: String, xpin: String, number: String = "", deviceFingerprint: String = "", publicKey: String = "", signature: String = "", totp: String = "") -> TMessage {
         let tmessage = TMessage()
-        tmessage.mCode = CoreMessage_TMessageCode.SEND_VERIFY_LOGIN
+        tmessage.mCode = CoreMessage_TMessageCode.SEND_VERIFY_LOGIN_V2
         tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
-        tmessage.mPIN = me
+        tmessage.mPIN = xpin
         tmessage.mBodies[CoreMessage_TMessageKey.EMAIL] = p_email
         tmessage.mBodies[CoreMessage_TMessageKey.OTP] = p_vercode
         tmessage.mBodies[CoreMessage_TMessageKey.PHONE_NUMBER] = number
@@ -1456,6 +1457,7 @@ public class CoreMessage_TMessageBank {
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_APP_NAME] = APIS.getAppNm()
         tmessage.mBodies[CoreMessage_TMessageKey.CPAAS_VERSION] = Utils.CPAAS_VERSION
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_PACKAGE_NAME] = (Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String) ?? ""
+        tmessage.mBodies[CoreMessage_TMessageKey.API] = Nexilis.sAPIKey
         if !deviceFingerprint.isEmpty {
             tmessage.mBodies[CoreMessage_TMessageKey.FINGERPRINT] = deviceFingerprint
         }
@@ -1471,6 +1473,21 @@ public class CoreMessage_TMessageBank {
         return tmessage;
     }
     
+    public static func getSetPassword(pPassword: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = User.getMyPin()!
+        tmessage.mCode = CoreMessage_TMessageCode.SET_PASSWORD_SIGN_UP
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_ID] = Utils.M_USER_ANDROID_ID
+        tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_PACKAGE_NAME] = (Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String) ?? ""
+        tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_APP_NAME] = APIS.getAppNm()
+        tmessage.mBodies[CoreMessage_TMessageKey.CPAAS_VERSION] = Utils.CPAAS_VERSION
+        tmessage.mBodies[CoreMessage_TMessageKey.PSWD] = pPassword
+        tmessage.mBodies[CoreMessage_TMessageKey.API] = Nexilis.sAPIKey
+        return tmessage
+    }
+    
     public static func getChangePersonMSISDN(msisdn: String) -> TMessage {
         let me = User.getMyPin()!
         let tmessage = TMessage()
@@ -2229,7 +2246,7 @@ public class CoreMessage_TMessageBank {
     
     public static func pullFloatingButton() -> TMessage {
         let tmessage = TMessage()
-        let me = User.getMyPin()!
+        let me = User.getMyPin() ?? ""
         tmessage.mCode = CoreMessage_TMessageCode.PULL_FLOATING_BUTTON
         tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
         tmessage.mPIN = me
@@ -2330,17 +2347,19 @@ public class CoreMessage_TMessageBank {
         return tmessage
     }
     
-    public static func getSignUpSignInAPI(p_name: String, p_password: String, deviceFingerprint: String = "", publicKey: String = "", signature: String = "", totp: String = "") -> TMessage {
+    public static func getSignUpSignInAPI(p_name: String, p_password: String, xPin: String, deviceFingerprint: String = "", publicKey: String = "", signature: String = "", totp: String = "", forceSU: String = "") -> TMessage {
         let tmessage = TMessage()
-        tmessage.mCode = CoreMessage_TMessageCode.SIGN_UP_AND_SIGN_IN_API
+        tmessage.mCode = CoreMessage_TMessageCode.SIGN_UP_AND_SIGN_IN_API_V2
         tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
-        tmessage.mPIN = User.getMyPin()!
+        tmessage.mPIN = xPin
         tmessage.mBodies[CoreMessage_TMessageKey.NAME] = p_name
         tmessage.mBodies[CoreMessage_TMessageKey.PSWD] = p_password
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_ID] = Utils.M_USER_ANDROID_ID
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_APP_NAME] = APIS.getAppNm()
         tmessage.mBodies[CoreMessage_TMessageKey.CPAAS_VERSION] = Utils.CPAAS_VERSION
         tmessage.mBodies[CoreMessage_TMessageKey.ANDROID_PACKAGE_NAME] = (Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String) ?? ""
+        tmessage.mBodies[CoreMessage_TMessageKey.API] = Nexilis.sAPIKey
+        tmessage.mBodies[CoreMessage_TMessageKey.SUBMIT_DATE] = "\(Date().currentTimeMillis())"
         if !deviceFingerprint.isEmpty {
             tmessage.mBodies[CoreMessage_TMessageKey.FINGERPRINT] = deviceFingerprint
         }
@@ -2353,6 +2372,9 @@ public class CoreMessage_TMessageBank {
         if !totp.isEmpty {
             tmessage.mBodies[CoreMessage_TMessageKey.TOTP] = totp
         }
+        if !forceSU.isEmpty {
+            tmessage.mBodies[CoreMessage_TMessageKey.FORCE_LOGIN_AUTH] = forceSU
+        }
         return tmessage
     }
     
@@ -2774,10 +2796,9 @@ public class CoreMessage_TMessageBank {
         return tMessage
     }
     
-    public static func getChalanger() -> TMessage {
+    public static func getChalanger(xPin: String) -> TMessage {
         let tMessage = NexilisLite.TMessage()
-        let me = User.getMyPin() ?? ""
-        tMessage.mPIN = me
+        tMessage.mPIN = xPin
         tMessage.mCode = CoreMessage_TMessageCode.AUTH_REQUEST
         tMessage.mStatus = CoreMessage_TMessageUtil.getTID()
         return tMessage

+ 3 - 0
NexilisLite/NexilisLite/Source/CoreMessage_TMessageCode.swift

@@ -654,6 +654,8 @@ public class CoreMessage_TMessageCode {
     public static let SEND_SIGNIN_OTP = "SSI01A";
     public static let SEND_SIGNUP_OTP = "SSU01O";
     public static let SEND_VERIFY_LOGIN = "SVL";
+    public static let SEND_VERIFY_LOGIN_V2 = "SVL2";
+    public static let SET_PASSWORD_SIGN_UP = "SHP";
     public static let SEND_OTP_LOGIN = "SOTL";
     
     //Whiteboard
@@ -752,6 +754,7 @@ public class CoreMessage_TMessageCode {
     public static let MOBILE_INQUIRY = "MINQ";
     
     public static let SIGN_UP_AND_SIGN_IN_API = "SUAI01";
+    public static let SIGN_UP_AND_SIGN_IN_API_V2 = "SUAI02";
     public static let BACKUP_AVAILABILITY = "BUA2";
     public static let BACKUP_UPLOADED = "BUU2";
     public static let BACKUP_RESTORED = "BUR2";

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

@@ -40,6 +40,7 @@ public class CoreMessage_TMessageKey {
     public static let N_MEMBERS      = "Bl"
     public static let PSWD            = "Bm"
     public static let PSWD_OLD      = "Bm0";
+    public static let SHOW_PSWD = "Bm1";
     public static let N_DOWNLOADS    = "Bn"
     public static let DEVICE_BRAND   = "Bo"
     public static let DEVICE_MODEL   = "Bp"
@@ -527,5 +528,7 @@ public class CoreMessage_TMessageKey {
     public static let ACT_TIME = "ACTMS";
     
     public static let TOTP = "TOTP";
+    public static let SUBMIT_DATE = "SD01";
+    public static let FORCE_LOGIN_AUTH = "FLA";
     public static let IS_BOT = "ibot";
 }

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

@@ -1612,12 +1612,12 @@ public class ImageCache {
         for (_, sanitizedKey) in cacheKeyMap {
             if isGif {
                 if let gifData = cacheGif.object(forKey: sanitizedKey as NSString) {
-                    try? FileEncryption.shared.writeSecure(filename: "\(sanitizedKey).gif", data: gifData as Data)
+                    try? FileEncryption.shared.writeSecure(filename: "\(sanitizedKey).gif", data: gifData as Data, withoutBiometric: true)
                 }
             } else {
                 if let image = cache.object(forKey: sanitizedKey as NSString),
                    let imageData = image.pngData() {
-                    try? FileEncryption.shared.writeSecure(filename: "\(sanitizedKey).png", data: imageData)
+                    try? FileEncryption.shared.writeSecure(filename: "\(sanitizedKey).png", data: imageData, withoutBiometric: true)
                 }
             }
         }
@@ -1641,7 +1641,7 @@ public class ImageCache {
             guard FileEncryption.shared.isSecureExists(filename: fileName) else { continue }
 
             do {
-                if var data = try FileEncryption.shared.readSecure(filename: fileName) {
+                if var data = try FileEncryption.shared.readSecure(filename: fileName, withoutBiometric: true) {
                     if let decrypted = FileEncryption.shared.decryptFileFromServer(data: data) {
                         data = decrypted
                     }

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

@@ -18,7 +18,7 @@ public class FileEncryption {
 
     private init() {}
     
-    public func readSecure(filename: String) throws -> Data? {
+    public func readSecure(filename: String, withoutBiometric: Bool = false) throws -> Data? {
         if Utils.getFeatureAccess().isEmpty {
             return nil
         }
@@ -28,12 +28,12 @@ public class FileEncryption {
 
         let encryptedData = try Data(contentsOf: fileURL)
         let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
-        let decryptedData = try AES.GCM.open(sealedBox, using: MasterKeyUtil.shared.getMasterKey())
+        let decryptedData = try AES.GCM.open(sealedBox, using: MasterKeyUtil.shared.getMasterKey(withoutBiometric: withoutBiometric))
 
         return decryptedData
     }
     
-    public func writeSecure(filename: String? = nil, data: Data? = nil) throws {
+    public func writeSecure(filename: String? = nil, data: Data? = nil, withoutBiometric: Bool = false) throws {
         do {
             let fileManager = FileManager.default
 
@@ -50,7 +50,7 @@ public class FileEncryption {
             let fileURL = secureDir.appendingPathComponent(filename ?? "")
 
             // Encrypt the data
-            let sealedBox = try AES.GCM.seal(data ?? Data(), using: MasterKeyUtil.shared.getMasterKey())
+            let sealedBox = try AES.GCM.seal(data ?? Data(), using: MasterKeyUtil.shared.getMasterKey(withoutBiometric: withoutBiometric))
             let encryptedData = sealedBox.combined!
 
             // Write encrypted data to file

+ 17 - 9
NexilisLite/NexilisLite/Source/IncomingThread.swift

@@ -811,14 +811,27 @@ class IncomingThread {
         if message.mBodies["message_id"] != nil {
             messageId = message.getBody(key: "message_id")
         }
+        if !messageId.contains("'") {
+            messageId = "'\(messageId)'"
+        }
         let type = message.getBody(key: CoreMessage_TMessageKey.DELETE_MESSAGE_FLAG)
         
+        var messageExist = false
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select last_edited from MESSAGE where message_id = \(messageId)"), cursor.next() {
+                messageExist = true
+                cursor.close()
+            }
+        })
+        if !messageExist {
+            let messageToSave = message
+            messageToSave.mBodies[CoreMessage_TMessageKey.MESSAGE_ID] = messageId.replacingOccurrences(of: "'", with: "")
+            Nexilis.saveMessage(message: message, withStatus: false)
+        }
+        
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
                 if type == "1" {
-                    if !messageId.contains("'") {
-                        messageId = "'\(messageId)'"
-                    }
                     _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
                         "message_text" : "🚫 _This message was deleted_",
                         "lock" : "1",
@@ -1335,12 +1348,7 @@ class IncomingThread {
     private func receiveMessage(message: TMessage) -> Void {
 //        print("receive message \(message.toLogString())")
         if Utils.getSecureFolderOffline() == "0" {
-            if API.nGetCLXConnState() == 0 {
-                do {
-                    let id = Utils.getConnectionID()
-                    try API.initConnection(sAPIK: Nexilis.sAPIKey, cbiI: Callback(), sTCPAddr: Nexilis.ADDRESS, nTCPPort: Nexilis.PORT, sUserID: id, sStartWH: "09:00")
-                } catch {}
-            }
+            _ = Nexilis.justInit(isChecking: true)
             if FileEncryption.shared.aesKey == nil {
                 IncomingThread.dispatch = DispatchGroup()
                 IncomingThread.dispatch?.enter()

+ 5 - 4
NexilisLite/NexilisLite/Source/MasterKeyUtil.swift

@@ -88,12 +88,12 @@ public class MasterKeyUtil {
     
     private let masterKeyQueue = DispatchQueue(label: "io.nexilis.masterKeyQueue")
 
-    func getMasterKey() throws -> SymmetricKey {
+    func getMasterKey(withoutBiometric: Bool = false) throws -> SymmetricKey {
         var retrievedKey: SymmetricKey?
         var thrownError: Error?
 
         masterKeyQueue.sync {
-            if Nexilis.checkingAccess(key: "authentication") && isDeviceNotSecure() {
+            if Nexilis.checkingAccess(key: "authentication") && isDeviceNotSecure() && !withoutBiometric {
                 let semaphore = DispatchSemaphore(value: 0)
                 var result = false
 
@@ -379,12 +379,12 @@ class KeyManagerNexilis {
         return [0x80 | UInt8(bytes.count)] + bytes
     }
     
-    static func getPrivateKey(useBiometric: Bool = true) -> SecKey? {
+    static func getPrivateKey(useBiometric: Bool = true, isSaveState: Bool = false) -> SecKey? {
         if useBiometric {
             let semaphore = DispatchSemaphore(value: 0)
             var result = false
 
-            Utils.authenticateWithBiometrics { success, errorMessage in
+            Utils.authenticateWithBiometrics(isSaveState: isSaveState) { success, errorMessage in
                 if success {
                     print("Access granted!")
                     result = true
@@ -497,6 +497,7 @@ class BiometricStateManager {
                                    localizedReason: "Validasi biometric anda!") { success, _ in
                 if success, let currentState = context.evaluatedPolicyDomainState,
                    let savedState = Utils.getBiometricState() {
+                    SecureUserDefaults.shared.set(Date(), forKey: "lastAuthenticationTime")
                     completion(savedState == currentState, 1)
                 } else {
                     completion(false, 0)

+ 333 - 224
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -19,7 +19,7 @@ import CryptoKit
 import WebKit
 
 public class Nexilis: NSObject {
-    public static var cpaasVersion = "5.0.72"
+    public static var cpaasVersion = "5.0.73"
     public static var sAPIKey = ""
     
     public static var ADDRESS = ""
@@ -61,8 +61,8 @@ public class Nexilis: NSObject {
     public static var defaultFloatingButton: [Int] = []
     
     public static var showButtonFB = false
-    public static var isShowForceSignIn = true
-    public static var afterConnect = true
+    
+    static var hasInit = false
     
     public static var imageCache = NSCache<NSString, UIImage>()
     
@@ -168,53 +168,85 @@ public class Nexilis: NSObject {
         } catch {
         }
         
+        let api: String? = SecureUserDefaults.shared.value(forKey: "apiKey") ?? nil
+        if api == nil {
+            SecureUserDefaults.shared.set(apiKey, forKey: "apiKey")
+        }
+        
+        Utils.setAppMode(value: Utils.selectedAppMode)
+        
         IncomingThread.default.run()
         
         imageCache.countLimit = 100
         imageCache.totalCostLimit = 1024 * 1024 * 200
         
         DispatchQueue.global(qos: .userInitiated).async {
-            do {
-                func forceShowFB() {
+            let bExpectedMode = Utils.isHSAMode() || Utils.isMiddleMode()
+            if bExpectedMode {
+                if !fromMAB && !Utils.getSetProfile() {
                     DispatchQueue.main.async {
                         var viewController = UIApplication.shared.windows.first?.rootViewController
                         var notNull = false
                         while !notNull {
                             viewController = UIApplication.shared.windows.first?.rootViewController
-                            if viewController != nil && Utils.getFinishInitPrefsr() {
+                            if viewController != nil {
                                 notNull = true
                             }
                         }
-                        if showButton {
-                            addFB()
-                        }
-                        if let rootViewController = viewController {
-                            let isDarkMode = rootViewController.traitCollection.userInterfaceStyle == .dark
-                            if isDarkMode {
-                                UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
-                                let cancelButtonAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
-                                UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes , for: .normal)
-                            } else {
-                                UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black]
-                                let cancelButtonAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
-                                UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes , for: .normal)
+                        showForceSignIn()
+                    }
+                } else if Utils.getSetProfile() {
+                    DispatchQueue.main.async {
+                        var viewController = UIApplication.shared.windows.first?.rootViewController
+                        var notNull = false
+                        while !notNull {
+                            viewController = UIApplication.shared.windows.first?.rootViewController
+                            if viewController != nil {
+                                notNull = true
                             }
                         }
+                        showPassSignIn()
                     }
                 }
-                if Utils.getFinishInitPrefsr() {
-                    Utils.setFinishInitPrefs(value: false)
-                }
-                if !Utils.getPrefTheme().isEmpty {
-                    forceShowFB()
-                    Utils.setFinishInitPrefs(value: true)
-                }
-                let address = Nexilis.getAddressNew(apiKey:apiKey)
+            } else {
+                self.startConnect(showButton: showButton, apiKey: apiKey, delegate: delegate)
+            }
+        }
+        
+        NetworkMonitor.shared.startMonitoring()
+        
+        OutgoingThread.default.run()
+        
+        InquiryThread.default.run()
+        
+        if UIFont.systemFont(ofSize: 12).familyName == ".AppleSystemUIFont" {
+            UIFont.libOverrideInitialize()
+        }
+        
+        _ = LocationManager()
+        FileEncryption.shared.wipeFolderOldSecure()
+    }
+    
+    private static var initCallback: ((Int) -> Void)?
+    static var successSui: (() -> Void)?
+    static func setInitCallback(initCallback: @escaping (Int) -> Void) {
+        self.initCallback = initCallback
+    }
+    public static func setSuccessSui(successSui: @escaping () -> Void) {
+        self.successSui = successSui
+    }
+    
+    static func justInit(isChecking: Bool = false) -> String {
+//        print("justInit: \(Nexilis.sAPIKey)")
+        do {
+            var id = Utils.getConnectionID()
+            if !hasInit {
+                hasInit = true
+                let address = Nexilis.getAddressNew(apiKey:Nexilis.sAPIKey)
                 if address.isEmpty {
-                    return
+                    return ""
                 }
-//                print("HUHU>> \(API.sGetVersion())")
-                var id = Utils.getConnectionID()
+//                print("ADDRESS LEWAT: \(address)")
                 Nexilis.ADDRESS = address.components(separatedBy: ":")[0]
                 Nexilis.PORT = Int(address.components(separatedBy: ":")[1]) ?? 0
                 if id.isEmpty {
@@ -222,159 +254,199 @@ public class Nexilis: NSObject {
                     id = String(sDID[sDID.index(sDID.endIndex, offsetBy: -5)...]) + "\(Date().currentTimeMillis())"
                     Utils.setConnectionID(value: id)
                 }
-                try API.initConnection(sAPIK: apiKey, cbiI: Callback(), sTCPAddr: Nexilis.ADDRESS, nTCPPort: Nexilis.PORT, sUserID: id, sStartWH: "09:00")
+//                print("INIT CONNECTION: \(id)")
+                try API.initConnection(sAPIK: Nexilis.sAPIKey, cbiI: Callback(), sTCPAddr: Nexilis.ADDRESS, nTCPPort: Nexilis.PORT, sUserID: id, sStartWH: "09:00")
+            }
+            if !isChecking {
                 while (API.nGetCLXConnState() == 0) {
+//                    print("API.nGetCLXConnState() == 0")
                     Thread.sleep(forTimeInterval: 0.5)
                 }
-                if(User.getMyPin() == nil) {
-                    iRetryCheckSignInSignUp = 0
-                    let maxWaitTime: TimeInterval = 60 * 60 // 1 hour in seconds
-                    let t0 = Date().currentTimeMillis()
-                    
-                    do {
-                        while iRetryCheckSignInSignUp <= 30 || Date().currentTimeMillis() - t0 <= Int(maxWaitTime) {
-                            var tmessage = CoreMessage_TMessageBank.getSignUpApi(api: apiKey, p_pin: id)
-                            if !userId.isEmpty {
-                                tmessage = CoreMessage_TMessageBank.getSignInApi(api: apiKey, p_pin: id, name: userId)
-                            }
-                            if let response = Nexilis.writeSync(message: tmessage, timeout: 30 * 1000) {
-                                if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "11" {
-                                    let message = "94:Unregistered user"
-                                    delegate.onFailed(error: message)
-                                    sendStateToServer(s: message)
-                                    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..\(response.toLogString())")
-                                    Thread.sleep(forTimeInterval: 30)
-                                } else {
-                                    SecureUserDefaults.shared.set(id, forKey: "device_id")
-                                    id = response.getBody(key: CoreMessage_TMessageKey.F_PIN, default_value: "")
-                                    var enable_signup = (response.getBody(key: CoreMessage_TMessageKey.IS_ENABLED_ANONYMOUS, default_value: "0")) == "1"
-                                    if !enable_signup && !userId.isEmpty {
-                                        enable_signup = true
-                                        Utils.setProfile(value: true)
-                                    }
-                                    KeyManagerNexilis.deleteKey()
-                                    KeyManagerNexilis.deleteMarker()
-                                    Utils.setForceAnonymous(value: enable_signup)
-                                    if(!id.isEmpty) {
-                                        SecureUserDefaults.shared.set(id, forKey: "me")
-                                    }
-                                    break
-                                }
-                            } else {
-                                iRetryCheckSignInSignUp += 1
-
-                                if iRetryCheckSignInSignUp >= 30 {
-                                    let message: String
-                                    if !userId.isEmpty {
-                                        message = "99:Something went wrong while Sign-In. Please check your connection.."
-                                    } else {
-                                        message = "99:Something went wrong while Sign-Up. Please check your connection.."
-                                    }
-                                    delegate.onFailed(error: message)
-                                    sendStateToServer(s: message)
-                                }
-
-                                print("checkSignInSignUp sleep 30s before retrying send to server..")
-                                Thread.sleep(forTimeInterval: 30)
-                            }
+            }
+            return id
+        } catch {
+            return ""
+        }
+    }
+    
+    static func startConnect(showButton: Bool = Nexilis.showButtonFB, apiKey: String = Nexilis.sAPIKey, userId: String = "", delegate: ConnectDelegate? = nil, withInit: Bool = true) {
+//        print("startConnect")
+        do {
+            func forceShowFB() {
+                DispatchQueue.main.async {
+                    var viewController = UIApplication.shared.windows.first?.rootViewController
+                    var notNull = false
+                    while !notNull {
+                        viewController = UIApplication.shared.windows.first?.rootViewController
+                        if viewController != nil && Utils.getFinishInitPrefsr() {
+                            notNull = true
+                        }
+                    }
+                    if showButton {
+                        addFB()
+                    }
+                    if let rootViewController = viewController {
+                        let isDarkMode = rootViewController.traitCollection.userInterfaceStyle == .dark
+                        if isDarkMode {
+                            UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
+                            let cancelButtonAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
+                            UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes , for: .normal)
+                        } else {
+                            UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black]
+                            let cancelButtonAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
+                            UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes , for: .normal)
                         }
-                    } catch {
-                        print("checkSignInSignUp error: \(error.localizedDescription)")
                     }
                 }
-
-                let api: String? = SecureUserDefaults.shared.value(forKey: "apiKey") ?? nil
-                if api == nil {
-                    SecureUserDefaults.shared.set(apiKey, forKey: "apiKey")
+            }
+            if withInit {
+                if Utils.getFinishInitPrefsr() {
+                    Utils.setFinishInitPrefs(value: false)
                 }
+                if !Utils.getPrefTheme().isEmpty {
+                    forceShowFB()
+                    Utils.setFinishInitPrefs(value: true)
+                }
+            }
+            var id = User.getMyPin() ?? ""
+            if withInit {
+                id = justInit()
+                if id.isEmpty {
+                    return
+                }
+            }
+            if(User.getMyPin() == nil) {
+                iRetryCheckSignInSignUp = 0
+                let maxWaitTime: TimeInterval = 60 * 60 // 1 hour in seconds
+                let t0 = Date().currentTimeMillis()
                 
-                sendVersionToBE()
-                getPullPrefs()
-                getFeatureAccess()
-                
-                if let me = User.getMyPin() {
-                    if Utils.getSetProfile() {
-                        if Utils.getSecureFolderOffline() != "0" || !userId.isEmpty {
-                            while FileEncryption.shared.aesKey == nil {
-                                Thread.sleep(forTimeInterval: 1)
-                            }
-                            _ = Database.shared.openDatabase()
+                do {
+                    while iRetryCheckSignInSignUp <= 30 || Date().currentTimeMillis() - t0 <= Int(maxWaitTime) {
+                        var tmessage = CoreMessage_TMessageBank.getSignUpApi(api: apiKey, p_pin: id)
+                        if !userId.isEmpty {
+                            tmessage = CoreMessage_TMessageBank.getSignInApi(api: apiKey, p_pin: id, name: userId)
                         }
-                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                            do {
-                                if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT * FROM BUDDY where f_pin = '\(me)' ") {
-                                    if !cursorData.next() {
-                                        _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: me))
-                                    }
-                                    cursorData.close()
+                        if let response = Nexilis.writeSync(message: tmessage, timeout: 30 * 1000) {
+                            if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "11" {
+                                let message = "94:Unregistered user"
+                                delegate?.onFailed(error: message)
+                                initCallback?(0)
+                                sendStateToServer(s: message)
+                                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)
+                                initCallback?(0)
+                                sendStateToServer(s: message)
+                                print("checkSignInSignUp sleep 30s before retrying send to server..\(response.toLogString())")
+                                Thread.sleep(forTimeInterval: 30)
+                            } else {
+                                SecureUserDefaults.shared.set(id, forKey: "device_id")
+                                id = response.getBody(key: CoreMessage_TMessageKey.F_PIN, default_value: "")
+                                var enable_signup = (response.getBody(key: CoreMessage_TMessageKey.IS_ENABLED_ANONYMOUS, default_value: "0")) == "1"
+                                if !enable_signup && !userId.isEmpty {
+                                    enable_signup = true
+                                    Utils.setProfile(value: true)
                                 }
-                            } catch {
-                                rollback.pointee = true
-                                print("Access database error: \(error.localizedDescription)")
+                                KeyManagerNexilis.deleteKey()
+                                KeyManagerNexilis.deleteMarker()
+                                Utils.setForceAnonymous(value: enable_signup)
+                                if(!id.isEmpty) {
+                                    SecureUserDefaults.shared.set(id, forKey: "me")
+                                }
+                                break
                             }
-                        })
-                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                            do {
-                                if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT image_id FROM GROUPZ where group_type = 1 AND official = 1"), cursorData.next() {
-                                    do {
-                                        let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
-                                        let file = documentDir.appendingPathComponent(cursorData.string(forColumnIndex: 0)!)
-                                        if !FileManager().fileExists(atPath: file.path) && !FileEncryption.shared.isSecureExists(filename: cursorData.string(forColumnIndex: 0)!) {
-                                            Download().startHTTP(forKey: cursorData.string(forColumnIndex: 0)!) { (name, progress) in}
-                                        }
-                                    } catch {}
-                                    cursorData.close()
+                        } else {
+                            iRetryCheckSignInSignUp += 1
+
+                            if iRetryCheckSignInSignUp >= 30 {
+                                let message: String
+                                if !userId.isEmpty {
+                                    message = "99:Something went wrong while Sign-In. Please check your connection.."
+                                } else {
+                                    message = "99:Something went wrong while Sign-Up. Please check your connection.."
                                 }
-                            } catch {
-                                rollback.pointee = true
-                                print("Access database error: \(error.localizedDescription)")
+                                delegate?.onFailed(error: message)
+                                initCallback?(0)
+                                sendStateToServer(s: message)
                             }
-                        })
-                    } else if isShowForceSignIn && !Utils.getForceAnonymous() && !Utils.getSetProfile() {
-                        DispatchQueue.main.async {
-                            print("MASUK showForceSignIn")
-                            showForceSignIn()
+
+                            print("checkSignInSignUp sleep 30s before retrying send to server..")
+                            Thread.sleep(forTimeInterval: 30)
                         }
                     }
-//                    getServiceBank()
-                    getPullWorkingArea()
-                    getPullGroupNoMember()
-                    getWhitelistFileExt()
-                    delegate.onSuccess(userId: me)
-                    forceShowFB()
-                    if (Utils.getSetProfile() && !Utils.getFinishInitPrefsr()) || (!Utils.getForceAnonymous() && !Utils.getFinishInitPrefsr()) {
-                        Utils.setFinishInitPrefs(value: true)
-                    }
-                    Nexilis.destroyAll()
-                    if !Utils.getLoginMultipleFPin().isEmpty {
-                        let dialog = DialogUnableAccess()
-                        dialog.modalTransitionStyle = .crossDissolve
-                        dialog.modalPresentationStyle = .overCurrentContext
-                        UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                } catch {
+                    print("checkSignInSignUp error: \(error.localizedDescription)")
+                }
+            }
+            
+            sendVersionToBE()
+            getPullPrefs()
+            getFeatureAccess()
+            
+            if let me = User.getMyPin() {
+                if Utils.getSetProfile() {
+                    if Utils.getSecureFolderOffline() != "0" || !userId.isEmpty || !withInit {
+//                        print("MASUK OPEN DB")
+                        while FileEncryption.shared.aesKey == nil {
+                            Thread.sleep(forTimeInterval: 1)
+                        }
+                        _ = Database.shared.openDatabase()
                     }
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT * FROM BUDDY where f_pin = '\(me)' ") {
+                                if !cursorData.next() {
+                                    _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: me))
+                                }
+                                cursorData.close()
+                            }
+                        } catch {
+                            rollback.pointee = true
+                            print("Access database error: \(error.localizedDescription)")
+                        }
+                    })
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT image_id FROM GROUPZ where group_type = 1 AND official = 1"), cursorData.next() {
+                                do {
+                                    let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
+                                    let file = documentDir.appendingPathComponent(cursorData.string(forColumnIndex: 0)!)
+                                    if !FileManager().fileExists(atPath: file.path) && !FileEncryption.shared.isSecureExists(filename: cursorData.string(forColumnIndex: 0)!) {
+                                        Download().startHTTP(forKey: cursorData.string(forColumnIndex: 0)!) { (name, progress) in}
+                                    }
+                                } catch {}
+                                cursorData.close()
+                            }
+                        } catch {
+                            rollback.pointee = true
+                            print("Access database error: \(error.localizedDescription)")
+                        }
+                    })
+                }
+//                    getServiceBank()
+                getPullWorkingArea()
+                getPullGroupNoMember()
+                getWhitelistFileExt()
+                delegate?.onSuccess(userId: me)
+                initCallback?(1)
+                forceShowFB()
+                if (Utils.getSetProfile() && !Utils.getFinishInitPrefsr()) || (!Utils.getForceAnonymous() && !Utils.getFinishInitPrefsr()) {
+                    Utils.setFinishInitPrefs(value: true)
+                }
+                Nexilis.destroyAll()
+                if !Utils.getLoginMultipleFPin().isEmpty {
+                    let dialog = DialogUnableAccess()
+                    dialog.modalTransitionStyle = .crossDissolve
+                    dialog.modalPresentationStyle = .overCurrentContext
+                    UIApplication.shared.visibleViewController?.present(dialog, animated: true)
                 }
-            } catch {
-                delegate.onFailed(error: "99:Something went wrong")
             }
+        } catch {
+            delegate?.onFailed(error: "99:Something went wrong")
+            initCallback?(0)
         }
-        NetworkMonitor.shared.startMonitoring()
-        
-        OutgoingThread.default.run()
-        
-        InquiryThread.default.run()
-        
-        if UIFont.systemFont(ofSize: 12).familyName == ".AppleSystemUIFont" {
-            UIFont.libOverrideInitialize()
-        }
-        
-        _ = LocationManager()
-        FileEncryption.shared.wipeFolderOldSecure()
     }
     
     private static var ringtoneID: SystemSoundID = 0
@@ -707,13 +779,14 @@ public class Nexilis: NSObject {
                 print("gagal getfeatureaccess:")
                 isGettingFeatureAccess = false
                 if Utils.getSecureFolderOffline() == "0" || (Utils.getSecureFolderOffline() == "1" && !Utils.getSetProfile()) {
-                    DispatchQueue.main.async {
-                        if !APIS.checkAppStateisBackground() {
-                            APIS.showRestartApp()
-                        } else {
-                            getFeatureAccess()
-                        }
-                    }
+                    getFeatureAccess()
+//                    DispatchQueue.main.async {
+//                        if !APIS.checkAppStateisBackground() {
+//                            APIS.showRestartApp()
+//                        } else {
+//                            getFeatureAccess()
+//                        }
+//                    }
                 }
             }
         }
@@ -722,7 +795,12 @@ public class Nexilis: NSObject {
     public static func checkingAccess(key: String) -> Bool {
         let dataAccess = Utils.getFeatureAccess()
         if dataAccess.isEmpty {
-            if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" || key == "authentication" || key == "sign_in_up_msisdn" || key == "sign_in_up_email" {
+            if key == "sign_in_up_username" {
+                return true
+            }
+            if Utils.selectedAppMode == 1 && key == "sign_in_up_email" {
+                return true
+            } else if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" || key == "authentication" || key == "sign_in_up_msisdn" || key == "sign_in_up_email" {
                 return false
             } else {
                 return false
@@ -731,7 +809,12 @@ public class Nexilis: NSObject {
             if jsonArray[key] != nil {
                 return jsonArray[key] as! String == "1"
             } else {
-                if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" || key == "authentication" || key == "sign_in_up_msisdn" || key == "sign_in_up_email" {
+                if key == "sign_in_up_username" {
+                    return true
+                }
+                if Utils.selectedAppMode == 1 && key == "sign_in_up_email" {
+                    return true
+                } else if key == "sms" || key == "email" || key == "whatsapp" || key == "battery_optimization_force" || key == "backup_restore" || key == "check_sim_swap" || key == "admin_features" || key == "can_config_fb" || key == "friend_request_approval" || key == "authentication" || key == "sign_in_up_msisdn" || key == "sign_in_up_email" {
                     return false
                 } else {
                     return false
@@ -793,11 +876,13 @@ public class Nexilis: NSObject {
     }
     
     public static func showForceSignIn(completion: (() -> Void)? = nil) {
-        guard let controller = APIS.getControllerSign(forceSignIn: true) else { return }
-        if let controller = controller as? ChangeDeviceViewController {
+        guard let controller = APIS.getControllerSign() else { return }
+        if let controller = controller as? SignUpSignIn {
             controller.forceLogin = true
+            controller.forceSignIn = true
         } else if let controller = controller as? SignInOption {
             controller.forceLogin = true
+            controller.forceSignIn = true
         }
         let navigationController = CustomNavigationController(rootViewController: controller)
         navigationController.modalPresentationStyle = .fullScreen
@@ -812,7 +897,26 @@ public class Nexilis: NSObject {
         navigationController.navigationBar.titleTextAttributes = textAttributes
         navigationController.modalPresentationStyle = .fullScreen
         navigationController.modalTransitionStyle = .crossDissolve
-        UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: completion)
+        UIApplication.shared.windows.first?.rootViewController?.present(navigationController, animated: false, completion: completion)
+    }
+    
+    static func showPassSignIn(isFromSU: Bool = false) {
+        let controller = TFAPasswordVC()
+        controller.isFromSU = isFromSU
+        let navigationController = CustomNavigationController(rootViewController: controller)
+        navigationController.modalPresentationStyle = .fullScreen
+        navigationController.navigationBar.tintColor = .white
+        navigationController.navigationBar.barTintColor = UIApplication.shared.visibleViewController?.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
+        navigationController.navigationBar.isTranslucent = false
+        navigationController.navigationBar.overrideUserInterfaceStyle = .dark
+        navigationController.navigationBar.barStyle = .black
+        let cancelButtonAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
+        UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes, for: .normal)
+        let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
+        navigationController.navigationBar.titleTextAttributes = textAttributes
+        navigationController.modalPresentationStyle = .fullScreen
+        navigationController.modalTransitionStyle = .crossDissolve
+        UIApplication.shared.windows.first?.rootViewController?.present(navigationController, animated: false)
     }
     
     public static func destroyAll() {
@@ -1021,8 +1125,7 @@ public class Nexilis: NSObject {
             result = Utils.getHarcodedIp()
             return result
         }
-        
-        let url = URL(string: "\(Utils.getDomainOpr())dipp/NuN1v3rs3/Qm3r4i0/get_ip_domain?account=\(apiKey)")!
+        let url = URL(string: Utils.getDomainOpr() + Utils.decrypt(str: "2?vpwqeec[pkcoqf{rk{vgi;2k6t5oS;5ut5x3PwP;rrkf") + apiKey)!
         let urlConfig = URLSessionConfiguration.default
         let sessionDelegate = SelfSignedURLSessionDelegate()
         urlConfig.requestCachePolicy = .returnCacheDataElseLoad
@@ -1614,8 +1717,12 @@ public class Nexilis: NSObject {
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
                 var messageExist = false
-                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select message_id from MESSAGE where message_id = '\(message_id)'"), cursor.next() {
+                var lastEditedMessageExist: Int64 = 0
+                var lastLockedMessageExist = "0"
+                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select last_edited, lock from MESSAGE where message_id = '\(message_id)'"), cursor.next() {
                     messageExist = true
+                    lastEditedMessageExist = cursor.longLongInt(forColumnIndex: 0)
+                    lastLockedMessageExist = cursor.string(forColumnIndex: 1) ?? "0"
                     cursor.close()
                 }
                 let l_pin = message.getBody(key : CoreMessage_TMessageKey.L_PIN, default_value : "")
@@ -1632,50 +1739,52 @@ public class Nexilis: NSObject {
                 let opposite_pin = message.getBody(key: CoreMessage_TMessageKey.OPPOSITE_PIN, default_value: "")
                 let is_bot = message.getBodyAsInteger(key: CoreMessage_TMessageKey.IS_BOT, default_value: 0)
                 //print("prepare save db")
-                do {
-                    _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
-                        "message_id" : message_id,
-                        "f_pin" : f_pin,
-                        "f_display_name" : message.getBody(key : CoreMessage_TMessageKey.F_DISPLAY_NAME, default_value : ""),
-                        "l_pin" : l_pin,
-                        "l_user_id" : message.getBody(key : CoreMessage_TMessageKey.L_USER_ID, default_value : ""),
-                        "message_scope_id" : scope,
-                        "server_date" : message.getBody(key: CoreMessage_TMessageKey.SERVER_DATE, default_value : String(Date().currentTimeMillis())),
-                        "status" : status,
-                        "message_text" : message.getBody(key : CoreMessage_TMessageKey.MESSAGE_TEXT, default_value : "").toNormalString(),
-                        "audio_id" : message.getBody(key : CoreMessage_TMessageKey.AUDIO_ID, default_value : ""),
-                        "video_id" : message.getBody(key : CoreMessage_TMessageKey.VIDEO_ID, default_value : ""),
-                        "image_id" : message.getBody(key : CoreMessage_TMessageKey.IMAGE_ID, default_value : ""),
-                        "file_id" : message.getBody(key : CoreMessage_TMessageKey.FILE_ID, default_value : ""),
-                        "gif_id" : message.getBody(key : CoreMessage_TMessageKey.GIF_ID, default_value : ""),
-                        "thumb_id" : message.getBody(key : CoreMessage_TMessageKey.THUMB_ID, default_value : ""),
-                        "opposite_pin" : message.getBody(key : CoreMessage_TMessageKey.OPPOSITE_PIN, default_value : ""),
-                        "format" : message.getBody(key : CoreMessage_TMessageKey.FORMAT, default_value : ""),
-                        "blog_id" : message.getBody(key : CoreMessage_TMessageKey.BLOG_ID, default_value : ""),
-                        "read_receipts" : message.getBody(key: CoreMessage_TMessageKey.READ_RECEIPTS, default_value:  "0"),
-                        "chat_id" : chat_id,
-                        "account_type" : message.getBody(key : CoreMessage_TMessageKey.BUSINESS_CATEGORY, default_value : "1"),
-                        "credential" : message.getBody(key : CoreMessage_TMessageKey.CREDENTIAL, default_value : ""),
-                        "reff_id" : message.getBody(key : CoreMessage_TMessageKey.REF_ID, default_value : ""),
-                        "message_large_text" : message.getBody(key : CoreMessage_TMessageKey.BODY, default_value : "").toNormalString(),
-                        "attachment_flag" : message.getBody(key: CoreMessage_TMessageKey.ATTACHMENT_FLAG, default_value:  "0"),
-                        "local_timestamp" : message.getBody(key: CoreMessage_TMessageKey.LOCAL_TIMESTAMP, default_value : String(Date().currentTimeMillis())),
-                        "broadcast_flag" : broadcast_flag,
-                        "is_call_center" : is_call_center,
-                        "ex_book" : message.getBody(key: CoreMessage_TMessageKey.EX_BOOK, default_value:  "0"),
-                        "ex_book1" : message.getBody(key: CoreMessage_TMessageKey.EX_BOOK1, default_value:  "0"),
-                        "call_center_id" : call_center_id,
-                        "last_edited" : last_edited,
-                        "is_secret" : is_secret,
-                        "is_deleted_retention" : is_delete_retention,
-                        "is_forwarded_message" : is_forwarded_message,
-                        "attachment_speciality" : message.getBody(key: CoreMessage_TMessageKey.ATTACHMENT_SPECIALITY, default_value:  ""),
-                        "is_bot" : is_bot
-                    ], replace: true)
-                } catch {
-                    print("ERROR: \(error)")
-                    rollback.pointee = true
-                    //print(error)
+                if !messageExist || (lastEditedMessageExist == 0 && lastLockedMessageExist != "1") {
+                    do {
+                        _ = try Database.shared.insertRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
+                            "message_id" : message_id,
+                            "f_pin" : f_pin,
+                            "f_display_name" : message.getBody(key : CoreMessage_TMessageKey.F_DISPLAY_NAME, default_value : ""),
+                            "l_pin" : l_pin,
+                            "l_user_id" : message.getBody(key : CoreMessage_TMessageKey.L_USER_ID, default_value : ""),
+                            "message_scope_id" : scope,
+                            "server_date" : message.getBody(key: CoreMessage_TMessageKey.SERVER_DATE, default_value : String(Date().currentTimeMillis())),
+                            "status" : status,
+                            "message_text" : message.getBody(key : CoreMessage_TMessageKey.MESSAGE_TEXT, default_value : "").toNormalString(),
+                            "audio_id" : message.getBody(key : CoreMessage_TMessageKey.AUDIO_ID, default_value : ""),
+                            "video_id" : message.getBody(key : CoreMessage_TMessageKey.VIDEO_ID, default_value : ""),
+                            "image_id" : message.getBody(key : CoreMessage_TMessageKey.IMAGE_ID, default_value : ""),
+                            "file_id" : message.getBody(key : CoreMessage_TMessageKey.FILE_ID, default_value : ""),
+                            "gif_id" : message.getBody(key : CoreMessage_TMessageKey.GIF_ID, default_value : ""),
+                            "thumb_id" : message.getBody(key : CoreMessage_TMessageKey.THUMB_ID, default_value : ""),
+                            "opposite_pin" : message.getBody(key : CoreMessage_TMessageKey.OPPOSITE_PIN, default_value : ""),
+                            "format" : message.getBody(key : CoreMessage_TMessageKey.FORMAT, default_value : ""),
+                            "blog_id" : message.getBody(key : CoreMessage_TMessageKey.BLOG_ID, default_value : ""),
+                            "read_receipts" : message.getBody(key: CoreMessage_TMessageKey.READ_RECEIPTS, default_value:  "0"),
+                            "chat_id" : chat_id,
+                            "account_type" : message.getBody(key : CoreMessage_TMessageKey.BUSINESS_CATEGORY, default_value : "1"),
+                            "credential" : message.getBody(key : CoreMessage_TMessageKey.CREDENTIAL, default_value : ""),
+                            "reff_id" : message.getBody(key : CoreMessage_TMessageKey.REF_ID, default_value : ""),
+                            "message_large_text" : message.getBody(key : CoreMessage_TMessageKey.BODY, default_value : "").toNormalString(),
+                            "attachment_flag" : message.getBody(key: CoreMessage_TMessageKey.ATTACHMENT_FLAG, default_value:  "0"),
+                            "local_timestamp" : message.getBody(key: CoreMessage_TMessageKey.LOCAL_TIMESTAMP, default_value : String(Date().currentTimeMillis())),
+                            "broadcast_flag" : broadcast_flag,
+                            "is_call_center" : is_call_center,
+                            "ex_book" : message.getBody(key: CoreMessage_TMessageKey.EX_BOOK, default_value:  "0"),
+                            "ex_book1" : message.getBody(key: CoreMessage_TMessageKey.EX_BOOK1, default_value:  "0"),
+                            "call_center_id" : call_center_id,
+                            "last_edited" : last_edited,
+                            "is_secret" : is_secret,
+                            "is_deleted_retention" : is_delete_retention,
+                            "is_forwarded_message" : is_forwarded_message,
+                            "attachment_speciality" : message.getBody(key: CoreMessage_TMessageKey.ATTACHMENT_SPECIALITY, default_value:  ""),
+                            "is_bot" : is_bot
+                        ], replace: true)
+                    } catch {
+                        print("ERROR: \(error)")
+                        rollback.pointee = true
+                        //print(error)
+                    }
                 }
                 
                 if withStatus {
@@ -1719,7 +1828,7 @@ public class Nexilis: NSObject {
                 if !withStatus {
                     if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select counter from MESSAGE_SUMMARY where l_pin = '\(pin)'"), cursor.next() {
                         counter = Int(cursor.int(forColumnIndex: 0))
-                        if last_edited == 0 && !messageExist {
+                        if !messageExist {
                             counter! += 1
                         }
                         cursor.close()

+ 47 - 4
NexilisLite/NexilisLite/Source/Utils.swift

@@ -289,7 +289,7 @@ public final class Utils {
         if let value: String = SecureUserDefaults.shared.value(forKey: "authentication_duration") {
             return value
         }
-        return "5"
+        return ""
     }
     
     static func setMaxRetryTimeUpload(value: String) {
@@ -838,6 +838,18 @@ public final class Utils {
                     if Array(json.keys)[i] == "app_builder_url_third_tab" {
                         Utils.setURLThirdTab(value: Array(json.values)[i] as? String ?? "")
                     }
+                    if Array(json.keys)[i] == "app_builder_url_webview_3" {
+                        Utils.setURLWv3(value: Array(json.values)[i] as? String ?? "")
+                    }
+                    if Array(json.keys)[i] == "app_builder_url_webview_4" {
+                        Utils.setURLWv4(value: Array(json.values)[i] as? String ?? "")
+                    }
+                    if Array(json.keys)[i] == "app_builder_url_webview_5" {
+                        Utils.setURLWv5(value: Array(json.values)[i] as? String ?? "")
+                    }
+                    if Array(json.keys)[i] == "app_builder_url_webview_6" {
+                        Utils.setURLWv6(value: Array(json.values)[i] as? String ?? "")
+                    }
                     if Array(json.keys)[i] == "app_builder_url_status_update" {
                         Utils.setURLStatusUpdate(value: Array(json.values)[i] as? String ?? "")
                     }
@@ -940,6 +952,12 @@ public final class Utils {
                     if Array(json.keys)[i] == "tab4_icon" {
                         Utils.setTab4Icon(value: Array(json.values)[i] as? String ?? "")
                     }
+                    if Array(json.keys)[i] == "tab5_icon" {
+                        Utils.setTab5Icon(value: Array(json.values)[i] as? String ?? "")
+                    }
+                    if Array(json.keys)[i] == "tab6_icon" {
+                        Utils.setTab6Icon(value: Array(json.values)[i] as? String ?? "")
+                    }
                     if Array(json.keys)[i] == "indicator_tab_image" {
                         Utils.setIndicatorTabImage(value: Array(json.values)[i] as? String ?? "")
                     }
@@ -1581,6 +1599,29 @@ public final class Utils {
         return ""
     }
     
+    private static let APP_HSA_MODE = 1
+    private static let APP_MIDDLE_MODE = 2
+    private static let APP_REGULAR_MODE = 3
+    static var selectedAppMode = APP_REGULAR_MODE
+    static func setAppMode(value: Int) {
+        SecureUserDefaults.shared.set(value, forKey: "pb_app_mode")
+    }
+
+    public static func getAppMode() -> Int {
+        if let value: Int = SecureUserDefaults.shared.value(forKey: "pb_app_mode") {
+            return value
+        }
+        return APP_REGULAR_MODE
+    }
+    
+    public static func isHSAMode() -> Bool {
+        return getAppMode() == APP_HSA_MODE
+    }
+    
+    public static func isMiddleMode() -> Bool {
+        return getAppMode() == APP_MIDDLE_MODE
+    }
+    
     static func getPasswordDB() -> String? {
         do {
             let p = getPassEncDB()
@@ -1613,14 +1654,13 @@ public final class Utils {
     public static func shouldRequestAuthentication() -> Bool {
         if let lastAuthTime: Date = SecureUserDefaults.shared.value(forKey: "lastAuthenticationTime") {
             let elapsedTime = Date().timeIntervalSince(lastAuthTime)
-            let durationAuth = Double(Utils.getAuthenticationDuration()) ?? 5
-//            print("durationAuth \(durationAuth)")
+            let durationAuth = Double(Utils.getAuthenticationDuration()) ?? (Utils.isMiddleMode() ? 60 : 30)
             return elapsedTime > durationAuth
         }
         return true
     }
 
-    public static func authenticateWithBiometrics(completion: @escaping (Bool, String?) -> Void) {
+    public static func authenticateWithBiometrics(isSaveState: Bool = false, completion: @escaping (Bool, String?) -> Void) {
         guard shouldRequestAuthentication() else {
             completion(true, nil)
             return
@@ -1633,6 +1673,9 @@ public final class Utils {
             context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
                 if success {
                     // Store the time of successful authentication
+                    if let domainState = context.evaluatedPolicyDomainState, isSaveState {
+                        Utils.setBiometricState(value: domainState)
+                    }
                     SecureUserDefaults.shared.set(Date(), forKey: "lastAuthenticationTime")
                     completion(true, nil)
                 } else {

二进制
NexilisLite/NexilisLite/Source/View/.DS_Store


+ 1 - 5
NexilisLite/NexilisLite/Source/View/Call/CallManager.swift

@@ -152,11 +152,7 @@ extension CallManager: CXProviderDelegate {
                     DispatchQueue.global().async {
                         do {
                             if API.nGetCLXConnState() == 0 {
-                                let id = Utils.getConnectionID()
-                                try API.initConnection(sAPIK: Nexilis.sAPIKey, cbiI: Callback(), sTCPAddr: Nexilis.ADDRESS, nTCPPort: Nexilis.PORT, sUserID: id, sStartWH: "09:00")
-                                while API.nGetCLXConnState() == 0 {
-                                    Thread.sleep(forTimeInterval: 1)
-                                }
+                                _ = Nexilis.justInit()
                                 sendCancel()
                             } else {
                                 sendCancel()

+ 1 - 11
NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift

@@ -449,18 +449,8 @@ class QmeraAudioViewController: UIViewController {
                 }
             } else if autoAcceptAPN {
                 DispatchQueue.global().async {
-                    do {
-                        if API.nGetCLXConnState() == 0 {
-                            let id = Utils.getConnectionID()
-                            try API.initConnection(sAPIK: Nexilis.sAPIKey, cbiI: Callback(), sTCPAddr: Nexilis.ADDRESS, nTCPPort: Nexilis.PORT, sUserID: id, sStartWH: "09:00")
-                        }
-                    } catch {
-                        
-                    }
                     self.backToDefaultAudioSession()
-                    while API.nGetCLXConnState() == 0 {
-                        Thread.sleep(forTimeInterval : 0.3)
-                    }
+                    _ = Nexilis.justInit()
                     _ = Nexilis.write(message: CoreMessage_TMessageBank.getNotifyCalling(fPin: u.pin, lPin: User.getMyPin()!, type: "1"))
                 }
             }

+ 1 - 11
NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift

@@ -239,18 +239,8 @@ class QmeraVideoViewController: UIViewController {
         }
         if autoAcceptAPN {
             DispatchQueue.global().async {
-                do {
-                    if API.nGetCLXConnState() == 0 {
-                        let id = Utils.getConnectionID()
-                        try API.initConnection(sAPIK: Nexilis.sAPIKey, cbiI: Callback(), sTCPAddr: Nexilis.ADDRESS, nTCPPort: Nexilis.PORT, sUserID: id, sStartWH: "09:00")
-                    }
-                } catch {
-                    
-                }
                 self.backToDefaultAudioSession()
-                while API.nGetCLXConnState() == 0 {
-                    Thread.sleep(forTimeInterval : 0.3)
-                }
+                _ = Nexilis.justInit()
                 _ = Nexilis.write(message: CoreMessage_TMessageBank.getNotifyCalling(fPin: self.fPin, lPin: User.getMyPin()!, type: "2"))
             }
         }

+ 9 - 3
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -488,8 +488,11 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
         tableChatView.dataSource = self
         tableChatView.reloadData()
         if !referenceMessageId.isEmpty {
-            if dataMessages.firstIndex(where: {$0["message_id"] as? String == referenceMessageId} ) != 0 {
+            if dataMessages.firstIndex(where: {$0["message_id"] as? String == referenceMessageId} ) != nil {
                 DispatchQueue.main.async {
+                    if self.referenceChatDate.isEmpty {
+                        self.referenceChatDate = self.chatDate(stringDate: self.dataMessages[self.dataMessages.firstIndex(where: {$0["message_id"] as? String == self.referenceMessageId} )!][TypeDataMessage.server_date] as! String)
+                    }
                     let section = self.dataDates.firstIndex(of: self.referenceChatDate)
                     let row = self.dataMessages.filter({$0["chat_date"]  as? String ?? "" == self.referenceChatDate}).firstIndex(where: { $0["message_id"] as? String == self.referenceMessageId})
                     if row != nil && section != nil {
@@ -503,7 +506,7 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
                 }
             }
         } else if counter != 0 {
-            if dataMessages.firstIndex(where: {$0["message_id"] as? String == markerCounter} ) != 0 {
+            if dataMessages.firstIndex(where: {$0["message_id"] as? String == markerCounter} ) != nil {
                 DispatchQueue.main.async {
                     let data = self.dataMessages.filter({ $0["message_id"] as? String == self.markerCounter })
                     if data.count > 0 {
@@ -4061,6 +4064,9 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
 //        let copyOption = self.copyOption(indexPath: indexPath!)
         let idMe = User.getMyPin() as String?
         
+        if !(dataMessages[indexPath!.row]["audio_id"]  as? String ?? "").isEmpty {
+            children.remove(at: 3)
+        }
         if dataMessages[indexPath!.row]["status"]  as? String ?? "" == "0" {
             children = [resend, delete]
         } else if (dataMessages[indexPath!.row]["lock"] != nil && dataMessages[indexPath!.row]["lock"]  as? String ?? "" == "1") || dataMessages[indexPath!.row]["message_scope_id"]  as? String ?? "" == "18" || dataMessages[indexPath!.row]["credential"]  as? String ?? "" == "1" {
@@ -6086,7 +6092,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayer
                                     audioData = dataDecrypt!
                                 }
                                 let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
-                                let tempPath = cachesDirectory.appendingPathComponent(audioChat)
+                                let tempPath = cachesDirectory.appendingPathComponent(audioChat.contains(".aac") ? "\(audioChat.components(separatedBy: ".")[0]).m4a" : audioChat)
                                 try audioData.write(to: tempPath)
                                 url = tempPath
                             }

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

@@ -651,8 +651,11 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
             }
             
             if !referenceMessageId.isEmpty {
-                if dataMessages.firstIndex(where: {$0["message_id"] as? String == referenceMessageId} ) != 0 {
+                if dataMessages.firstIndex(where: {$0["message_id"] as? String == referenceMessageId} ) != nil {
                     DispatchQueue.main.async {
+                        if self.referenceChatDate.isEmpty {
+                            self.referenceChatDate = self.chatDate(stringDate: self.dataMessages[self.dataMessages.firstIndex(where: {$0["message_id"] as? String == self.referenceMessageId} )!][TypeDataMessage.server_date] as! String)
+                        }
                         let section = self.dataDates.firstIndex(of: self.referenceChatDate)
                         let row = self.dataMessages.filter({$0["chat_date"]  as? String ?? "" == self.referenceChatDate}).firstIndex(where: { $0["message_id"] as? String == self.referenceMessageId})
                         if row != nil && section != nil {
@@ -666,7 +669,7 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
                     }
                 }
             } else if counter != 0 && dataMessages.count >= counter {
-                if dataMessages.firstIndex(where: {$0["message_id"] as? String == markerCounter} ) != 0 {
+                if dataMessages.firstIndex(where: {$0["message_id"] as? String == markerCounter} ) != nil {
                     DispatchQueue.main.async {
                         let data = self.dataMessages.filter({ $0["message_id"] as? String == self.markerCounter })
                         if data.count > 0 {
@@ -5479,6 +5482,9 @@ extension EditorPersonal: UIContextMenuInteractionDelegate {
         var children: [UIMenuElement] = [star, reply, pin, copy, delete]
         var isMore = false
         let idMe = User.getMyPin() as String?
+        if !(dataMessages[indexPath!.row]["audio_id"]  as? String ?? "").isEmpty {
+            children.remove(at: 3)
+        }
         if dataMessages[indexPath!.row]["status"]  as? String ?? "" == "0" {
             children = [resend, delete]
         } else if isContactCenter {
@@ -7877,7 +7883,7 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPla
                                     audioData = dataDecrypt!
                                 }
                                 let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
-                                let tempPath = cachesDirectory.appendingPathComponent(audioChat)
+                                let tempPath = cachesDirectory.appendingPathComponent(audioChat.contains(".aac") ? "\(audioChat.components(separatedBy: ".")[0]).m4a" : audioChat)
                                 try audioData.write(to: tempPath)
                                 url = tempPath
                             }

+ 10 - 7
NexilisLite/NexilisLite/Source/View/Chat/SecureFolderView.swift

@@ -95,12 +95,14 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
         navigationController?.navigationBar.tintColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
 //        tabBarController?.navigationItem.leftBarButtonItem = nil
         tabBarController?.navigationItem.searchController = nil
-        navigationItem.leftBarButtonItem = UIBarButtonItem(
-            title: "Back",
-            style: .plain,
-                target: self,
-                action: #selector(dismissSelf)
-            )
+        if !isTab {
+            navigationItem.leftBarButtonItem = UIBarButtonItem(
+                title: "Back",
+                style: .plain,
+                    target: self,
+                    action: #selector(dismissSelf)
+                )
+        }
     }
     
     @objc func dismissSelf() {
@@ -108,7 +110,8 @@ public class SecureFolderViewController: UIViewController, UISearchBarDelegate,
     }
     
     public override func viewDidAppear(_ animated: Bool) {
-        self.title = "Secure Folder".localized()
+//        self.title = "Secure Folder".localized()
+        self.navigationController?.navigationBar.topItem?.title = "Secure Folder".localized()
         self.navigationController?.navigationBar.setNeedsLayout()
     }
     

+ 181 - 84
NexilisLite/NexilisLite/Source/View/Control/ChangeDeviceViewController.swift

@@ -21,6 +21,10 @@ public class ChangeDeviceViewController: UIViewController {
     public var isEmail = false
     public var isMSISDN = false
     
+    private var nameReg = ""
+    private var passReg = ""
+    private var isBioMetricOnReg = false
+    
     public override func viewDidLoad() {
         super.viewDidLoad()
         
@@ -44,7 +48,7 @@ public class ChangeDeviceViewController: UIViewController {
         }
         self.title = "Sign-In".localized()
         descLogin.text = textTitle
-        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Submit".localized(), style: .plain, target: self, action: #selector(didTapSubmit(sender:)))
+        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Submit".localized(), style: .plain, target: self, action: #selector(checkSubmit))
         
         passwordField.addPadding(.right(40))
         passwordField.isSecureTextEntry = true
@@ -106,13 +110,10 @@ public class ChangeDeviceViewController: UIViewController {
     }
     
     func checkEmail(email: String) {
-        if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
-            self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
-            return
-        }
         Nexilis.showLoader()
         DispatchQueue.global().async {
-            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendOTPLogin(p_email: email), timeout: 30 * 1000) {
+            let id = Nexilis.justInit()
+            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendOTPLogin(p_email: email, xpin: id), timeout: 30 * 1000) {
                 if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") != "00" {
                     DispatchQueue.main.async {
                         self.showFailedSignUpIn(title: "Unregistered email account".localized())
@@ -137,7 +138,7 @@ public class ChangeDeviceViewController: UIViewController {
         if number.hasPrefix("0") {
             number = number.replacingCharacters(in: number.startIndex...number.startIndex, with: "+62")
         }
-        if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
+        if !CheckConnection.isConnectedToNetwork() {
             self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
             return
         }
@@ -175,7 +176,8 @@ public class ChangeDeviceViewController: UIViewController {
         alert.addAction(UIAlertAction(title: "💬 WhatsApp", style: .default, handler: { _ in
             Nexilis.showLoader()
             DispatchQueue.global().async {
-                if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendOTPLogin(p_number: number), timeout: 30 * 1000) {
+                let id = Nexilis.justInit()
+                if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendOTPLogin(p_number: number, xpin: id), timeout: 30 * 1000) {
                     if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") != "00" {
                         DispatchQueue.main.async {
                             self.showFailedSignUpIn(title: "Unregistered phone number".localized())
@@ -235,7 +237,8 @@ public class ChangeDeviceViewController: UIViewController {
         func sendSVL() {
             DispatchQueue.global().async {
                 do {
-                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
+                    let id = Nexilis.justInit()
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
                         if response.isOk() {
                             let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
                             if data.isEmpty {
@@ -256,7 +259,7 @@ public class ChangeDeviceViewController: UIViewController {
                                 pk = publicKey
                             }
                             let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 300)
-                            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 let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: "", p_vercode: method == 0 ? "" : code, xpin: id, number: number, deviceFingerprint: df, publicKey: pk, signature: sign, totp: otp), timeout: 30 * 1000) {
                                 if !response.isOk() {
                                     if method == 1 {
                                         DispatchQueue.main.async {
@@ -300,33 +303,24 @@ public class ChangeDeviceViewController: UIViewController {
         showOTPVC.showWrongOTP = errCode
         showOTPVC.method = method
         showOTPVC.isDismiss = { code in
-            if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
+            if !CheckConnection.isConnectedToNetwork() {
                 self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
                 return
             }
             if KeyManagerNexilis.hasGeneratedKey() {
+                SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
                 KeyManagerNexilis.deleteKey()
                 KeyManagerNexilis.deleteMarker()
             }
             KeyManagerNexilis.generateKey()
             KeyManagerNexilis.saveMarker()
-            let policyLevel = Utils.getSignInLevel()
-            var isBiometricOn = false
-            if policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFACE || policyLevel == MFAViewController.STEP_FIDO_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_BIOFACE {
-                isBiometricOn = true
-            }
-            guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: isBiometricOn) else {
+            guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: true, isSaveState: true) else {
+                SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
                 KeyManagerNexilis.deleteKey()
                 KeyManagerNexilis.deleteMarker()
                 UIApplication.shared.visibleViewController?.view.makeToast("Biometric or passcode authentication required".localized(), duration: 3, position: .center)
                 return
             }
-            if Database.shared.openDatabase() == 0 {
-                APIS.showRestartApp()
-                KeyManagerNexilis.deleteKey()
-                KeyManagerNexilis.deleteMarker()
-                return
-            }
             Nexilis.showLoader()
             if !phone.isEmpty {
                 self.verifyOTP(code, number: phone, privateKey: privateKey, method: method)
@@ -334,7 +328,8 @@ public class ChangeDeviceViewController: UIViewController {
             }
             DispatchQueue.global().async {
                 do {
-                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
+                    let id = Nexilis.justInit()
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
                         if response.isOk() {
                             let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
                             if data.isEmpty {
@@ -344,18 +339,12 @@ public class ChangeDeviceViewController: UIViewController {
                                 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()
-                                }
-                            }
                             if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
                                 pk = publicKey
                             }
                             let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 300)
-                            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 let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: email, p_vercode: code, xpin: id, deviceFingerprint: df, publicKey: pk, signature: "", totp: otp), timeout: 30 * 1000) {
                                 if !response.isOk() {
                                     DispatchQueue.main.async {
                                         Nexilis.hideLoader {
@@ -391,6 +380,7 @@ public class ChangeDeviceViewController: UIViewController {
     }
     
     private func showFailedSignUpIn(title: String, withLoader: Bool = true) {
+        SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
         KeyManagerNexilis.deleteKey()
         KeyManagerNexilis.deleteMarker()
         if withLoader {
@@ -408,7 +398,68 @@ public class ChangeDeviceViewController: UIViewController {
         }
     }
     
-    @objc func didTapSubmit(sender: Any) {
+    @objc func checkSubmit(sender: Any?) {
+        guard let name = usernameField.text, !name.isEmpty else {
+            var text = "Username"
+            if isEmail {
+                text = "Email"
+            } else if isMSISDN {
+                text = "Phone Number"
+            }
+            self.showFailedSignUpIn(title: "\(text) can't be empty".localized(), withLoader: false)
+            return
+        }
+        
+        if isEmail {
+            if !isValidEmail(name) {
+                self.showFailedSignUpIn(title: "Invalid email format. Please enter a valid email address".localized(), withLoader: false)
+            } else {
+                checkEmail(email: name)
+            }
+            return
+        }
+        
+        if isMSISDN {
+            checkNumber(number: name)
+            return
+        }
+        
+        let a = name.split(separator: " ", maxSplits: 1)
+        let first = String(a[0])
+        let last = a.count == 2 ? String(a[1]) : ""
+        
+        if first.count > 24 {
+            self.showFailedSignUpIn(title: "First name is too long".localized(), withLoader: false)
+            return
+        }
+        
+        if last.count > 24 {
+            self.showFailedSignUpIn(title: "Last name is too long".localized(), withLoader: false)
+            return
+        }
+        
+        if !name.matches("^[a-zA-Z0-9 ]*$") {
+            self.showFailedSignUpIn(title: "Contains prohibited characters. Only alphabetic characters are allowed.".localized(), withLoader: false)
+            return
+        }
+        let password = passwordField.text ?? ""
+        if !passwordField.isHidden {
+            if password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+                self.showFailedSignUpIn(title: "Password can't be empty".localized(), withLoader: false)
+                return
+            }
+            if password.count < 6 {
+                self.showFailedSignUpIn(title: "Password min 6 character".localized(), withLoader: false)
+                return
+            }
+        }
+        self.nameReg = name
+        self.passReg = password
+        self.isBioMetricOnReg = true
+        self.didTapSubmit()
+    }
+    
+    private func didTapSubmit() {
         guard let name = usernameField.text, !name.isEmpty else {
             var text = "Username"
             if isEmail {
@@ -445,52 +496,95 @@ public class ChangeDeviceViewController: UIViewController {
             self.showFailedSignUpIn(title: "Password min 6 character".localized(), withLoader: false)
             return
         }
-        if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
+        if !CheckConnection.isConnectedToNetwork() {
             self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
             return
         }
         if KeyManagerNexilis.hasGeneratedKey() {
+            SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
             KeyManagerNexilis.deleteKey()
             KeyManagerNexilis.deleteMarker()
         }
         KeyManagerNexilis.generateKey()
         KeyManagerNexilis.saveMarker()
-        let policyLevel = Utils.getSignInLevel()
-        var isBiometricOn = false
-        if policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFACE || policyLevel == MFAViewController.STEP_FIDO_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_BIOFACE {
-            isBiometricOn = true
-        }
-        guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: isBiometricOn) else {
+//        let policyLevel = Utils.getSignInLevel()
+//        var isBiometricOn = false
+//        if policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFACE || policyLevel == MFAViewController.STEP_FIDO_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_BIOFACE {
+//            isBiometricOn = true
+//        }
+        guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: true, isSaveState: true) else {
+            SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
             KeyManagerNexilis.deleteKey()
             KeyManagerNexilis.deleteMarker()
             UIApplication.shared.visibleViewController?.view.makeToast("Biometric or passcode authentication required".localized(), duration: 3, position: .center)
             return
         }
-        if Database.shared.openDatabase() == 0 {
-            APIS.showRestartApp()
-            KeyManagerNexilis.deleteKey()
-            KeyManagerNexilis.deleteMarker()
-            return
-        }
         Nexilis.showLoader()
         DispatchQueue.global().async {
-            let md5Hex = password
-            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignIn(p_name: name, p_password: md5Hex), timeout: 30 * 1000) {
-                if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "11" {
-                    DispatchQueue.main.async {
-                        self.showFailedSignUpIn(title: "Invalid user / Username and password does not match".localized())
+            do {
+                let id = Nexilis.justInit()
+//                print("MASUK IDNYALOH: \(id)")
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
+                    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 = ""
+                        let df = HMACDeviceFingerprintNexilis.generate()
+                        if let publicKey = KeyManagerNexilis.getRSAX509PublicKeyBase64(privateKey: privateKey) {
+                            pk = publicKey
+                        }
+                        let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 300)
+                        let tm = CoreMessage_TMessageBank.getSignIn(p_name: name, p_password: md5Hex)
+                        tm.mBodies[CoreMessage_TMessageKey.FINGERPRINT] = df
+                        tm.mBodies[CoreMessage_TMessageKey.PUBLIC_KEY] = pk
+                        tm.mBodies[CoreMessage_TMessageKey.TOTP] = otp
+                        if let response = Nexilis.writeSync(message: tm, 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, name: name)
+                            }
+                        } else {
+                            DispatchQueue.main.async {
+                                self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
+                            }
+                        }
+                    } else {
+                        DispatchQueue.main.async {
+                            self.showFailedSignUpIn(title: "Failed to get auth, please try again".localized())
+                        }
                     }
-                } else if !response.isOk() {
+                } else {
                     DispatchQueue.main.async {
                         self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
                     }
-                } else {
-                    self.successSubmit(response: response, name: name)
-                }
-            } else {
-                DispatchQueue.main.async {
-                    self.showFailedSignUpIn(title: "Unable to access servers. Try again later".localized())
                 }
+            } catch {
+                
             }
         }
     }
@@ -538,38 +632,41 @@ public class ChangeDeviceViewController: UIViewController {
             })
             return
         }
-        self.deleteAllRecordDatabase()
         if(!id.isEmpty) {
-//                            Nexilis.changeUser(f_pin: device_id)
-            SecureUserDefaults.shared.set(device_id, forKey: "device_id")
+            SecureUserDefaults.shared.set(f_pin, forKey: "me")
             Utils.setProfile(value: true)
             // pos registration
+            self.deleteAllRecordDatabase()
             _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: id))
-            DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
-                Nexilis.hideLoader(completion: {
-                    let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
-                    imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "Successfully Sign-In".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
-                    banner.show()
-                    if Nexilis.showFB {
-                        Nexilis.floatingButton.removeFromSuperview()
-                        FloatingButton.datePull = nil
-                        Nexilis.floatingButton = FloatingButton()
-                        Nexilis.addFB()
-                    }
-                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
-                    if self.forceLogin {
-                        self.navigationController?.dismiss(animated: true)
-                    } else {
-                        let controllers = self.navigationController?.viewControllers
-                        if controllers![controllers!.count - 2] is SignInOption {
-                            self.navigationController?.popToViewController(controllers![controllers!.count - 3], animated: true)
-                        } else {
-                            self.navigationController?.popViewController(animated: true)
+            Nexilis.setInitCallback() { res in
+                if res == 1 {
+                    Nexilis.successSui?()
+                    closePage()
+                }
+            }
+            Nexilis.startConnect(withInit: false)
+            func closePage() {
+                DispatchQueue.main.async {
+                    Nexilis.hideLoader(completion: {
+                        let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Successfully Sign-In".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
+                        banner.show()
+                        if Nexilis.showFB {
+                            Nexilis.floatingButton.removeFromSuperview()
+                            FloatingButton.datePull = nil
+                            Nexilis.floatingButton = FloatingButton()
+                            Nexilis.addFB()
                         }
-                    }
-                })
-            })
+                        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
+                        self.navigationController?.dismiss(animated: true) {
+                            if self.isEmail {
+                                Nexilis.showPassSignIn(isFromSU: true)
+                            }
+                        }
+                    })
+                }
+            }
         }
     }
 

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

@@ -46,7 +46,7 @@ class ChangePasswordViewController: UIViewController {
     }
     
     @objc func didTapNext(sender: Any) {
-        guard let oldPassword = oldPassField.text, !oldPassword.isEmpty else {
+        guard let oldPassword = oldPassField.text, !oldPassword.trimmingCharacters(in: .whitespaces).isEmpty else {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
             let banner = FloatingNotificationBanner(title: "Old password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
@@ -66,7 +66,7 @@ class ChangePasswordViewController: UIViewController {
 //            banner.show()
 //            return
 //        }
-        guard let newPassword = newPassField.text, !newPassword.isEmpty else {
+        guard let newPassword = newPassField.text, !newPassword.trimmingCharacters(in: .whitespaces).isEmpty else {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
             let banner = FloatingNotificationBanner(title: "New password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)

+ 8 - 0
NexilisLite/NexilisLite/Source/View/Control/GroupDetailViewController.swift

@@ -196,6 +196,7 @@ class GroupDetailViewController: UITableViewController, UITextFieldDelegate {
                 }
             }
         })
+        submitAction.isEnabled = false
         self.alert2?.addAction(submitAction)
         self.alert2?.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
         
@@ -208,6 +209,13 @@ class GroupDetailViewController: UITableViewController, UITextFieldDelegate {
                   let stringRange = Range(range, in: currentText) else {
                 return false
             }
+            if textField == alertTextFieldSubGroup {
+                if currentText.trimmingCharacters(in: .whitespaces).isEmpty {
+                    self.alert2?.actions[0].isEnabled = false
+                } else {
+                    self.alert2?.actions[0].isEnabled = true
+                }
+            }
 
             let updatedText = currentText.replacingCharacters(in: stringRange, with: string)
             return updatedText.count <= (textField == alertTextFieldSubGroup ? 100 : 50)

+ 7 - 1
NexilisLite/NexilisLite/Source/View/Control/MFAViewController.swift

@@ -308,7 +308,13 @@ class MFAViewController: UIViewController {
                     }
                     return
                 }
-                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
+                var id = ""
+                if Utils.isMiddleMode() || Utils.isHSAMode() {
+                    id = Nexilis.justInit()
+                } else {
+                    id = User.getMyPin() ?? ""
+                }
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
                     if response.isOk() {
                         let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
                         if data.isEmpty {

+ 3 - 3
NexilisLite/NexilisLite/Source/View/Control/SignInOption.swift

@@ -15,7 +15,7 @@ public class SignInOption: UIViewController {
     private let containerView = UIStackView()
     
     public override func viewDidLoad() {
-        if forceSignIn {
+        if Utils.isHSAMode() {
             self.title = "Sign-In Method".localized()
         } else {
             self.title = "Sign-Up/Sign-In Method".localized()
@@ -55,7 +55,7 @@ public class SignInOption: UIViewController {
         let title = UILabel()
         self.view.addSubview(title)
         title.anchor(top: iconIV.bottomAnchor, paddingTop: 10, centerX: iconIV.centerXAnchor)
-        if forceSignIn {
+        if Utils.isHSAMode() {
             title.text = "Choose your Sign-In method :".localized()
         } else {
             title.text = "Choose your Sign-Up or Sign-In method :".localized()
@@ -113,7 +113,7 @@ public class SignInOption: UIViewController {
     }
     
     @objc func didTapSignIn(sender: UIButton) {
-        if !forceSignIn {
+        if !Utils.isHSAMode() {
             let vc = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "signupsignin") as! SignUpSignIn
             if sender.tag == 0 {
                 vc.isMSISDN = true

+ 208 - 115
NexilisLite/NexilisLite/Source/View/Control/SignUpSignIn.swift

@@ -19,9 +19,14 @@ public class SignUpSignIn: UIViewController {
     @IBOutlet weak var topConstDesc: NSLayoutConstraint!
     
     public var forceLogin = false
+    public var forceSignIn = false
     public var isEmail = false
     public var isMSISDN = false
     
+    private var nameReg = ""
+    private var passReg = ""
+    private var isBioMetricOnReg = false
+    
     public override func viewDidLoad() {
         super.viewDidLoad()
 
@@ -56,18 +61,16 @@ public class SignUpSignIn: UIViewController {
         let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
         tapGesture.cancelsTouchesInView = false
         view.addGestureRecognizer(tapGesture)
+        self.title = "Sign-Up/Sign-In".localized()
         if isEmail || isMSISDN {
-            self.title = "Sign-In".localized()
             passwordField.isHidden = true
             showPasswordButton.isHidden = true
             if isMSISDN{
                 usernameField.keyboardType = .numberPad
             }
-        } else {
-            self.title = "Sign-Up/Sign-In".localized()
         }
         let controllers = self.navigationController?.viewControllers
-        if forceLogin && (controllers!.count < 2 || !(controllers![controllers!.count - 2] is SignInOption)) {
+        if forceLogin && !forceSignIn && (controllers!.count < 2 || !(controllers![controllers!.count - 2] is SignInOption)) {
             navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(didTapCancel(sender:)))
         }
         self.navigationController?.navigationBar.topItem?.backButtonTitle = ""
@@ -83,7 +86,7 @@ public class SignUpSignIn: UIViewController {
         navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance
         navigationController?.navigationBar.tintColor = .white
         
-        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Submit".localized(), style: .plain, target: self, action: #selector(didTapSubmit(sender:)))
+        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Submit".localized(), style: .plain, target: self, action: #selector(checkSubmit))
     }
     
     @objc func didTapCancel(sender: Any) {
@@ -124,13 +127,10 @@ public class SignUpSignIn: UIViewController {
     }
     
     func checkEmail(email: String) {
-        if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
-            self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
-            return
-        }
         Nexilis.showLoader()
         DispatchQueue.global().async {
-            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendOTPLogin(p_email: email), timeout: 30 * 1000) {
+            let id = Nexilis.justInit()
+            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendOTPLogin(p_email: email, xpin: id), timeout: 30 * 1000) {
                 if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") != "00" {
                     DispatchQueue.main.async {
                         self.showFailedSignUpIn(title: "Unregistered email account".localized())
@@ -155,10 +155,6 @@ public class SignUpSignIn: UIViewController {
         if number.hasPrefix("0") {
             number = number.replacingCharacters(in: number.startIndex...number.startIndex, with: "+62")
         }
-        if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
-            self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
-            return
-        }
         self.showOTPSelectionAlert(self, number)
     }
     
@@ -168,13 +164,22 @@ public class SignUpSignIn: UIViewController {
                                       preferredStyle: .actionSheet) // use .alert if you want centered popup
         
         alert.addAction(UIAlertAction(title: "📩 SMS", style: .default, handler: { _ in
+            if !CheckConnection.isConnectedToNetwork() {
+                self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
+                return
+            }
             self.sendOTP(to: number)
         }))
         
         alert.addAction(UIAlertAction(title: "💬 WhatsApp", style: .default, handler: { _ in
+            if !CheckConnection.isConnectedToNetwork() {
+                self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
+                return
+            }
             Nexilis.showLoader()
             DispatchQueue.global().async {
-                if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendOTPLogin(p_number: number), timeout: 30 * 1000) {
+                let id = Nexilis.justInit()
+                if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendOTPLogin(p_number: number, xpin: id), timeout: 30 * 1000) {
                     if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") != "00" {
                         DispatchQueue.main.async {
                             self.showFailedSignUpIn(title: "Unregistered phone number".localized())
@@ -234,7 +239,13 @@ public class SignUpSignIn: UIViewController {
         func sendSVL() {
             DispatchQueue.global().async {
                 do {
-                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
+                    var id = ""
+                    if Utils.isMiddleMode() {
+                        id = Nexilis.justInit()
+                    } else {
+                        id = User.getMyPin() ?? ""
+                    }
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
                         if response.isOk() {
                             let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
                             if data.isEmpty {
@@ -255,7 +266,7 @@ public class SignUpSignIn: UIViewController {
                                 pk = publicKey
                             }
                             let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 300)
-                            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 let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: "", p_vercode: method == 0 ? "" : code, xpin: id, number: number, deviceFingerprint: df, publicKey: pk, signature: sign, totp: otp), timeout: 30 * 1000) {
                                 if !response.isOk() {
                                     if method == 1 {
                                         DispatchQueue.main.async {
@@ -298,29 +309,28 @@ public class SignUpSignIn: UIViewController {
         showOTPVC.isMSISDN = !phone.isEmpty
         showOTPVC.showWrongOTP = errCode
         showOTPVC.method = method
+        let isBiometricOn = self.isBioMetricOnReg
         showOTPVC.isDismiss = { code in
-            if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
+            if !CheckConnection.isConnectedToNetwork() {
                 self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
                 return
             }
             if KeyManagerNexilis.hasGeneratedKey() {
+                SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
                 KeyManagerNexilis.deleteKey()
                 KeyManagerNexilis.deleteMarker()
             }
             KeyManagerNexilis.generateKey()
             KeyManagerNexilis.saveMarker()
-            let policyLevel = Utils.getSignInLevel()
-            var isBiometricOn = false
-            if policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFACE || policyLevel == MFAViewController.STEP_FIDO_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_BIOFACE {
-                isBiometricOn = true
-            }
-            guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: isBiometricOn) else {
+            guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: isBiometricOn, isSaveState: isBiometricOn) else {
+                SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
                 KeyManagerNexilis.deleteKey()
                 KeyManagerNexilis.deleteMarker()
                 UIApplication.shared.visibleViewController?.view.makeToast("Biometric or passcode authentication required".localized(), duration: 3, position: .center)
                 return
             }
-            if Database.shared.openDatabase() == 0 {
+            if Database.shared.openDatabase() == 0 && !Utils.isMiddleMode() {
+                SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
                 APIS.showRestartApp()
                 KeyManagerNexilis.deleteKey()
                 KeyManagerNexilis.deleteMarker()
@@ -333,7 +343,13 @@ public class SignUpSignIn: UIViewController {
             }
             DispatchQueue.global().async {
                 do {
-                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
+                    var id = ""
+                    if Utils.isMiddleMode() {
+                        id = Nexilis.justInit()
+                    } else {
+                        id = User.getMyPin() ?? ""
+                    }
+                    if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
                         if response.isOk() {
                             let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
                             if data.isEmpty {
@@ -354,7 +370,7 @@ public class SignUpSignIn: UIViewController {
                                 pk = publicKey
                             }
                             let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 300)
-                            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 let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSendVerifyChangeDevice(p_email: email, p_vercode: code, xpin: id, deviceFingerprint: df, publicKey: pk, signature: sign, totp: otp), timeout: 30 * 1000) {
                                 if !response.isOk() {
                                     DispatchQueue.main.async {
                                         Nexilis.hideLoader {
@@ -389,10 +405,25 @@ public class SignUpSignIn: UIViewController {
         })
     }
     
-    private func showFailedSignUpIn(title: String, withLoader: Bool = true) {
+    private func showFailedSignUpIn(title: String, withLoader: Bool = true, isAlert: Bool = false) {
+        SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
         KeyManagerNexilis.deleteKey()
         KeyManagerNexilis.deleteMarker()
-        if withLoader {
+        if isAlert {
+            Nexilis.hideLoader(completion: {
+                let alert = UIAlertController(title: title,
+                                              message: "Do you want to create new user with this password?".localized(),
+                                              preferredStyle: .alert) // use .alert if you want centered popup
+                
+                alert.addAction(UIAlertAction(title: "Yes".localized(), style: .default, handler: { [self] _ in
+                    didTapSubmit(forceSU: true)
+                }))
+                
+                alert.addAction(UIAlertAction(title: "No".localized(), style: .cancel, handler: nil))
+
+                self.present(alert, animated: true)
+            })
+        } else if withLoader {
             Nexilis.hideLoader(completion: {
                 let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                 imageView.tintColor = .white
@@ -407,7 +438,7 @@ public class SignUpSignIn: UIViewController {
         }
     }
     
-    @objc func didTapSubmit(sender: Any) {
+    @objc func checkSubmit(sender: Any?) {
         guard let name = usernameField.text, !name.isEmpty else {
             var text = "Username"
             if isEmail {
@@ -447,15 +478,13 @@ public class SignUpSignIn: UIViewController {
             return
         }
         
-        let idMe = User.getMyPin()!
-
         if !name.matches("^[a-zA-Z0-9 ]*$") {
             self.showFailedSignUpIn(title: "Contains prohibited characters. Only alphabetic characters are allowed.".localized(), withLoader: false)
             return
         }
         let password = passwordField.text ?? ""
         if !passwordField.isHidden {
-            if password.isEmpty {
+            if password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                 self.showFailedSignUpIn(title: "Password can't be empty".localized(), withLoader: false)
                 return
             }
@@ -464,28 +493,55 @@ public class SignUpSignIn: UIViewController {
                 return
             }
         }
-        if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
-            self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
-            return
+        self.nameReg = name
+        self.passReg = password
+        if Utils.isMiddleMode() {
+            let alert = UIAlertController(title: "Touch/Face ID(Optional)".localized(),
+                                          message: "Do you want to use Touch/Face ID for next Sign-In?".localized(),
+                                          preferredStyle: .alert) // use .alert if you want centered popup
+            
+            alert.addAction(UIAlertAction(title: "Yes".localized(), style: .default, handler: { [self] _ in
+                self.isBioMetricOnReg = true
+                didTapSubmit()
+            }))
+            
+            alert.addAction(UIAlertAction(title: "No".localized(), style: .cancel, handler: { [self] _ in
+                didTapSubmit()
+            }))
+            
+            self.present(alert, animated: true)
+        } else {
+            if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
+                self.showFailedSignUpIn(title: "Check your connection".localized(), withLoader: false)
+                return
+            }
+            didTapSubmit()
         }
+    }
+    
+    private func didTapSubmit(forceSU: Bool = false) {
+        let name = self.nameReg
+        let password = self.passReg
+        let isBiometricOn = self.isBioMetricOnReg
+        let a = name.split(separator: " ", maxSplits: 1)
+        let first = String(a[0])
+        let last = a.count == 2 ? String(a[1]) : ""
         if KeyManagerNexilis.hasGeneratedKey() {
+            SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
             KeyManagerNexilis.deleteKey()
             KeyManagerNexilis.deleteMarker()
         }
         KeyManagerNexilis.generateKey()
         KeyManagerNexilis.saveMarker()
-        let policyLevel = Utils.getSignInLevel()
-        var isBiometricOn = false
-        if policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_PWD_BIOFACE || policyLevel == MFAViewController.STEP_FIDO_BIOFINGER || policyLevel == MFAViewController.STEP_FIDO_BIOFACE {
-            isBiometricOn = true
-        }
-        guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: isBiometricOn) else {
+        guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: isBiometricOn, isSaveState: isBiometricOn) else {
+            SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
             KeyManagerNexilis.deleteKey()
             KeyManagerNexilis.deleteMarker()
             UIApplication.shared.visibleViewController?.view.makeToast("Biometric or passcode authentication required".localized(), duration: 3, position: .center)
             return
         }
-        if Database.shared.openDatabase() == 0 {
+        if Database.shared.openDatabase() == 0 && !Utils.isMiddleMode() {
+            SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
             APIS.showRestartApp()
             KeyManagerNexilis.deleteKey()
             KeyManagerNexilis.deleteMarker()
@@ -494,7 +550,14 @@ public class SignUpSignIn: UIViewController {
         Nexilis.showLoader()
         DispatchQueue.global().async {
             do {
-                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger()) {
+                var id = ""
+                if Utils.isMiddleMode() {
+                    id = Nexilis.justInit()
+                } else {
+                    id = User.getMyPin() ?? ""
+                }
+//                print("MASUK IDNYALOH: \(id)")
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
                     if response.isOk() {
                         let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
                         if data.isEmpty {
@@ -507,19 +570,19 @@ public class SignUpSignIn: UIViewController {
                         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 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
                         }
                         let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 300)
-                        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 let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getSignUpSignInAPI(p_name: name, p_password: md5Hex, xPin: id, deviceFingerprint: df, publicKey: pk, signature: sign, totp: otp, forceSU: forceSU ? "1" : ""), 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())
+                                    self.showFailedSignUpIn(title: "Invalid user / Username and password does not match".localized(), isAlert: true)
                                 }
                             } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "11" {
                                 DispatchQueue.main.async {
@@ -562,13 +625,14 @@ public class SignUpSignIn: UIViewController {
     }
     
     private func successSubmit(response: TMessage, first: String, last: String, email: String = "", number: String = "") {
+//        print("response successSubmit: \(response.toLogString())")
         let sign = response.getBody(key: CoreMessage_TMessageKey.SIGN, default_value: "")
+        let id = response.getBody(key: CoreMessage_TMessageKey.F_PIN, default_value: "")
+        let f_pin = response.getBody(key: CoreMessage_TMessageKey.F_PIN_REAL, default_value: "")
+        let device_id = response.getBody(key: CoreMessage_TMessageKey.IMEI, default_value: id)
+        let last_sign = response.getBody(key: CoreMessage_TMessageKey.LAST_SIGN, default_value: "0")
         if sign == "1" || !email.isEmpty || !number.isEmpty {
-            let id = response.getBody(key: CoreMessage_TMessageKey.F_PIN, default_value: "")
-            let f_pin = response.getBody(key: CoreMessage_TMessageKey.F_PIN_REAL, default_value: "")
-            let device_id = response.getBody(key: CoreMessage_TMessageKey.IMEI, default_value: id)
-            let last_sign = response.getBody(key: CoreMessage_TMessageKey.LAST_SIGN, default_value: "0")
-            //print("last sign: \(last_sign)")
+//            print("last sign: \(last_sign)")
             if last_sign != "0" {
                 Utils.setLoginMultipleFPin(value: f_pin)
                 DispatchQueue.main.async {
@@ -606,26 +670,101 @@ public class SignUpSignIn: UIViewController {
                 })
                 return
             }
-            self.deleteAllRecordDatabase()
             if(!id.isEmpty) {
-//                            Nexilis.changeUser(f_pin: device_id)
-                SecureUserDefaults.shared.set(device_id, forKey: "device_id")
+                SecureUserDefaults.shared.set(f_pin, forKey: "me")
                 Utils.setProfile(value: true)
                 // pos registration
-                _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: id))
-                DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
+                if !Utils.isMiddleMode() {
+                    self.deleteAllRecordDatabase()
+                    _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: id))
+                }
+                if Utils.isMiddleMode() {
+                    Nexilis.setInitCallback() { res in
+                        if res == 1 {
+                            Nexilis.successSui?()
+                            closePage()
+                        }
+                    }
+                    Nexilis.startConnect(withInit: false)
+                } else {
+                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
+                    closePage()
+                }
+                func closePage() {
+                    DispatchQueue.main.async {
+                        Nexilis.hideLoader(completion: {
+                            let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Successfully Sign-In".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
+                            banner.show()
+                            if Nexilis.showFB {
+                                Nexilis.floatingButton.removeFromSuperview()
+                                FloatingButton.datePull = nil
+                                Nexilis.floatingButton = FloatingButton()
+                                Nexilis.addFB()
+                            }
+                            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
+                            if self.forceLogin {
+                                self.navigationController?.dismiss(animated: true)
+                            } else {
+                                let controllers = self.navigationController?.viewControllers
+                                if controllers![controllers!.count - 2] is SignInOption {
+                                    self.navigationController?.popToViewController(controllers![controllers!.count - 3], animated: true)
+                                } else {
+                                    self.navigationController?.popViewController(animated: true)
+                                }
+                            }
+                            Nexilis.getFeatureAccess()
+                        })
+                    }
+                }
+            }
+        } else {
+            if !Utils.isMiddleMode() {
+                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                    var firstN = first
+                    if firstN.isEmpty {
+                        if !email.isEmpty {
+                            firstN = email
+                        } else {
+                            firstN = number
+                        }
+                    }
+                    do {
+                        if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT * FROM BUDDY where f_pin = '\(f_pin)' ") {
+                            if !cursorData.next() {
+                                _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: f_pin))
+                            } else {
+                                _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: ["first_name": first , "last_name": last], _where: "f_pin = '\(f_pin)'")
+                            }
+                            cursorData.close()
+                        }
+                    } catch {
+                        rollback.pointee = true
+                    }
+                })
+            }
+            SecureUserDefaults.shared.set(f_pin, forKey: "me")
+            Utils.setProfile(value: true)
+            if Utils.isMiddleMode() {
+                Nexilis.setInitCallback() { res in
+                    if res == 1 {
+                        Nexilis.successSui?()
+                        closePage()
+                    }
+                }
+                Nexilis.startConnect(withInit: false)
+            } else {
+                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
+                closePage()
+            }
+            func closePage() {
+                DispatchQueue.main.async {
                     Nexilis.hideLoader(completion: {
                         let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
                         imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "Successfully Sign-In".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
+                        let banner = FloatingNotificationBanner(title: "Successfully Sign-Up".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
                         banner.show()
-                        if Nexilis.showFB {
-                            Nexilis.floatingButton.removeFromSuperview()
-                            FloatingButton.datePull = nil
-                            Nexilis.floatingButton = FloatingButton()
-                            Nexilis.addFB()
-                        }
-                        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
                         if self.forceLogin {
                             self.navigationController?.dismiss(animated: true)
                         } else {
@@ -636,54 +775,8 @@ public class SignUpSignIn: UIViewController {
                                 self.navigationController?.popViewController(animated: true)
                             }
                         }
-                        Nexilis.getFeatureAccess()
                     })
-                })
-            }
-        } else {
-            let idMe = User.getMyPin()!
-            Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                var firstN = first
-                if firstN.isEmpty {
-                    if !email.isEmpty {
-                        firstN = email
-                    } else {
-                        firstN = number
-                    }
                 }
-                do {
-                    if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT * FROM BUDDY where f_pin = '\(idMe)' ") {
-                        if !cursorData.next() {
-                            _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: idMe))
-                        } else {
-                            _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: ["first_name": first , "last_name": last], _where: "f_pin = '\(idMe)'")
-                        }
-                        cursorData.close()
-                    }
-                } catch {
-                    rollback.pointee = true
-                    print("Access database error: \(error.localizedDescription)")
-                }
-            })
-            Utils.setProfile(value: true)
-            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil, userInfo: nil)
-            DispatchQueue.main.async {
-                Nexilis.hideLoader(completion: {
-                    let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
-                    imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "Successfully Sign-Up".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
-                    banner.show()
-                    if self.forceLogin {
-                        self.navigationController?.dismiss(animated: true)
-                    } else {
-                        let controllers = self.navigationController?.viewControllers
-                        if controllers![controllers!.count - 2] is SignInOption {
-                            self.navigationController?.popToViewController(controllers![controllers!.count - 3], animated: true)
-                        } else {
-                            self.navigationController?.popViewController(animated: true)
-                        }
-                    }
-                })
             }
         }
     }

+ 439 - 0
NexilisLite/NexilisLite/Source/View/Control/TFAPasswordVC.swift

@@ -0,0 +1,439 @@
+//
+//  TFAPasswordVC.swift
+//  Pods
+//
+//  Created by Qindi on 22/10/25.
+//
+
+import UIKit
+import Foundation
+import os
+import LocalAuthentication
+import nuSDKService
+
+
+class TFAPasswordVC: UIViewController {
+    
+    static let STEP_FIDO = "1";
+    static let STEP_FIDO_PWD = "1,2";
+    static let STEP_FIDO_PWD_BIOFINGER = "1,2,3";
+    static let STEP_FIDO_PWD_BIOFACE = "1,2,4";
+    static let STEP_FIDO_BIOFINGER = "1,3";
+    static let STEP_FIDO_BIOFACE = "1,4";
+    
+    var STEP_NEEDED = STEP_FIDO_PWD
+    var METHOD = "Sign In"
+
+    private let imageViewBackground = UIImageView()
+    private let scrollView = UIScrollView()
+    private let mainStackView = UIStackView()
+    private let headerImageView1 = UIImageView()
+    private let headerImageView2 = UIImageView()
+    private let headerTitleLabel = UILabel()
+    private let subtitleLabel = UILabel()
+    private let passwordTextField = UITextField()
+    private let passwordVisibilityButton = UIButton(type: .system)
+    private let poweredStackView = UIStackView()
+    private let noteLabel = UILabel()
+    private let poweredLabel = UILabel()
+    private let poweredImageView = UIImageView()
+
+    private var isPasswordVisible = false
+    var isFromSU = false
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        if isFromSU {
+            SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
+        }
+        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()
+        DispatchQueue.global().async {
+            if Utils.isMiddleMode() && Utils.getBiometricState() != nil {
+                self.biometricAuth()
+            }
+        }
+    }
+    
+    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) {
+        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
+    private func setupUI() {
+        view.backgroundColor = .systemBackground
+
+        // Background Image View
+        imageViewBackground.contentMode = .scaleAspectFill
+        imageViewBackground.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(imageViewBackground)
+
+        // Scroll View
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(scrollView)
+
+        // Main Stack View
+        mainStackView.axis = .vertical
+        mainStackView.alignment = .center
+        mainStackView.spacing = 16
+        mainStackView.translatesAutoresizingMaskIntoConstraints = false
+        scrollView.addSubview(mainStackView)
+
+        // Header Images
+        headerImageView1.contentMode = .scaleAspectFit
+        headerImageView1.image = UIImage(named: "pb_ic_attach_spc_badge", 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", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+        headerImageView2.heightAnchor.constraint(equalToConstant: 200).isActive = true
+        mainStackView.addArrangedSubview(headerImageView2)
+
+        // Header Title Label
+        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 = .systemFont(ofSize: 12)
+        subtitleLabel.textAlignment = .center
+        subtitleLabel.numberOfLines = 0
+        mainStackView.addArrangedSubview(subtitleLabel)
+        
+        // Password Input Container
+        let passwordContainerView = UIView()
+        passwordContainerView.translatesAutoresizingMaskIntoConstraints = false
+        passwordContainerView.widthAnchor.constraint(equalToConstant: 300).isActive = true
+        passwordContainerView.heightAnchor.constraint(equalToConstant: 48).isActive = true
+        mainStackView.addArrangedSubview(passwordContainerView)
+        
+        // Password Text Field
+        passwordTextField.placeholder = "Type your password..."
+        passwordTextField.isSecureTextEntry = true
+        passwordTextField.font = .systemFont(ofSize: 15)
+        passwordTextField.borderStyle = .roundedRect
+        passwordTextField.keyboardType = .default
+        passwordTextField.autocapitalizationType = .none
+        passwordTextField.autocorrectionType = .no
+        passwordTextField.translatesAutoresizingMaskIntoConstraints = false
+        passwordContainerView.addSubview(passwordTextField)
+        
+        // Password Visibility Button
+        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)
+        
+        if isFromSU {
+            print("MASUK SINI GA?")
+            noteLabel.attributedText = "_Note: If you have not changed the password provided by this user or are unsure of it, please note that the default password is *abcd1234*_".localized().richText()
+            noteLabel.numberOfLines = 0
+            mainStackView.addArrangedSubview(noteLabel)
+        }
+        
+        
+        mainStackView.setCustomSpacing(12, after: subtitleLabel)
+        mainStackView.setCustomSpacing(24, after: passwordContainerView)
+
+        // Powered By StackView
+        poweredStackView.axis = .horizontal
+        poweredStackView.alignment = .center
+        poweredStackView.spacing = 8
+        poweredStackView.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(poweredStackView)
+
+        poweredLabel.text = "Powered by"
+        poweredLabel.font = .systemFont(ofSize: 12)
+        poweredStackView.addArrangedSubview(poweredLabel)
+
+        poweredImageView.contentMode = .scaleAspectFit
+        poweredImageView.image = UIImage(named: "pb_powered_button")
+        poweredImageView.widthAnchor.constraint(equalToConstant: 25).isActive = true
+        poweredImageView.heightAnchor.constraint(equalToConstant: 25).isActive = true
+        poweredStackView.addArrangedSubview(poweredImageView)
+    }
+
+    private func setupLayout() {
+        NSLayoutConstraint.activate([
+            // Background
+            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),
+
+            // Scroll View
+            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
+            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
+            scrollView.bottomAnchor.constraint(equalTo: poweredStackView.topAnchor, constant: -8),
+
+            // Main Stack View
+            mainStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20),
+            mainStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
+            mainStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
+            mainStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
+            mainStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
+            
+            // Password Field
+            passwordTextField.leadingAnchor.constraint(equalTo: passwordTextField.superview!.leadingAnchor),
+            passwordTextField.trailingAnchor.constraint(equalTo: passwordTextField.superview!.trailingAnchor),
+            passwordTextField.topAnchor.constraint(equalTo: passwordTextField.superview!.topAnchor),
+            passwordTextField.bottomAnchor.constraint(equalTo: passwordTextField.superview!.bottomAnchor),
+            
+            // Password Visibility Button
+            passwordVisibilityButton.trailingAnchor.constraint(equalTo: passwordTextField.trailingAnchor, constant: -8),
+            passwordVisibilityButton.centerYAnchor.constraint(equalTo: passwordTextField.centerYAnchor),
+            passwordVisibilityButton.widthAnchor.constraint(equalToConstant: 40),
+            passwordVisibilityButton.heightAnchor.constraint(equalToConstant: 40),
+
+            // Powered by
+            poweredStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
+            poweredStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8),
+        ])
+    }
+    
+    // MARK: - Data & Logic
+    private func loadData() {
+        let poweredText = "Nexilis"
+        if !poweredText.isEmpty {
+            poweredLabel.text = "Powered by \(poweredText)"
+        }
+    }
+    
+    // MARK: - Actions
+    @objc private func togglePasswordVisibility() {
+        isPasswordVisible.toggle()
+        passwordTextField.isSecureTextEntry = !isPasswordVisible
+        let iconName = isPasswordVisible ? "eye.fill" : "eye.slash.fill"
+        passwordVisibilityButton.setImage(UIImage(systemName: iconName), for: .normal)
+    }
+
+    @objc private func submitAction() {
+        guard let password = passwordTextField.text, !password.trimmingCharacters(in: .whitespaces).isEmpty else {
+            self.view.makeToast("Password cannot be empty.".localized(), duration: 2.0, position: .center)
+            return
+        }
+
+        guard password.count >= 6 else {
+            self.view.makeToast("Password must be at least 6 characters.".localized(), duration: 2.0, position: .center)
+            return
+        }
+
+        submit()
+    }
+
+    private func submit(fromBiometric: Bool = false) {
+        guard let password = passwordTextField.text else { return }
+        Nexilis.showLoader()
+        
+        DispatchQueue.global().async {
+            do {
+                // 1. Encrypt password
+                let encryptedPwd = password
+                
+                // 2. Create message for the server
+                let me = User.getMyPin() ?? ""
+                let tMessage = CoreMessage_TMessageBank.getMFAValidation(data: me)
+                if !fromBiometric {
+                    tMessage.mBodies[CoreMessage_TMessageKey.PSWD] = encryptedPwd
+                }
+                tMessage.mBodies[CoreMessage_TMessageKey.SUBMIT_DATE] = "\(Date().currentTimeMillis())"
+                tMessage.mBodies[CoreMessage_TMessageKey.ACTVITY] = self.METHOD
+                guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: Utils.isHSAMode()) else {
+                    DispatchQueue.main.async {
+                        Nexilis.hideLoader {
+                            let errorMessage = "Biometric Failed".localized()
+                            let dialog = DialogErrorMFA()
+                            dialog.modalTransitionStyle = .crossDissolve
+                            dialog.modalPresentationStyle = .overCurrentContext
+                            dialog.errorDesc = errorMessage
+                            dialog.method = self.METHOD
+                            UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                        }
+                    }
+                    return
+                }
+                var id = ""
+                if Utils.isMiddleMode() || Utils.isHSAMode() {
+                    id = Nexilis.justInit()
+                } else {
+                    id = User.getMyPin() ?? ""
+                }
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
+                    if response.isOk() {
+                        let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
+                        if data.isEmpty {
+                            DispatchQueue.main.async {
+                                Nexilis.hideLoader {
+                                    let errorMessage = "Auth Failure".localized()
+                                    let dialog = DialogErrorMFA()
+                                    dialog.modalTransitionStyle = .crossDissolve
+                                    dialog.modalPresentationStyle = .overCurrentContext
+                                    dialog.errorDesc = errorMessage
+                                    dialog.method = self.METHOD
+                                    UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                                }
+                            }
+                            return
+                        }
+                        let df = HMACDeviceFingerprintNexilis.generate()
+                        tMessage.mBodies[CoreMessage_TMessageKey.FINGERPRINT] = df
+                        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
+                        let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 300)
+                        tMessage.mBodies[CoreMessage_TMessageKey.TOTP] = otp
+                        if let response = Nexilis.writeAndWait(message: tMessage) {
+                            if response.isOk() {
+                                if Utils.isMiddleMode() && Utils.getBiometricState() == nil {
+                                    SecureUserDefaults.shared.set(Date(), forKey: "lastAuthenticationTime")
+                                }
+                                Nexilis.setInitCallback() { res in
+                                    if res == 1 {
+                                        Nexilis.successSui?()
+                                        closePage()
+                                    }
+                                }
+                                Nexilis.startConnect(withInit: false)
+                                func closePage() {
+                                    DispatchQueue.main.async {
+                                        Nexilis.hideLoader {
+                                            self.navigationController?.dismiss(animated: true, completion: {
+                                                UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
+                                                self.dismissKeyboard()
+                                            })
+                                        }
+                                    }
+                                }
+                            }
+                            else {
+                                DispatchQueue.main.async {
+                                    Nexilis.hideLoader {
+                                        let errorMessage = response.getBody(key: CoreMessage_TMessageKey.MESSAGE_TEXT, default_value: "Auth Failure".localized())
+                                        let dialog = DialogErrorMFA()
+                                        dialog.modalTransitionStyle = .crossDissolve
+                                        dialog.modalPresentationStyle = .overCurrentContext
+                                        dialog.errorDesc = errorMessage
+                                        dialog.method = self.METHOD
+                                        UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                                    }
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        Nexilis.hideLoader {
+                            let errorMessage = "Unable to access servers. Check your internet connection and try again later".localized()
+                            let dialog = DialogErrorMFA()
+                            dialog.modalTransitionStyle = .crossDissolve
+                            dialog.modalPresentationStyle = .overCurrentContext
+                            dialog.errorDesc = errorMessage
+                            dialog.method = self.METHOD
+                            UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                        }
+                    }
+                }
+            } catch {
+                
+            }
+        }
+    }
+
+    private func biometricAuth() {
+        let semaphore = DispatchSemaphore(value: 0)
+        var result = true
+        var stateErr = 0
+        let manager = BiometricStateManager()
+        manager.hasBiometricStateChanged { (res, state) in
+            result = res
+            stateErr = state
+            semaphore.signal()
+        }
+        
+        semaphore.wait()
+
+        if result {
+            if Utils.isMiddleMode() {
+                DispatchQueue.main.async {
+                    self.submit(fromBiometric: true)
+                }
+            }
+        } else if stateErr == 1 {
+            DispatchQueue.main.async {
+                Nexilis.hideLoader {
+                    let errorMessage = "Terjadi Perubahan Biometric (Touch/Face ID)"
+                    let dialog = DialogErrorMFA()
+                    dialog.modalTransitionStyle = .crossDissolve
+                    dialog.modalPresentationStyle = .overCurrentContext
+                    dialog.errorDesc = errorMessage
+                    dialog.method = self.METHOD
+                    dialog.hideTryAgain = (stateErr == 1)
+                    UIApplication.shared.visibleViewController?.present(dialog, animated: true)
+                }
+            }
+        }
+    }
+    
+    private func updateUIBasedOnMethod() {
+        headerTitleLabel.text = METHOD
+    }
+}